Explorar o código

1. 奉建计算
2. 安全生产费编辑相关0.1

MaiXinRong hai 1 ano
pai
achega
0ea4fabdb6

+ 47 - 36
app/base/base_tree_service.js

@@ -33,7 +33,6 @@ class TreeService extends Service {
      */
     constructor(ctx, setting) {
         super(ctx);
-        this.rootId = -1;
         this.tableName = setting.tableName;
         this.setting = setting;
         // 以下字段仅可通过树结构操作改变,不可直接通过update方式从接口提交,发现时过滤
@@ -47,6 +46,10 @@ class TreeService extends Service {
         this.readOnlyFields.push(this.setting.isLeaf);
     }
 
+    get rootId() {
+        return rootId;
+    }
+
     getCondition (condition) {
         const result = {};
         if (condition.mid) result[this.setting.mid] = condition.mid;
@@ -139,7 +142,7 @@ class TreeService extends Service {
             value: pid,
             operate: '=',
         });
-        this.sqlBuilder.orderBy = [['order', 'DESC']];
+        this.sqlBuilder.orderBy = [[this.setting.order, 'DESC']];
         const [sql, sqlParam] = this.sqlBuilder.build(this.tableName);
         const resultData = await this.db.queryOne(sql, sqlParam);
 
@@ -179,7 +182,7 @@ class TreeService extends Service {
             value: order2,
             operate: '<',
         });
-        this.sqlBuilder.orderBy = [['order', 'ASC']];
+        this.sqlBuilder.orderBy = [[this.setting.order, 'ASC']];
 
         const [sql, sqlParam] = this.sqlBuilder.build(this.tableName);
         const data = await this.db.query(sql, sqlParam);
@@ -208,7 +211,7 @@ class TreeService extends Service {
             value: order,
             operate: '>',
         });
-        this.sqlBuilder.orderBy = [['order', 'ASC']];
+        this.sqlBuilder.orderBy = [[this.setting.order, 'ASC']];
 
         const [sql, sqlParam] = this.sqlBuilder.build(this.tableName);
         const data = await this.db.query(sql, sqlParam);
@@ -217,19 +220,19 @@ class TreeService extends Service {
     }
 
     /**
-     * 根据full_path获取数据 full_path Like ‘1.2.3%’(传参full_path = '1.2.3%')
+     * 根据fullPath获取数据 fullPath Like ‘1.2.3%’(传参fullPath = '1.2.3%')
      * @param {Number} tenderId - 标段id
-     * @param {String} full_path - 路径
+     * @param {String} fullPath - 路径
      * @return {Promise<void>}
      */
-    async getDataByFullPath(mid, full_path) {
+    async getDataByFullPath(mid, fullPath) {
         this.initSqlBuilder();
         this.sqlBuilder.setAndWhere(this.setting.mid, {
             value: mid,
             operate: '=',
         });
         this.sqlBuilder.setAndWhere(this.setting.fullPath, {
-            value: this.db.escape(full_path),
+            value: this.db.escape(fullPath),
             operate: 'Like',
         });
         const [sql, sqlParam] = this.sqlBuilder.build(this.tableName);
@@ -237,7 +240,7 @@ class TreeService extends Service {
         return resultData;
     }
     /**
-     * 根据full_path检索自己及所有父项
+     * 根据fullPath检索自己及所有父项
      * @param {Number} tenderId - 标段id
      * @param {Array|String} fullPath - 节点完整路径
      * @return {Promise<*>}
@@ -281,7 +284,7 @@ class TreeService extends Service {
             value: pids,
             operate: 'in',
         });
-        this.sqlBuilder.orderBy = [['order', 'ASC']];
+        this.sqlBuilder.orderBy = [[this.setting.order, 'ASC']];
 
         const [sql, sqlParam] = this.sqlBuilder.build(this.tableName);
         const data = await this.db.query(sql, sqlParam);
@@ -412,7 +415,7 @@ class TreeService extends Service {
 
         if (this.setting.uuid) data.id = this.uuid.v4();
         data[this.setting.kid] = maxId + 1;
-        data[this.setting.pid] = select ? select[this.setting.pid] : rootId;
+        data[this.setting.pid] = select ? select[this.setting.pid] : this.rootId;
         data[this.setting.mid] = mid;
         data[this.setting.level] = select ? select[this.setting.level] : 1;
         data[this.setting.order] = select ? select[this.setting.order] + 1 : 1;
@@ -473,7 +476,7 @@ class TreeService extends Service {
                 const newData = Object.assign({}, data);
                 if (this.setting.uuid) newData.id = this.uuid.v4();
                 newData[this.setting.kid] = maxId + i;
-                newData[this.setting.pid] = select ? select[this.setting.pid] : rootId;
+                newData[this.setting.pid] = select ? select[this.setting.pid] : this.rootId;
                 newData[this.setting.mid] = mid;
                 newData[this.setting.level] = select ? select[this.setting.level] : 1;
                 newData[this.setting.order] = select ? select[this.setting.order] + i : i;
@@ -559,7 +562,7 @@ class TreeService extends Service {
         try {
             // 删除
             const operate = await this._deleteNodeData(mid, select);
-            // 选中节点--父节点 只有一个子节点时,应升级is_leaf
+            // 选中节点--父节点 只有一个子节点时,应升级isLeaf
             if (parent) {
                 const count = await this.db.count(this.tableName, this.getCondition({mid: mid, pid: select[this.setting.pid]}));
                 if (count === 1) {
@@ -607,7 +610,7 @@ class TreeService extends Service {
             for (const s of selects) {
                 const operate = await this._deleteNodeData(mid, s);
             }
-            // 选中节点--父节点 只有一个子节点时,应升级is_leaf
+            // 选中节点--父节点 只有一个子节点时,应升级isLeaf
             if (parent && childCount === count) {
                 const updateParent = {id: parent.id };
                 updateParent[this.setting.isLeaf] = true;
@@ -661,10 +664,14 @@ class TreeService extends Service {
         this.transaction = await this.db.beginTransaction();
         try {
             for (const s of selects) {
-                const sData = await this.transaction.update(this.tableName, { id: s.id, order: s[this.setting.order] - 1 });
+                const sData = { id: s.id };
+                sData[this.setting.order] = s[this.setting.order] - 1;
+                await this.transaction.update(this.tableName, sData);
                 order.push(s[this.setting.order] - 1);
             }
-            const pData = await this.transaction.update(this.tableName, { id: pre.id, order: pre[this.setting.order] + count });
+            const pData = { id: pre.id };
+            pData[this.setting.order] = pre[this.setting.order] + count;
+            await this.transaction.update(this.tableName, pData);
             order.push(pre[this.setting.order] + count);
             await this.transaction.commit();
             this.transaction = null;
@@ -702,10 +709,14 @@ class TreeService extends Service {
         this.transaction = await this.db.beginTransaction();
         try {
             for (const s of selects) {
-                const sData = await this.transaction.update(this.tableName, { id: s.id, order: s[this.setting.order] + 1 });
+                const sData = { id: s.id };
+                sData[this.setting.order] = s[this.setting.order] + 1;
+                await this.transaction.update(this.tableName, sData);
                 order.push(s[this.setting.order] + 1);
             }
-            const nData = await this.transaction.update(this.tableName, { id: next.id, order: next[this.setting.order] - count });
+            const nData = { id: next.id };
+            nData[this.setting.order] = next[this.setting.order] - count;
+            await this.transaction.update(this.tableName, nData);
             order.push(next[this.setting.order] - count);
             await this.transaction.commit();
             this.transaction = null;
@@ -792,17 +803,18 @@ class TreeService extends Service {
             const [sql1, sqlParam1] = this.sqlBuilder.build(this.tableName, 'update');
             await this.transaction.query(sql1, sqlParam1);
 
-            // 选中节点 is_leaf应为false
-            if (select.is_leaf) {
-                const updateData = { id: select.id, is_leaf: false };
+            // 选中节点 isLeaf应为false
+            if (select[this.setting.isLeaf]) {
+                const updateData = { id: select.id };
+                updateData[this.setting.isLeaf] = false;
                 await this.transaction.update(this.tableName, updateData);
             }
 
-            // 修改nextsData及其子节点的full_path
+            // 修改nextsData及其子节点的fullPath
             const oldSubStr = this.db.escape(select[this.setting.pid] + '-');
             const newSubStr = this.db.escape(select[this.setting.kid] + '-');
             const sqlArr = [];
-            sqlArr.push('Update ?? SET `full_path` = Replace(`full_path`,' + oldSubStr + ',' + newSubStr + ') Where');
+            sqlArr.push('Update ?? SET `' + this.setting.fullPath + '` = Replace(`' + this.setting.fullPath + '`,' + oldSubStr + ',' + newSubStr + ') Where');
             sqlArr.push('(`' + this.setting.mid + '` = ' + select[this.setting.mid] + ')');
             sqlArr.push(' And (');
             for (const data of nexts) {
@@ -837,35 +849,34 @@ class TreeService extends Service {
         const newPath = [];
         this.transaction = await this.db.beginTransaction();
         try {
-            // 选中节点--父节点 选中节点为firstChild时,修改is_leaf
+            // 选中节点--父节点 选中节点为firstChild时,修改isLeaf
             if (first[this.setting.order] === 1) {
-                await this.transaction.update(this.tableName, {
-                    id: parent.id,
-                    is_leaf: true,
-                });
+                const updateParentData = { id: parent.id };
+                updateParentData[this.setting.isLeaf] = true;
+                await this.transaction.update(this.tableName, updateParentData);
             }
             // 选中节点--父节点--全部后兄弟节点 order+1
             await this._updateChildrenOrder(mid, parent[this.setting.pid], parent[this.setting.order] + 1, count);
             for (const [i, s] of selects.entries()) {
-                // 选中节点 修改pid, order, full_path, level, is_leaf, 清空计算项
+                // 选中节点 修改pid, order, fullPath, level, isLeaf, 清空计算项
                 const updateData = { id: s.id };
                 updateData[this.setting.pid] = parent[this.setting.pid];
                 updateData[this.setting.order] = parent[this.setting.order] + i + 1;
                 updateData[this.setting.level] = s[this.setting.level] - 1;
                 updateData[this.setting.fullPath] = s[this.setting.fullPath].replace(`-${s[this.setting.pid]}-`, '-');
                 newPath.push(updateData[this.setting.fullPath]);
-                if (s.is_leaf && s.id === last.id) {
+                if (s[this.setting.isLeaf] && s.id === last.id) {
                     const nexts = await this.getNextsData(mid, parent[this.setting.kid], last[this.setting.order]);
                     if (nexts.length > 0) {
-                        updateData.is_leaf = false;
+                        updateData[this.setting.isLeaf] = false;
                         this.clearParentingData(updateData);
                     }
                 }
                 await this.transaction.update(this.tableName, updateData);
-                // 选中节点--全部子节点(含孙) level-1, full_path变更
+                // 选中节点--全部子节点(含孙) level-1, fullPath变更
                 await this._syncUplevelChildren(s);
             }
-            // 选中节点--全部后兄弟节点 收编为子节点 修改pid, order, full_path
+            // 选中节点--全部后兄弟节点 收编为子节点 修改pid, order, fullPath
             await this._syncUpLevelNexts(last);
             await this.transaction.commit();
             this.transaction = null;
@@ -938,7 +949,7 @@ class TreeService extends Service {
             await this._updateChildrenOrder(mid, first[this.setting.pid], last[this.setting.order] + 1, -count);
 
             for (const [i, s] of selects.entries()) {
-                // 选中节点 修改pid, level, order, full_path
+                // 选中节点 修改pid, level, order, fullPath
                 const updateData = { id: s.id };
                 updateData[this.setting.pid] = pre[this.setting.kid];
                 updateData[this.setting.order] = preLastChild ? preLastChild[this.setting.order] + i + 1 : i + 1;
@@ -951,10 +962,10 @@ class TreeService extends Service {
                 }
                 newPath.push(updateData[this.setting.fullPath]);
                 await this.transaction.update(this.tableName, updateData);
-                // 选中节点--全部子节点(含孙) level++, full_path
+                // 选中节点--全部子节点(含孙) level++, fullPath
                 await this._syncDownlevelChildren(s, updateData[this.setting.fullPath]);
             }
-            // 选中节点--前兄弟节点 is_leaf应为false, 清空计算相关字段
+            // 选中节点--前兄弟节点 isLeaf应为false, 清空计算相关字段
             const updateData2 = { id: pre.id };
             updateData2[this.setting.isLeaf] = false;
             this.clearParentingData(updateData2);

+ 6 - 1
app/const/payment.js

@@ -50,7 +50,7 @@ const modes_value = ['form', 'safe'];
 const modes_value_object = {
     form: 0,
     safe: 1,
-}
+};
 
 const rpt_dataType = {
     intact_type_text: 'text',
@@ -76,6 +76,10 @@ const signature_msg = {
     content: null,
 };
 
+const defaultSafeBills = [
+    { b_code: '', name: '安全生产费用清单', tree_id: 1, tree_pid: -1, tree_level: 1, tree_order: 1, tree_full_path: '1', tree_is_leaf: 1 }
+];
+
 module.exports = {
     audit_admin_permission,
     audit_permission,
@@ -89,4 +93,5 @@ module.exports = {
     setting_modes,
     modes_value,
     modes_value_object,
+    defaultSafeBills,
 };

+ 1 - 1
app/controller/ledger_controller.js

@@ -152,7 +152,7 @@ module.exports = app => {
                     dealBillsPermission: this._canUpdateDealBills(tender.data, auditors.filter(x => {return x.audit_order > 0})),
                     shenpiConst,
                     categoryData,
-                    syncLedgerUrl: syncLedger ? `${ctx.app.config.url3f}/${syncLedger.pull_class}/sync-tz/${tender.id}` : '',
+                    syncLedgerUrl: syncLedger ? `${ctx.app.config.url3f}/${syncLedger.pull_class}/sync-tz?pCode=${tender.id}` : '',
                     nodeType: stdConst.nodeType,
                 };
                 if ((tender.data.ledger_status === auditConst.status.uncheck || tender.data.ledger_status === auditConst.status.checkNo) && tender.data.user_id === ctx.session.sessionUser.accountId) {

+ 147 - 0
app/controller/payment_controller.js

@@ -1107,6 +1107,153 @@ module.exports = app => {
 
             ctx.body = responseData;
         }
+
+        async safeBills(ctx) {
+            try {
+                await this._getDetailAuditViewData(ctx);
+                const auditIdList = ctx.helper._.map(ctx.detail.auditors, 'aid');
+                const rptAuditIdList = ctx.helper._.map(ctx.detail.rptAudits, 'uid');
+                const uidList = ctx.helper._.uniq([...auditIdList, ...rptAuditIdList]);
+
+                // 获取附件列表
+                const attList = await ctx.service.paymentDetailAtt.getPaymentDetailAttachment(ctx.detail.id, 'desc');
+                const stdBills = await ctx.service.stdGclList.getSafeGcl();
+                const renderData = {
+                    trInfo: ctx.trInfo,
+                    paymentConst,
+                    jsFiles: this.app.jsFiles.common.concat(this.app.jsFiles.payment.safe),
+                    auditConst,
+                    shenpiConst,
+                    attList,
+                    moment,
+                    whiteList: ctx.app.config.multipart.whitelist,
+                    uidList,
+                    preUrl: '/payment/' + ctx.paymentTender.id + '/detail/' + ctx.detail.id,
+                    OSS_PATH: ctx.app.config.fujianOssPath,
+                    stdBills,
+                };
+                renderData.nextDetail = await ctx.service.paymentDetail.getDataByCondition({ tr_id: ctx.trInfo.id, order: ctx.detail.order + 1 });
+                const content = [];
+                renderData.content = content;
+                if ((ctx.detail.status === auditConst.status.uncheck || ctx.detail.status === auditConst.status.checkNo) && ctx.session.sessionUser.accountId === ctx.detail.uid) {
+                    // 获取所有项目参与者
+                    const accountList = await ctx.service.projectAccount.getAllDataByCondition({
+                        where: { project_id: ctx.session.sessionProject.id, enable: 1 },
+                        columns: ['id', 'name', 'company', 'role', 'enable', 'is_admin', 'account_group', 'mobile'],
+                    });
+                    renderData.accountList = accountList;
+                    const unitList = await ctx.service.constructionUnit.getAllDataByCondition({ where: { pid: ctx.session.sessionProject.id } });
+                    const accountGroupList = unitList.map(item => {
+                        const groupList = accountList.filter(item1 => item1.company === item.name);
+                        return { groupName: item.name, groupList };
+                    });
+                    renderData.accountGroup = accountGroupList;
+                }
+                await this.layout('payment_safe/index.ejs', renderData, 'payment_safe/modal.ejs');
+            } catch (err) {
+                this.log(err);
+                this.ctx.postError(err, '读取安全生产费错误');
+                if (ctx.detail.tender_id && ctx.detail.tr_id) {
+                    ctx.redirect('/payment' + ctx.detail.tender_id + '/list');
+                } else {
+                    ctx.redirect(this.menu.menu.dashboard.url);
+                }
+            }
+        }
+
+        async safeCompare(ctx) {
+            try {
+                const renderData = {
+                    trInfo: ctx.trInfo,
+                    paymentConst,
+                    jsFiles: this.app.jsFiles.common.concat(this.app.jsFiles.payment.compare),
+                    auditConst,
+                };
+                await this.layout('payment_safe/compare.ejs', renderData);
+            } catch (err) {
+                this.log(err);
+                this.ctx.postError(err, '读取安全生产费错误');
+                if (ctx.detail.tender_id && ctx.detail.tr_id) {
+                    ctx.redirect('/payment' + ctx.detail.tender_id + '/list');
+                } else {
+                    ctx.redirect(this.menu.menu.dashboard.url);
+                }
+            }
+        }
+
+        async safeLoad(ctx) {
+            try {
+                const data = JSON.parse(ctx.request.body.data);
+                const filter = data.filter.split(';');
+                const responseData = { err: 0, msg: '', data: {}, hpack: [] };
+                for (const f of filter) {
+                    switch (f) {
+                        case 'bills':
+                            responseData.data.bills = await ctx.service.paymentSafeBills.getAllDataByCondition({ where: {detail_id: ctx.params.did }});
+                            break;
+                        default:
+                            responseData.data[f] = [];
+                            break;
+                    }
+                }
+
+                ctx.body = responseData;
+            } catch (err) {
+                this.log(err);
+                ctx.body = { err: 1, msg: err.toString(), data: null };
+            }
+        }
+
+        async _billsBase(detail, 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.paymentSafeBills.addSafeBillsNode(detail, data.id, data.count);
+                case 'delete':
+                    return await this.ctx.service.paymentSafeBills.delete(detail.id, data.id, data.count);
+                case 'up-move':
+                    return await this.ctx.service.paymentSafeBills.upMoveNode(detail.id, data.id, data.count);
+                case 'down-move':
+                    return await this.ctx.service.paymentSafeBills.downMoveNode(detail.id, data.id, data.count);
+                case 'up-level':
+                    return await this.ctx.service.paymentSafeBills.upLevelNode(detail.id, data.id, data.count);
+                case 'down-level':
+                    return await this.ctx.service.paymentSafeBills.downLevelNode(detail.id, data.id, data.count);
+            }
+        }
+        async safeUpdate(ctx) {
+            try {
+                const data = JSON.parse(ctx.request.body.data);
+                if (!data.postType || !data.postData) throw '数据错误';
+                const responseData = { err: 0, msg: '', data: {} };
+
+                switch (data.postType) {
+                    case 'add':
+                    case 'delete':
+                    case 'up-move':
+                    case 'down-move':
+                    case 'up-level':
+                    case 'down-level':
+                        responseData.data = await this._billsBase(ctx.detail, data.postType, data.postData);
+                        break;
+                    case 'update':
+                        responseData.data = await this.ctx.service.paymentSafeBills.updateCalc(ctx.detail, data.postData);
+                        break;
+                    case 'add-std':
+                        responseData.data = await this.ctx.service.paymentSafeBills.addStdNodeWithParent(ctx.detail, data.postData.id, data.postData.stdData);
+                        break;
+                    default:
+                        throw '未知操作';
+                }
+                ctx.body = responseData;
+            } catch (err) {
+                this.log(err);
+                ctx.body = this.ajaxErrorBody(err, '数据错误');
+            }
+        }
     }
 
     return PaymentController;

+ 32 - 2
app/lib/rptCustomData.js

@@ -627,10 +627,12 @@ class fjHelper {
             if (!b) continue;
 
             const field = (d.sid < sid ? 'pre_' : '') + (d.rela_qty > 0 ? 'positive_' : 'negative_') + 'qc_qty';
+            const minusField = (d.sid < sid ? 'pre_c_' : 'c_') + (d.rela_minus > 0 ? 'minus_' : '') + 'qc_qty';
             b[field] = this.ctx.helper.add(b[field], d.rela_qty);
+            b[minusField] = this.ctx.helper.add(b[minusField], d.rela_qty);
         }
 
-        const result = [];
+        const result = [], changeSum = {};
         for (const b of bills) {
             b.quantity = helper.sum(b.ledgerSource.map(x => { return x.quantity; }));
             b.total_price = helper.sum(b.ledgerSource.map(x => { return x.total_price; }));
@@ -653,6 +655,34 @@ class fjHelper {
             b.end_positive_qc_tp = helper.add(helper.add(b.pre_positive_qc_tp, b.positive_qc_tp), b.positive_qc_pc_tp);
             b.end_negative_qc_qty = helper.add(b.pre_negative_qc_qty, b.negative_qc_qty);
             b.end_negative_qc_tp = helper.add(helper.add(b.pre_negative_qc_tp, b.negative_qc_tp), b.negative_qc_pc_tp);
+
+            if (b.org_price) {
+                b.pre_c_qc_tp = helper.mul(b.org_price, b.pre_c_qc_qty, tpDecimal);
+                b.pre_c_minus_qc_tp = helper.mul(b.org_price, b.pre_c_minus_qc_qty, tpDecimal);
+                b.c_qc_pc_tp = helper.sub(helper.mul(b.unit_price, b.pre_c_qc_qty, tpDecimal), b.pre_c_qc_tp);
+                b.c_minus_qc_pc_tp = helper.sub(helper.mul(b.unit_price, b.pre_c_minus_qc_qty, tpDecimal), b.pre_c_minus_qc_tp);
+            } else {
+                b.pre_c_qc_tp = helper.mul(b.unit_price, b.pre_c_qc_qty, tpDecimal);
+                b.pre_c_minus_qc_tp = helper.mul(b.unit_price, b.pre_c_minus_qc_qty, tpDecimal);
+                b.c_positive_qc_pc_tp = 0;
+                b.c_negative_qc_pc_tp = 0;
+            }
+            b.c_qc_tp = helper.mul(b.unit_price, b.c_qc_qty, tpDecimal);
+            b.c_minus_qc_tp = helper.mul(b.unit_price, b.c_minus_qc_qty, tpDecimal);
+            b.end_c_qc_qty = helper.add(b.pre_c_qc_qty, b.c_qc_qty);
+            b.end_c_qc_tp = helper.add(helper.add(b.pre_c_qc_tp, b.c_qc_tp), b.c_qc_pc_tp);
+            b.end_c_minus_qc_qty = helper.add(b.pre_c_minus_qc_qty, b.c_minus_qc_qty);
+            b.end_c_minus_qc_tp = helper.add(helper.add(b.pre_c_minus_qc_tp, b.c_minus_qc_tp), b.c_minus_qc_pc_tp);
+
+            // 统计合计
+            changeSum.pre_c_tp = helper.add(changeSum.pre_c_tp, b.pre_c_qc_tp);
+            changeSum.c_tp = helper.add(changeSum.c_tp, b.c_qc_tp);
+            changeSum.c_pc_tp = helper.add(changeSum.c_pc_tp, b.c_qc_pc_tp);
+            changeSum.end_c_tp = helper.add(changeSum.end_c_tp, b.end_c_qc_tp);
+            changeSum.pre_c_minus_tp = helper.add(changeSum.pre_c_minus_tp, b.pre_c_minus_qc_tp);
+            changeSum.c_minus_tp = helper.add(changeSum.c_minus_tp, b.c_minus_qc_tp);
+            changeSum.c_minus_pc_tp = helper.add(changeSum.c_minus_pc_tp, b.c_minus_qc_pc_tp);
+            changeSum.end_c_minus_tp = helper.add(changeSum.end_c_minus_tp, b.end_c_minus_qc_tp);
             result.push({
                 b_code: b.b_code, name: b.name, unit: b.unit, unit_price: b.unit_price,
                 quantity: b.quantity, total_price: b.total_price,
@@ -672,7 +702,7 @@ class fjHelper {
                 minus: 1,
             });
         }
-        return result;
+        return { mem_fj_change_progress: result, mem_fj_change_sum: [changeSum] };
     }
     async getChangeProgressData(tid, sid) {
         await this.ctx.service.stage.checkStage(sid);

+ 1 - 1
app/public/js/path_tree.bak.js

@@ -229,7 +229,7 @@ const createNewPathTree = function (setting) {
      */
     proto.isLastSibling = function (node) {
         const siblings = this.getChildren(this.getParent(node));
-        return node.order === siblings[siblings.length - 1].order;
+        return node[this.setting.order] === siblings[siblings.length - 1][this.setting.order];
     };
     /**
      * 刷新子节点是否可见

+ 47 - 38
app/public/js/path_tree.js

@@ -285,6 +285,8 @@ const createNewPathTree = function (type, setting) {
             this.children = [];
             // 树设置
             this.setting = setting;
+            if (!this.setting.isLeaf) this.setting.isLeaf = 'is_leaf';
+            if (!this.setting.fullPath) this.setting.fullPath = 'full_path';
 
             this.hasMark = false;
             if (this.setting.markExpandKey) {
@@ -318,7 +320,7 @@ const createNewPathTree = function (type, setting) {
                     if (!isResort) {
                         nodes[i].children = self.getChildren(nodes[i]);
                     } else {
-                        nodes[i].children.sort((a, b) => { return a[self.setting.order] - b[self.setting.order]; })
+                        self.sortByOrder(nodes[i].children);
                     }
                     addSortNodes(nodes[i].children);
                 }
@@ -327,10 +329,22 @@ const createNewPathTree = function (type, setting) {
             if (!isResort) {
                 this.children = this.getChildren();
             } else {
-                this.children.sort((a, b) => { return a[self.setting.order] - b[self.setting.order]; });
+                this.sortByOrder(this.children);
             }
             addSortNodes(this.children);
         }
+        sortByOrder(datas) {
+            const setting = this.setting;
+            datas.sort((a, b) => { return a[setting.order] - b[setting.order]; });
+        }
+        sortByLevel(datas) {
+            const setting = this.setting;
+            datas.sort((a, b) => { return a[setting.level] - b[setting.level]; });
+        }
+        sortByLevelConverse(datas) {
+            const setting = this.setting;
+            datas.sort((a, b) => { return b[setting.level] - a[setting.level]; });
+        }
         /**
          * 加载数据(初始化), 并给数据添加部分树结构必须数据
          * @param datas
@@ -343,9 +357,8 @@ const createNewPathTree = function (type, setting) {
             this.datas = [];
             this.children = [];
             // 加载全部数据
-            datas.sort(function (a, b) {
-                return a.level - b.level;
-            });
+            this.sortByLevel(datas);
+            const setting = this.setting;
             for (const data of datas) {
                 const keyName = itemsPre + data[this.setting.id];
                 if (this.items[keyName]) continue;
@@ -364,7 +377,7 @@ const createNewPathTree = function (type, setting) {
                 this.items[keyName] = item;
                 this.datas.push(item);
             }
-            this.children.sort((a, b) => { return a[self.setting.order] - b[self.setting.order]; });
+            this.sortByOrder(this.children);
             this.sortTreeNode(true);
             if (this.hasMark) {
                 if (this.setting.markExpandKey) {
@@ -404,7 +417,7 @@ const createNewPathTree = function (type, setting) {
                 }
                 parents.push(...this.getAllParents(node));
             }
-            parents.sort((x, y) => { return y.level - x.level});
+            this.sortByLevel(parents)
             for (const parent of parents) {
                 if (!parent.children || parent.children.length === 0) continue;
                 parent.filter = !parent.children.find(x => { return !x.filter });
@@ -440,7 +453,7 @@ const createNewPathTree = function (type, setting) {
         };
         getTopParent(node) {
             const parents = this.getAllParents(node);
-            parents.sort((a, b) => { return a.level - b.level; });
+            this.sortByLevel(parents);
             return parents[0];
         };
         getAllParents(node) {
@@ -518,7 +531,7 @@ const createNewPathTree = function (type, setting) {
             const children = this.datas.filter(function (x) {
                 return x[setting.pid] === pid;
             });
-            children.sort((a, b) => { return a[setting.order] - b[setting.order]; });
+            this.sortByOrder(children);
             return children;
         };
 
@@ -573,7 +586,7 @@ const createNewPathTree = function (type, setting) {
          */
         isLastViewSibling(node) {
             const siblings = (this.getChildren(this.getParent(node))).filter(x => { return !x.filter });
-            return (siblings && siblings.length > 0) ? node.order === siblings[siblings.length - 1].order : false;
+            return (siblings && siblings.length > 0) ? node[this.setting.order] === siblings[siblings.length - 1][this.setting.order] : false;
         };
 
         /**
@@ -687,8 +700,9 @@ const createNewPathTree = function (type, setting) {
          * @param {Number} level - 展开层数
          */
         expandByLevel(level) {
+            const field = this.setting.level;
             this.expandByCustom(function (n) {
-                return n.level < level;
+                return n[field] < level;
             });
         }
 
@@ -999,9 +1013,7 @@ const createNewPathTree = function (type, setting) {
             return loadedData;
         }
         removeData(datas) {
-            datas.sort(function (a, b) {
-                return b.level - a.level;
-            });
+            this.sortByLevel(datas);
             const removeArrayData = function (array, data) {
                 const index = array.indexOf(data);
                 array.splice(index, 1);
@@ -1023,7 +1035,7 @@ const createNewPathTree = function (type, setting) {
             const datas = data instanceof Array ? data : [data];
             for (const d of datas) {
                 let node = this.getItems(d[this.setting.id]);
-                if (node && node.is_leaf) {
+                if (node && node[this.setting.isLeaf]) {
                     for (const prop in d) {
                         if (data[prop] !== undefined) {
                             node[prop] = d[prop];
@@ -1129,7 +1141,7 @@ const createNewPathTree = function (type, setting) {
             for (const node of nodes) {
                 const parent = this.getParent(node);
                 if (parent) {
-                    const paths = this.getFullPathNodes(parent.full_path);
+                    const paths = this.getFullPathNodes(parent[this.setting.fullPath]);
                     for (const p of paths) {
                         if (parents.indexOf(p) === -1) {
                             parents.push(p);
@@ -1146,7 +1158,7 @@ const createNewPathTree = function (type, setting) {
             for (const node of nodes) {
                 const parent = this.getParent(node);
                 if (parent) {
-                    const paths = this.getFullPathNodes(parent.full_path);
+                    const paths = this.getFullPathNodes(parent[this.setting.fullPath]);
                     for (const p of paths) {
                         if (reCalcNodes.indexOf(p) === -1) {
                             reCalcNodes.push(p);
@@ -1181,9 +1193,7 @@ const createNewPathTree = function (type, setting) {
                 result.update = this._updateData(data.update);
                 this._getReCalcNodes(reCalcNodes, result.update);
             }
-            reCalcNodes.sort((a, b) => {
-                return b.level - a.level;
-            });
+            this.sortByLevelConverse(reCalcNodes);
             for (const node of reCalcNodes) {
                 treeCalc.calculateNode(this, node, this.setting.calcFields, this.setting.calcFun);
             }
@@ -1282,6 +1292,7 @@ const createNewPathTree = function (type, setting) {
             return result;
         }
         loadPostData(data) {
+            const setting = this.setting;
             const result = {}, reCalcNodes = [];
             if (!data) return result;
             if (data.delete) {
@@ -1298,9 +1309,7 @@ const createNewPathTree = function (type, setting) {
                 this.loadPostReivsePrice(result.update);
                 this._getReCalcNodes(reCalcNodes, result.update);
             }
-            reCalcNodes.sort((a, b) => {
-                return b.level - a.level;
-            });
+            this.sortByLevelConverse(reCalcNodes);
             for (const node of reCalcNodes) {
                 treeCalc.calculateNode(this, node, this.setting.calcFields, this.setting.calcFun);
             }
@@ -1447,7 +1456,7 @@ const createNewPathTree = function (type, setting) {
             for (const node of nodes) {
                 const parent = this.getParent(node);
                 if (parent) {
-                    const paths = this.getFullPathNodes(parent.full_path);
+                    const paths = this.getFullPathNodes(parent[this.setting.fullPath]);
                     for (const p of paths) {
                         if (parents.indexOf(p) === -1) {
                             parents.push(p);
@@ -1499,9 +1508,7 @@ const createNewPathTree = function (type, setting) {
                 result = result ? result.concat(dgnResult) : dgnResult;
             }
             result = result ? result.concat(parents) : parents;
-            result.sort((a, b) => {
-                return b.level - a.level;
-            });
+            this.sortByLevelConverse(result);
             for (const node of result) {
                 treeCalc.calculateNode(this, node);
             }
@@ -1734,7 +1741,7 @@ const createNewPathTree = function (type, setting) {
                 };
                 cur[this.setting.id] = id;
                 cur[this.setting.pid] = parent ? parent[this.setting.id] : this.setting.rootId;
-                cur[this.setting.full_path] = parent ? parent[this.setting.full_path] + '-' + id : '' + id;
+                cur[this.setting.fullPath] = parent ? parent[this.setting.fullPath] + '-' + id : '' + id;
                 cur[this.setting.level] = parent ? parent[this.setting.level] + 1 : 1;
                 cur[this.setting.order] = siblings.length + 1;
                 siblings.push(cur);
@@ -2000,22 +2007,24 @@ const createNewPathTree = function (type, setting) {
 
 const treeCalc = {
     mapTreeNode: function (tree) {
+        const setting = tree.setting;
         let map = {}, maxLevel = 0;
         for (const node of tree.nodes) {
-            let levelArr = map[node.level];
+            let levelArr = map[node[setting.level]];
             if (!levelArr) {
                 levelArr = [];
-                map[node.level] = levelArr;
+                map[node[setting.level]] = levelArr;
             }
-            if (node.level > maxLevel) {
-                maxLevel = node.level;
+            if (node[setting.level] > maxLevel) {
+                maxLevel = node[setting.level];
             }
             levelArr.push(node);
         }
         return [maxLevel, map];
     },
     getMaxLevel: function (tree) {
-        return Math.max.apply(Math, tree.datas.map(function (o) { return o.level }));
+        const setting = tree.setting;
+        return Math.max.apply(Math, tree.datas.map(function (o) { return o[setting.level] }));
     },
     calculateNode: function (tree, node) {
         if (node.children && node.children.length > 0) {
@@ -2041,7 +2050,8 @@ const treeCalc = {
         }
     },
     calculateLevelNode: function (tree, level) {
-        const nodes = tree.datas.filter((n) => { return n.level === level });
+        const setting = tree.setting;
+        const nodes = tree.datas.filter((n) => { return n[setting.level] === level });
         for (const node of nodes) {
             this.calculateNode(tree, node);
         }
@@ -2058,10 +2068,9 @@ const treeCalc = {
         }
     },
     calculateParent: function (tree, node) {
-        const nodes = tree.getFullPathNodes(node.full_path);
-        nodes.sort((a, b) => {
-            return b.level - a.level;
-        });
+        const setting = tree.setting;
+        const nodes = tree.getFullPathNodes(node[setting.fullPath]);
+        tree.sortByLevel(nodes);
         for (const n of nodes) {
             this.calculateNode(tree, n);
         }

+ 112 - 0
app/public/js/payment_compare.js

@@ -0,0 +1,112 @@
+$(document).ready(function() {
+    autoFlashHeight();
+    class BillsObj {
+        constructor() {
+            this.spread = SpreadJsObj.createNewSpread($('#bills-spread')[0]);
+            this.sheet = this.spread.getActiveSheet();
+            this.treeSetting = {
+                id: 'tree_id',
+                pid: 'tree_pid',
+                order: 'tree_order',
+                level: 'tree_level',
+                rootId: -1,
+                autoExpand: 3,
+                markExpandKey: 'bills-expand',
+                markExpandSubKey: window.location.pathname.split('/')[2],
+                calcFields: ['cur_tp', 'pre_tp', 'end_tp'],
+            };
+            this.spreadSetting = {
+                baseCols: [
+                    {title: '编号', colSpan: '1', rowSpan: '2', field: 'b_code', hAlign: 0, width: 70, formatter: '@'},
+                    {title: '名称', colSpan: '1', rowSpan: '2', field: 'name', hAlign: 0, width: 185, formatter: '@'},
+                    {title: '规格', colSpan: '1', rowSpan: '2', field: 'spec', hAlign: 0, width: 150, formatter: '@'},
+                    {title: '单位', colSpan: '1', rowSpan: '2', field: 'unit', hAlign: 1, width: 50, formatter: '@', cellType: 'unit'},
+                    {title: '单价', colSpan: '1', rowSpan: '2', field: 'unit_price', hAlign: 2, width: 60, type: 'Number'},
+                ],
+                extraCols: [
+                    {title: '%s|数量', colSpan: '2|1', rowSpan: '1|1', field: '{%s}_qty{%d}', hAlign: 2, width: 60, type: 'Number', },
+                    {title: '|金额', colSpan: '|1', rowSpan: '|1', field: '{%s}_tp{%d}', hAlign: 2, width: 60, type: 'Number', },
+                ],
+                emptyRows: 3,
+                headRows: 2,
+                headRowHeight: [25, 25],
+                defaultRowHeight: 21,
+                headerFont: '12px 微软雅黑',
+                font: '12px 微软雅黑',
+            };
+            this.tree = createNewPathTree('ledger', this.treeSetting);
+            this.ckBillsSpread = window.location.pathname + '-billsSelect';
+
+            this.initSpread();
+        }
+        refreshSpreadSetting(roles) {
+            this.spreadSetting.cols = [];
+            for (const col of this.spreadSetting.baseCols) {
+                this.spreadSetting.cols.push(col);
+            }
+            // todo 根据参与审批的人加载数据
+        }
+        initSpread(roles = []) {
+            this.refreshSpreadSetting(roles);
+            SpreadJsObj.initSheet(this.sheet, this.spreadSetting);
+        }
+        loadData(datas, roles, history) {
+            // todo 整理数据
+            this.initSpread(roles);
+            this.tree.loadDatas(datas);
+            treeCalc.calculateAll(this.tree);
+            SpreadJsObj.loadSheetData(this.sheet, SpreadJsObj.DataType.Tree, this.tree);
+            SpreadJsObj.loadTopAndSelect(this.sheet, this.ckBillsSpread);
+        }
+    }
+    const billsObj = new BillsObj();
+
+    // 加载安全生产费数据
+    postData('load', { filter: 'bills;audit;history' }, function(result) {
+        billsObj.loadData(result.bills);
+    });
+
+    // 导航Menu
+    $.subMenu({
+        menu: '#sub-menu', miniMenu: '#sub-mini-menu', miniMenuList: '#mini-menu-list',
+        toMenu: '#to-menu', toMiniMenu: '#to-mini-menu',
+        key: 'menu.1.0.0',
+        miniHint: '#sub-mini-hint', hintKey: 'menu.hint.1.0.1',
+        callback: function (info) {
+            if (info.mini) {
+                $('.panel-title').addClass('fluid');
+                $('#sub-menu').removeClass('panel-sidebar');
+            } else {
+                $('.panel-title').removeClass('fluid');
+                $('#sub-menu').addClass('panel-sidebar');
+            }
+            autoFlashHeight();
+            billsObj.spread.refresh();
+        }
+    });
+    // 显示层次
+    (function (select, sheet) {
+        $(select).click(function () {
+            if (!sheet.zh_tree) return;
+            const tag = $(this).attr('tag');
+            const tree = sheet.zh_tree;
+            setTimeout(() => {
+                showWaitingView();
+                switch (tag) {
+                    case "1":
+                    case "2":
+                    case "3":
+                    case "4":
+                        tree.expandByLevel(parseInt(tag));
+                        SpreadJsObj.refreshTreeRowVisible(sheet);
+                        break;
+                    case "last":
+                        tree.expandByCustom(() => { return true; });
+                        SpreadJsObj.refreshTreeRowVisible(sheet);
+                        break;
+                }
+                closeWaitingView();
+            }, 100);
+        });
+    })('a[name=showLevel]', billsObj.sheet);
+});

+ 734 - 0
app/public/js/payment_safe.js

@@ -0,0 +1,734 @@
+function getTenderId() {
+    return window.location.pathname.split('/')[2];
+}
+const invalidFields = {
+    parent: ['cur_qty', 'cur_tp', 'unit_price'],
+};
+
+$(document).ready(function() {
+    let stdGcl;
+    autoFlashHeight();
+    class BillsObj {
+        constructor() {
+            this.spread = SpreadJsObj.createNewSpread($('#bills-spread')[0]);
+            this.sheet = this.spread.getActiveSheet();
+            this.treeSetting = {
+                id: 'tree_id',
+                pid: 'tree_pid',
+                order: 'tree_order',
+                level: 'tree_level',
+                isLeaf: 'tree_is_leaf',
+                fullPath: 'tree_full_path',
+                rootId: -1,
+                calcFields: ['cur_tp', 'pre_tp', 'end_tp'],
+                keys: ['id', 'detail_id', 'tree_id'],
+            };
+            this.tree = createNewPathTree('ledger', this.treeSetting);
+            this.spreadSetting = {
+                cols: [
+                    {title: '编号', colSpan: '1', rowSpan: '2', field: 'b_code', hAlign: 0, width: 230, formatter: '@', cellType: 'tree'},
+                    {title: '名称', colSpan: '1', rowSpan: '2', field: 'name', hAlign: 0, width: 185, formatter: '@'},
+                    {title: '规格', colSpan: '1', rowSpan: '2', field: 'spec', hAlign: 0, width: 150, formatter: '@'},
+                    {title: '单位', colSpan: '1', rowSpan: '2', field: 'unit', hAlign: 1, width: 50, formatter: '@', cellType: 'unit'},
+                    {title: '单价', colSpan: '1', rowSpan: '2', field: 'unit_price', hAlign: 2, width: 60, type: 'Number'},
+                    {title: '本期|数量', colSpan: '2|1', rowSpan: '1|1', field: 'cur_qty', hAlign: 2, width: 60, type: 'Number'},
+                    {title: '|金额', colSpan: '|1', rowSpan: '|1', field: 'cur_tp', hAlign: 2, width: 60, type: 'Number', readOnly: true},
+                    {title: '截止本期|数量', colSpan: '2|1', rowSpan: '1|1', field: 'end_qty', hAlign: 2, width: 60, type: 'Number'},
+                    {title: '|金额', colSpan: '|1', rowSpan: '|1', field: 'end_tp', hAlign: 2, width: 60, type: 'Number', readOnly: true},
+                    {title: '发票号', colSpan: '1', rowSpan: '2', field: 'invoice_code', hAlign: 0, width: 80, formatter: '@'},
+                    {title: '备注', colSpan: '1', rowSpan: '2', field: 'memo', hAlign: 0, width: 100, formatter: '@', cellType: 'ellipsisAutoTip'},
+                ],
+                emptyRows: 3,
+                headRows: 2,
+                headRowHeight: [25, 25],
+                defaultRowHeight: 21,
+                headerFont: '12px 微软雅黑',
+                font: '12px 微软雅黑',
+                readOnly: readOnly,
+            };
+            this.ckBillsSpread = window.location.pathname + '-billsSelect';
+
+            this.initSpread();
+            this.initOtherEvent();
+        }
+        initSpread() {
+            SpreadJsObj.initSheet(this.sheet, this.spreadSetting);
+            this.spread.bind(spreadNS.Events.SelectionChanged, this.selectionChanged);
+            this.spread.bind(spreadNS.Events.topRowChanged, this.topRowChanged);
+            this.spread.bind(spreadNS.Events.ClipboardChanging, function (e, info) {
+                const copyText = SpreadJsObj.getFilterCopyText(info.sheet);
+                SpreadJsObj.Clipboard.setCopyData(copyText);
+            });
+            if (readOnly) return;
+
+            this.spread.bind(spreadNS.Events.EditEnded, this.editEnded);
+            this.spread.bind(spreadNS.Events.EditStarting, this.editStarting);
+            this.spread.bind(spreadNS.Events.ClipboardPasting, this.clipboardPasting);
+            SpreadJsObj.addDeleteBind(this.spread, this.deletePress);
+        }
+        initOtherEvent() {
+            const self = this;
+            // 增删上下移升降级
+            $('a[name="base-opr"]').click(function () {
+                self.baseOpr(this.getAttribute('type'));
+            });
+        }
+        refreshOperationValid() {
+            const setObjEnable = function (obj, enable) {
+                if (enable) {
+                    obj.removeClass('disabled');
+                } else {
+                    obj.addClass('disabled');
+                }
+            };
+            const invalidAll = function () {
+                setObjEnable($('a[name=base-opr][type=add]'), false);
+                setObjEnable($('a[name=base-opr][type=delete]'), false);
+                setObjEnable($('a[name=base-opr][type=up-move]'), false);
+                setObjEnable($('a[name=base-opr][type=down-move]'), false);
+                setObjEnable($('a[name=base-opr][type=up-level]'), false);
+                setObjEnable($('a[name=base-opr][type=down-level]'), false);
+            };
+            const sel = this.sheet.getSelections()[0];
+            const row = sel ? sel.row : -1;
+            const tree = this.sheet.zh_tree;
+            if (!tree) {
+                invalidAll();
+                return;
+            }
+            const first = tree.nodes[row];
+            if (!first) {
+                invalidAll();
+                return;
+            }
+            let last = first, sameParent = true, nodeUsed = this.checkNodeUsed(tree, first);
+            if (sel.rowCount > 1 && first) {
+                for (let r = 1; r < sel.rowCount; r++) {
+                    const rNode = tree.nodes[sel.row + r];
+                    if (!rNode) {
+                        sameParent = false;
+                        break;
+                    }
+                    nodeUsed = nodeUsed || this.checkNodeUsed(tree, rNode);
+                    if (rNode.tree_level > first.tree_level) continue;
+                    if ((rNode.tree_level < first.tree_level) || (rNode.tree_level === first.tree_level && rNode.tree_pid !== first.tree_pid)) {
+                        sameParent = false;
+                        break;
+                    }
+                    last = rNode;
+                }
+            }
+            const preNode = tree.getPreSiblingNode(first);
+            const valid = !this.sheet.zh_setting.readOnly;
+
+            setObjEnable($('a[name=base-opr][type=add]'), valid && first && first.tree_level > 1);
+            setObjEnable($('a[name=base-opr][type=delete]'), valid && first && sameParent && first.tree_level > 1 && !nodeUsed);
+            setObjEnable($('a[name=base-opr][type=up-move]'), valid && first && sameParent && first.tree_level > 1 && preNode);
+            setObjEnable($('a[name=base-opr][type=down-move]'), valid && first && sameParent && first.tree_level > 1 && !tree.isLastSibling(last));
+            setObjEnable($('a[name=base-opr][type=up-level]'), valid && first && sameParent && tree.getParent(first) && !nodeUsed
+                && first.tree_level > 2 && !tree.isLastSibling(last));
+            setObjEnable($('a[name=base-opr][type=down-level]'), valid && first && sameParent
+                && first.tree_level > 1 && preNode && !this.checkNodeUsed(tree, preNode));
+        }
+        loadRelaData() {
+            this.refreshOperationValid();
+            SpreadJsObj.saveTopAndSelect(this.sheet, this.ckBillsSpread);
+        }
+        refreshTree(data) {
+            const sheet = this.sheet;
+            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));
+                    }
+                    SpreadJsObj.reLoadRowsData(sheet, rows);
+                }
+                // 处理展开
+                if (data.expand) {
+                    const expanded = [];
+                    for (const e of data.expand) {
+                        if (expanded.indexOf(e) === -1) {
+                            const posterity = tree.getPosterity(e);
+                            for (const p of posterity) {
+                                sheet.setRowVisible(tree.nodes.indexOf(p), p.visible);
+                                expanded.push(p);
+                            }
+                        }
+                    }
+                }
+            });
+        }
+        loadData(datas) {
+            this.tree.loadDatas(datas);
+            treeCalc.calculateAll(this.tree);
+            SpreadJsObj.loadSheetData(this.sheet, SpreadJsObj.DataType.Tree, this.tree);
+            SpreadJsObj.loadTopAndSelect(this.sheet, this.ckBillsSpread);
+            this.refreshOperationValid();
+        }
+        getDefaultSelectInfo() {
+            if (!this.tree) return;
+            const sel = this.sheet.getSelections()[0];
+            const node = this.sheet.zh_tree.nodes[sel.row];
+            if (!node) return;
+            let count = 1;
+            if (sel.rowCount > 1) {
+                for (let r = 1; r < sel.rowCount; r++) {
+                    const rNode = sheet.zh_tree.nodes[sel.row + r];
+                    if (rNode.tree_level > node.tree_level) continue;
+                    if ((rNode.tree_level < node.tree_level) || (rNode.tree_level === node.tree_level && rNode.tree_pid !== node.tree_pid)) {
+                        toastr.warning('请选择同一节点下的节点,进行该操作');
+                        return;
+                    }
+                    count += 1;
+                }
+            }
+            return [this.tree, node, count];
+        }
+        checkNodeUsed(tree, node) {
+            // todo 检查节点是否已使用
+            return false;
+        }
+        baseOpr(type, addCount = 1) {
+            const self = this;
+            const sheet = self.sheet;
+            const sel = sheet.getSelections()[0];
+            const [tree, node, count] = this.getDefaultSelectInfo();
+            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 (this.checkNodeUsed(tree, child)) {
+                        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 (this.checkNodeUsed(tree, child)) {
+                        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);
+                if (index > 0 && this.checkNodeUsed(tree, children[index-1])) {
+                    toastr.warning('其前节点已计量,选中的节点不可降级');
+                    return;
+                }
+                for (let i = index; i < count; i++) {
+                    const child = children[i+index];
+                    if (this.checkNodeUsed(tree, child)) {
+                        toastr.warning('选中的节点已计量,不可降级');
+                        return;
+                    }
+                }
+            }
+
+            const updateData = {
+                postType: type,
+                postData: {
+                    id: node.tree_id,
+                    count: type === 'add' ? addCount : count,
+                }
+            };
+            if (type === 'delete') {
+                deleteAfterHint(function () {
+                    postData('update', updateData, function (result) {
+                        const refreshData = tree.loadPostData(result);
+                        self.refreshTree(refreshData);
+                        if (sel) {
+                            sheet.setSelection(sel.row, sel.col, 1, sel.colCount);
+                        }
+                        self.refreshOperationValid();
+                    });
+                });
+            } else {
+                postData('update', updateData, function (result) {
+                    const refreshData = tree.loadPostData(result);
+                    self.refreshTree(refreshData);
+                    if (['up-move', 'down-move'].indexOf(type) > -1) {
+                        if (sel) {
+                            sheet.setSelection(tree.nodes.indexOf(node), sel.col, sel.rowCount, sel.colCount);
+                            // SpreadJsObj.reloadRowsBackColor(sheet, [sel.row, tree.nodes.indexOf(node)]);
+                        }
+                    } 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);
+                            // SpreadJsObj.reloadRowsBackColor(sheet, [sel.row, tree.nodes.indexOf(refreshData.create[0])]);
+                        }
+                    }
+                    self.refreshOperationValid();
+                });
+            }
+        }
+        // 事件
+        selectionChanged(e, info) {
+            if (info.newSelections) {
+                if (!info.oldSelections || info.newSelections[0].row !== info.oldSelections[0].row) {
+                    billsObj.loadRelaData();
+                }
+            }
+        }
+        topRowChanged(e, info) {
+            SpreadJsObj.saveTopAndSelect(info.sheet, billsObj.ckBillsSpread);
+        }
+        editEnded(e, info) {
+            if (!info.sheet.zh_setting) return;
+
+            const tree = info.sheet.zh_tree;
+            const node = SpreadJsObj.getSelectObject(info.sheet);
+            const data = { id: node.id, detail_id: node.detail_id, tree_id: node.tree_id };
+            // 未改变值则不提交
+            const col = info.sheet.zh_setting.cols[info.col];
+            const orgValue = node[col.field];
+            const newValue = trimInvalidChar(info.editingText);
+            if (orgValue == info.editingText || ((!orgValue || orgValue === '') && (newValue === ''))) return;
+
+            if (info.editingText) {
+                const text = newValue;
+                if (billsObj.checkNodeUsed(tree, node) && col.field ==='b_code' && orgValue !== '' && text === '') {
+                    toastr.error('节点已计量,请勿删除编号');
+                    SpreadJsObj.reLoadRowData(info.sheet, info.row);
+                    return;
+                }
+                if (col.type === 'Number') {
+                    const num = _.toNumber(text);
+                    if (_.isFinite(num)) {
+                        data[col.field] = num;
+                    } else {
+                        try {
+                            data[col.field] = math.evaluate(transExpr(text));
+                        } catch(err) {
+                            toastr.error('输入的表达式非法');
+                            SpreadJsObj.reLoadRowData(info.sheet, info.row);
+                            return;
+                        }
+                    }
+                } else {
+                    data[col.field] = text;
+                }
+            } else {
+                if (billsObj.checkNodeUsed(tree, node) && (col.field ==='b_code') && orgValue !== '') {
+                    toastr.error('节点已计量,请勿删除编号');
+                    SpreadJsObj.reLoadRowData(info.sheet, info.row);
+                    return;
+                }
+                data[col.field] = col.type === 'Number' ? 0 : '';
+            }
+            // 更新至服务器
+            postData('update', {postType: 'update', postData: data}, function (result) {
+                const refreshNode = billsObj.tree.loadPostData(result);
+                billsObj.refreshTree(refreshNode);
+            }, function () {
+                SpreadJsObj.reLoadRowData(info.sheet, info.row, 1);
+            });
+        }
+        editStarting(e, info) {
+            if (!info.sheet.zh_setting || !info.sheet.zh_tree) return;
+
+            const tree = info.sheet.zh_tree;
+            const col = info.sheet.zh_setting.cols[info.col];
+            const node = info.sheet.zh_tree.nodes[info.row];
+            if (!node) {
+                info.cancel = true;
+                return;
+            }
+
+            switch (col.field) {
+                case 'b_code':
+                    info.cancel = readOnly || billsObj.checkNodeUsed(tree, node);
+                    break;
+                case 'unit_price':
+                    info.cancel = readOnly || (node.children && node.children.length > 0) || billsObj.checkNodeUsed(tree, node);
+                    break;
+                case 'cur_qty':
+                case 'cur_tp':
+                    info.cancel = (node.children && node.children.length > 0);
+                    break;
+            }
+        }
+        deletePress (sheet) {
+            if (!sheet.zh_setting) return;
+            const sel = sheet.getSelections()[0], datas = [];
+            for (let iRow = sel.row; iRow < sel.row + sel.rowCount; iRow++) {
+                let bDel = false;
+                const node = sheet.zh_tree.nodes[iRow];
+                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];
+                    const style = sheet.getStyle(iRow, iCol);
+                    if (style.locked) continue;
+
+                    if (col.field === 'b_code' &&  sheet.zh_tree.checkNodeUsed(node, pos)) {
+                        toastr.warning(`"${node.b_code || ''} ${node.name}"已计量,请勿修改`);
+                        return;
+                    }
+
+                    data[col.field] = col.type === 'Number' ? 0 : '';
+                    bDel = true;
+                }
+                if (bDel) datas.push(data);
+            }
+            if (datas.length > 0) {
+                postData('update', {postType: 'update', postData: datas}, function (result) {
+                    const refreshNode = sheet.zh_tree.loadPostData(result);
+                    billsObj.refreshTree(refreshNode);
+                }, function () {
+                    SpreadJsObj.reLoadRowData(info.sheet, sel.row, sel.rowCount);
+                });
+            }
+        }
+        clipboardPasting(e, info) {
+            info.cancel = true;
+            const tree = info.sheet.zh_tree, setting = info.sheet.zh_setting;
+            if (!setting || !tree) return;
+
+            const pasteData = info.pasteData.html
+                ? SpreadJsObj.analysisPasteHtml(info.pasteData.html)
+                : (info.pasteData.text === ''
+                    ? SpreadJsObj.Clipboard.getAnalysisPasteText()
+                    : SpreadJsObj.analysisPasteText(info.pasteData.text));
+            const hint = {
+                usedUp: {type: 'warning', msg: '节点已计量,不可修改单价'},
+                usedCode: {type: 'warning', msg: '节点已计量,编号不可修改'},
+                invalidExpr: {type: 'warning', msg: '粘贴的表达式非法'},
+                parent: {type: 'warning', msg: '含有子项的清单,不可粘贴数量、单价、金额'},
+            };
+            const datas = [], filterNodes = [];
+
+            let level, filterRow = 0;
+            for (let iRow = 0; iRow < info.cellRange.rowCount; iRow ++) {
+                const curRow = info.cellRange.row + iRow;
+                const node = tree.nodes[curRow];
+                if (!node) continue;
+
+                if (!level) level = node.level;
+                if (node.level < level) break;
+
+                let bPaste = false;
+                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 = trimInvalidChar(pasteData[iRow-filterRow][iCol]);
+                    if (node.children && node.children.length > 0 && invalidFields.parent.indexOf(colSetting.field) >= 0) {
+                        toastMessageUniq(hint.parent);
+                        continue;
+                    }
+
+                    if (billsObj.checkNodeUsed(tree, node) && colSetting.field === 'unit_price') {
+                        toastMessageUniq (hint.usedUp);
+                        continue;
+                    }
+                    if (colSetting.type === 'Number') {
+                        const num = _.toNumber(value);
+                        if (num) {
+                            data[colSetting.field] = num;
+                        } else {
+                            try {
+                                data[colSetting.field] = math.evaluate(transExpr(value));
+                                bPaste = true;
+                            } catch (err) {
+                                toastMessageUniq(hint.invalidExpr);
+                                continue;
+                            }
+                        }
+                    } else {
+                        if (node.used && (colSetting.field ==='b_code') && data[colSetting.field] !== '' && value === '') {
+                            toastMessageUniq(hint.usedCode);
+                            continue;
+                        }
+                        data[colSetting.field] = value;
+                    }
+                    bPaste = true;
+                }
+                if (bPaste) {
+                    datas.push(data);
+                } else {
+                    filterNodes.push(node);
+                }
+            }
+            if (datas.length > 0) {
+                postData('update', {postType: 'update', postData: datas}, function (result) {
+                    const refreshNode = tree.loadPostData(result);
+                    if (refreshNode.update) refreshNode.update = refreshNode.update.concat(filterNodes);
+                    billsObj.refreshTree(refreshNode);
+                }, function () {
+                    SpreadJsObj.reLoadRowData(info.sheet, info.cellRange.row, info.cellRange.rowCount);
+                });
+            } else {
+                SpreadJsObj.reLoadRowData(info.sheet, info.cellRange.row, info.cellRange.rowCount);
+            }
+        }
+    }
+    const billsObj = new BillsObj();
+
+    // 清单右键菜单
+    const billsContextMenuOptions = {
+        selector: '#bills-spread',
+        build: function ($trigger, e) {
+            const target = SpreadJsObj.safeRightClickSelection($trigger, e, billsSpread);
+            billsObj.loadRelaData();
+            return target.hitTestType === spreadNS.SheetArea.viewport || target.hitTestType === spreadNS.SheetArea.rowHeader;
+        },
+        items: {}
+    };
+    if (!readOnly) {
+        billsContextMenuOptions.items.create = {
+            name: '新增',
+            icon: 'fa-sign-in',
+            callback: function (key, opt) {
+                billsObj.baseOpr('add');
+            },
+            disabled: function (key, opt) {
+                const sheet = billsObj.sheet;
+                const selection = sheet.getSelections();
+                const sel = selection ? selection[0] : sheet.getSelections()[0];
+                const row = sel ? sel.row : -1;
+                const tree = sheet.zh_tree;
+                if (!tree) return true;
+                const first = sheet.zh_tree.nodes[row];
+                const valid = !sheet.zh_setting.readOnly;
+                return !(valid && first && first.tree_level > 1);
+            }
+        };
+        billsContextMenuOptions.items.delete = {
+            name: '删除',
+            icon: 'fa-remove',
+            callback: function (key, opt) {
+                billsObj.baseOpr('delete');
+            },
+            disabled: function (key, opt) {
+                const sheet = billsObj.sheet;
+                const selection = sheet.getSelections();
+                const sel = selection ? selection[0] : sheet.getSelections()[0];
+                const row = sel ? sel.row : -1;
+                const tree = sheet.zh_tree;
+                if (!tree) return true;
+                const first = sheet.zh_tree.nodes[row];
+                let last = first, sameParent = true, nodeUsed = billsObj.checkNodeUsed(tree, first);
+                if (sel.rowCount > 1 && first) {
+                    for (let r = 1; r < sel.rowCount; r++) {
+                        const rNode = tree.nodes[sel.row + r];
+                        if (!rNode) {
+                            sameParent = false;
+                            break;
+                        }
+                        nodeUsed = nodeUsed || billsObj.checkNodeUsed(tree, rNode);
+                        if (rNode.tree_level > first.tree_level) continue;
+                        if ((rNode.tree_level < first.tree_level) || (rNode.tree_level === first.tree_level && rNode.tree_pid !== first.tree_pid)) {
+                            sameParent = false;
+                            break;
+                        }
+                        last = rNode;
+                    }
+                }
+                const valid = !sheet.zh_setting.readOnly;
+                return !(valid && first && sameParent && !(first.tree_level === 1) && !nodeUsed);
+            }
+        };
+    }
+
+    // 加载安全生产费数据
+    postData('load', { filter: 'bills' }, function(result) {
+        billsObj.loadData(result.bills);
+    });
+
+    const stdGclSetting = {
+        selector: '#std-gcl',
+        stdType: 'gcl',
+        libs: stdBills,
+        treeSetting: {
+            id: 'bill_id',
+            pid: 'pid',
+            order: 'order',
+            level: 'level',
+            isLeaf: 'is_leaf',
+            fullPath: 'full_path',
+            rootId: -1,
+            keys: ['id', 'list_id', 'bill_id']
+        },
+        spreadSetting: {
+            cols: [
+                {title: '清单编号', field: 'b_code', hAlign: 0, width: 170, formatter: '@', cellType: 'tree'},
+                {title: '名称', field: 'name', hAlign: 0, width: 150, formatter: '@'},
+                {title: '单位', field: 'unit', hAlign: 1, width: 50, formatter: '@'}
+            ],
+            treeCol: 0,
+            emptyRows: 0,
+            headRows: 1,
+            headRowHeight: [32],
+            defaultRowHeight: 21,
+            headerFont: '12px 微软雅黑',
+            font: '12px 微软雅黑',
+            headColWidth: [30],
+            selectedBackColor: '#fffacd',
+            readOnly: true,
+        },
+        page: 'paymentSafe',
+        tid: getTenderId(),
+        cellDoubleClick: function (e, info) {
+            const stdSheet = info.sheet;
+            const stdTree = stdSheet.zh_tree;
+            const stdNode = stdTree.nodes[info.row];
+            if (!stdNode || !stdNode.b_code) return;
+
+            const mainSheet = billsObj.sheet;
+            if (!stdSheet.zh_setting || !stdSheet.zh_tree || !mainSheet.zh_tree) return;
+            const mainTree = mainSheet.zh_tree;
+
+            const nodes = [stdNode, ...stdTree.getAllParents(stdNode)];
+            nodes.sort((a, b) => { return a.level - b.level; });
+            const stdData = [];
+            let mainChildren = mainTree.children, mainCur, checkNode;
+            for (const sd of nodes) {
+                const field = sd.b_code ? 'b_code' : 'name';
+                checkNode = mainChildren.find(x => { return x[field] === sd[field]; });
+                if (!checkNode) {
+                    stdData.push({ b_code: sd.b_code, name: sd.name, unit: sd.unit });
+                } else {
+                    mainCur = checkNode;
+                    mainChildren = mainCur ? mainCur.children : [];
+                }
+            }
+
+            postData('update', { postType: 'add-std',
+                postData: {
+                    id: mainCur ? mainCur.tree_id : mainTree.setting.rootId,
+                    stdData
+                }
+            }, function (result) {
+                const refreshNode = mainTree.loadPostData(result);
+                billsObj.refreshTree(refreshNode);
+                const node = _.find(billsObj.tree.nodes, { code: stdNode.code, name: stdNode.name });
+                if (node) {
+                    mainSheet.setSelection(billsObj.tree.nodes.indexOf(node), sel.col, sel.rowCount, sel.colCount);
+                    SpreadJsObj.reloadRowsBackColor(mainSheet, [sel.row, billsObj.tree.nodes.indexOf(node)]);
+                }
+                billsObj.refreshOperationValid;
+                billsObj.spread.focus();
+            });
+        },
+    };
+
+    // 展开收起标准清单
+    $('a', '#side-menu').bind('click', function (e) {
+        e.preventDefault();
+        const tab = $(this), tabPanel = $(tab.attr('content'));
+        // 展开工具栏、切换标签
+        if (!tab.hasClass('active')) {
+            // const close = $('.active', '#side-menu').length === 0;
+            $('a', '#side-menu').removeClass('active');
+            $('.tab-content .tab-select-show.tab-pane.active').removeClass('active');
+            tab.addClass('active');
+            tabPanel.addClass('active');
+            // $('.tab-content .tab-pane').removeClass('active');
+            showSideTools(tab.hasClass('active'));
+            if (tab.attr('content') === '#std-gcl') {
+                if (!stdGcl) stdGcl = $.stdLib(stdGclSetting);
+                stdGcl.spread.refresh();
+            } else if (tab.attr('content') === '#fujian') {
+                // todo 附件
+            }
+        } else { // 收起工具栏
+            tab.removeClass('active');
+            tabPanel.removeClass('active');
+            showSideTools(tab.hasClass('active'));
+        }
+        billsObj.spread.refresh();
+    });
+
+    // 工具栏spr
+    $.divResizer({
+        select: '#revise-right-spr',
+        callback: function () {
+            billsObj.spread.refresh();
+            if (stdGcl) stdGcl.spread.refresh();
+        }
+    });
+    // 导航Menu
+    $.subMenu({
+        menu: '#sub-menu', miniMenu: '#sub-mini-menu', miniMenuList: '#mini-menu-list',
+        toMenu: '#to-menu', toMiniMenu: '#to-mini-menu',
+        key: 'menu.1.0.0',
+        miniHint: '#sub-mini-hint', hintKey: 'menu.hint.1.0.1',
+        callback: function (info) {
+            if (info.mini) {
+                $('.panel-title').addClass('fluid');
+                $('#sub-menu').removeClass('panel-sidebar');
+            } else {
+                $('.panel-title').removeClass('fluid');
+                $('#sub-menu').addClass('panel-sidebar');
+            }
+            autoFlashHeight();
+            billsObj.spread.refresh();
+            if (stdGcl) stdGcl.spread.refresh();
+        }
+    });
+    // 显示层次
+    (function (select, sheet) {
+        $(select).click(function () {
+            if (!sheet.zh_tree) return;
+            const tag = $(this).attr('tag');
+            const tree = sheet.zh_tree;
+            setTimeout(() => {
+                showWaitingView();
+                switch (tag) {
+                    case "1":
+                    case "2":
+                    case "3":
+                    case "4":
+                        tree.expandByLevel(parseInt(tag));
+                        SpreadJsObj.refreshTreeRowVisible(sheet);
+                        break;
+                    case "last":
+                        tree.expandByCustom(() => { return true; });
+                        SpreadJsObj.refreshTreeRowVisible(sheet);
+                        break;
+                }
+                closeWaitingView();
+            }, 100);
+        });
+    })('a[name=showLevel]', billsObj.sheet);
+});

+ 1 - 1
app/public/js/shares/cs_tools.js

@@ -1314,7 +1314,7 @@ const showSelectTab = function(select, spread, afterShow) {
 
         const spread = SpreadJsObj.createNewSpread($(`#std-${setting.stdType}-spread`)[0]);
         const sheet = spread.getActiveSheet();
-        SpreadJsObj.initSheet(sheet, spreadSetting);
+        SpreadJsObj.initSheet(sheet, setting.spreadSetting || spreadSetting);
 
         if (setting.cellDoubleClick) sheet.bind(spreadNS.Events.CellDoubleClick, setting.cellDoubleClick);
         sheet.bind(spreadNS.Events.SelectionChanged, function (e, info) {

+ 5 - 4
app/public/js/spreadjs_rela/spreadjs_zh.js

@@ -1391,12 +1391,13 @@ const SpreadJsObj = {
 
 
                 const tree = options.sheet.zh_tree;
+                const setting = tree.setting;
                 // 使用TreeCellType前,需定义sheet.tree
                 if (tree) {
                     const node = options.row < tree.nodes.length ? tree.nodes[options.row] : null;
                     if (node) {
                         const showTreeLine = true;
-                        const centerX = Math.floor(x) + (node.level - 1) * indent + (node.level) * levelIndent + indent / 2 + xOffset;
+                        const centerX = Math.floor(x) + (node[setting.level] - 1) * indent + (node[setting.level]) * levelIndent + indent / 2 + xOffset;
                         const centerY = Math.floor((y + (y + h)) / 2);
                         // Draw Sibling Line
                         if (showTreeLine) {
@@ -1416,7 +1417,7 @@ const SpreadJsObj = {
                                 const y1 = tree.isLastViewSibling(node) ? centerY : y + h;
                                 const parent = tree.getParent(node);
                                 const y2 = y1 - centerY;
-                                if (node.order === 1 && !parent) {
+                                if (node[setting.order] === 1 && !parent) {
                                     if (dotLine) {
                                         drawDotLine(canvas, centerX, centerY, centerX, y1, lineColor);
                                         // drawDotLine(canvas, centerX, centerY, centerX, y1, style.foreColor);
@@ -1436,7 +1437,7 @@ const SpreadJsObj = {
                             }
                         }
                         // Draw Expand Box
-                        if (!node.is_leaf) {
+                        if (!node[setting.isLeaf]) {
                             drawExpandBox(canvas, x, y, w, h, centerX, centerY, node.expanded, style);
                         }
                         // Draw Parent Line
@@ -1459,7 +1460,7 @@ const SpreadJsObj = {
                             }
                         };
                         // 重定位x
-                        const move = (node.level) * indent + (node.level) * levelIndent + xOffset;
+                        const move = (node[setting.level]) * indent + (node[setting.level]) * levelIndent + xOffset;
                         x = x + move;
                         w = w - move;
                     }

+ 5 - 0
app/router.js

@@ -758,6 +758,11 @@ module.exports = app => {
     app.post('/payment/:id/detail/:did/file/upload', sessionAuth, paymentTenderCheck, paymentDetailCheck, 'paymentController.uploadDetailFile');
     app.post('/payment/:id/detail/:did/file/delete', sessionAuth, paymentTenderCheck, paymentDetailCheck, 'paymentController.deleteDetailFile');
     app.get('/payment/:id/detail/:did/file/:fid/download', sessionAuth, paymentTenderCheck, paymentDetailCheck, 'paymentController.downloadDetailFile');
+    // 安全生产费
+    app.get('/payment/:id/safe/:did/bills', sessionAuth, paymentTenderCheck, paymentDetailCheck, 'paymentController.safeBills');
+    app.get('/payment/:id/safe/:did/compare', sessionAuth, paymentTenderCheck, paymentDetailCheck, 'paymentController.safeCompare');
+    app.post('/payment/:id/safe/:did/load', sessionAuth, paymentTenderCheck, paymentDetailCheck, 'paymentController.safeLoad');
+    app.post('/payment/:id/safe/:did/update', sessionAuth, paymentTenderCheck, paymentDetailCheck, 'paymentController.safeUpdate');
 
 
     // 企业微信回调

+ 81 - 20
app/service/payment_detail.js

@@ -1,5 +1,7 @@
 'use strict';
 const auditConst = require('../const/audit').stage;
+const paymentConst = require('../const/payment');
+
 module.exports = app => {
     class PaymentDetail extends app.BaseService {
         constructor(ctx) {
@@ -37,36 +39,40 @@ module.exports = app => {
             return result !== 0;
         }
 
-        async addDetail(trInfo, code, s_time) {
+        async addCommomCheck(trInfo, code) {
+            const details = await this.getAllDataByCondition({
+                where: { tr_id: trInfo.id },
+                order: ['order'],
+            });
+            const preDetail = details[details.length - 1];
+            if (details.length > 0 && details[details.length - 1].status !== auditConst.status.checked) {
+                throw '上一期未审批通过,请等待上一期审批通过后,再新增';
+            }
+
+            if (this._.findIndex(details, { code }) !== -1) {
+                throw '编号不能重复';
+            }
+
+            trInfo.rpt_audit = JSON.parse(trInfo.rpt_audit);
+            if (this._.findIndex(trInfo.rpt_audit, { uid: null }) !== -1) {
+                throw '未配置好表单角色,无法新建';
+            }
+            return preDetail;
+        }
+
+        async addFormDetail(trInfo, code, s_time) {
             const transaction = await this.db.beginTransaction();
             try {
                 if (!(trInfo.is_del === 0 && trInfo.rpt_audit)) {
                     throw '报表已删除或表单人员数据有误,无法新建';
                 }
-                const details = await this.getAllDataByCondition({
-                    where: { tr_id: trInfo.id },
-                    order: ['order'],
-                });
-                const preDetail = details[details.length - 1];
-                if (details.length > 0 && details[details.length - 1].status !== auditConst.status.checked) {
-                    throw '上一期未审批通过,请等待上一期审批通过后,再新增';
-                }
-
-                if (this._.findIndex(details, { code }) !== -1) {
-                    throw '编号不能重复';
-                }
-
-                trInfo.rpt_audit = JSON.parse(trInfo.rpt_audit);
-                if (this._.findIndex(trInfo.rpt_audit, { uid: null }) !== -1) {
-                    throw '未配置好表单角色,无法新建';
-                }
+                const preDetail = await this.addCommomCheck(trInfo, code);
                 const rptTpl = await this.ctx.service.rptTpl.getDataById(trInfo.rpt_id);
                 const pageRst = this.ctx.service.jpcReport.getAllPreviewPagesCommon(rptTpl, 'A4');
-                const order = details.length + 1;
                 const newDetail = {
                     tender_id: this.ctx.paymentTender.id,
                     tr_id: trInfo.id,
-                    order,
+                    order: preDetail.order ? preDetail.order + 1 : 1,
                     times: 1,
                     status: auditConst.status.uncheck,
                     uid: this.ctx.session.sessionUser.accountId,
@@ -112,6 +118,61 @@ module.exports = app => {
             }
         }
 
+        async addSafeDetail(trInfo, code, s_time) {
+            const transaction = await this.db.beginTransaction();
+            try {
+                const preDetail = await this.addCommomCheck(trInfo, code);
+                const newDetail = {
+                    tender_id: this.ctx.paymentTender.id,
+                    tr_id: trInfo.id,
+                    order: preDetail && preDetail.order ? preDetail.order + 1 : 1,
+                    times: 1,
+                    status: auditConst.status.uncheck,
+                    uid: this.ctx.session.sessionUser.accountId,
+                    s_time,
+                    code,
+                    in_time: new Date(),
+                    type: paymentConst.modes_value_object.safe,
+                };
+                const result = await transaction.insert(this.tableName, newDetail);
+                if (result.affectedRows === 1) {
+                    newDetail.id = result.insertId;
+                } else {
+                    throw '新增支付审批数据失败';
+                }
+                // 初始化安全生产费
+                await this.ctx.service.paymentSafeBills.init(newDetail, transaction);
+
+                // 存在上一期时,复制上一期审批流程
+                if (preDetail) {
+                    const auditResult = await this.ctx.service.paymentDetailAudit.copyPreDetailAuditors(transaction, preDetail, newDetail);
+                    if (!auditResult) {
+                        throw '复制上一期审批流程失败';
+                    }
+                }
+                // 更新is_change值
+                await transaction.update(this.ctx.service.paymentTenderRpt.tableName, { id: trInfo.id, is_change: 0 });
+                await transaction.commit();
+                return newDetail;
+            } catch (err) {
+                await transaction.rollback();
+                throw err;
+            }
+        }
+
+        async addDetail(trInfo, code, s_time) {
+            switch (trInfo.type) {
+                case paymentConst.modes_value_object.form:
+                    this.addFormDetail(trInfo, code, s_time);
+                    break;
+                case paymentConst.modes_value_object.safe:
+                    this.addSafeDetail(trInfo, code, s_time);
+                    break;
+                default:
+                    throw '未知类型,新建失败';
+            }
+        }
+
         async updateReportJson(id, report_json) {
             return await this.db.update(this.tableName, { id, report_json: JSON.stringify(report_json) });
         }

+ 442 - 0
app/service/payment_safe_bills.js

@@ -0,0 +1,442 @@
+'use strict';
+
+/**
+ *
+ * 支付审批-安全生产
+ * @author Mai
+ * @date
+ * @version
+ */
+
+const defaultBills = require('../const/payment').defaultSafeBills;
+const billsUtils = require('../lib/bills_utils');
+const SafeBillsFields = {
+    textFields: ['b_code', 'name', 'unit', 'spec', 'invoice_code', 'memo'],
+    calcFields: ['unit_price', 'cur_qty', 'cur_tp', 'end_qty', 'end_tp'],
+    fixedFields: ['safe_id', 'tender_id', 'pre_qty', 'pre_tp', 'cur_his_qty', 'cur_his_tp', 'add_user_id', 'add_time'],
+    treeFields: ['detail_id', 'tree_id', 'tree_pid', 'tree_level', 'tree_order', 'tree_full_path', 'tree_is_leaf'],
+};
+
+module.exports = app => {
+
+    class Ledger extends app.BaseTreeService {
+
+        /**
+         * 构造函数
+         *
+         * @param {Object} ctx - egg全局变量
+         * @return {void}
+         */
+        constructor(ctx) {
+            super(ctx, {
+                mid: 'detail_id',
+                kid: 'tree_id',
+                pid: 'tree_pid',
+                order: 'tree_order',
+                level: 'tree_level',
+                isLeaf: 'tree_is_leaf',
+                fullPath: 'tree_full_path',
+                keyPre: 'safe_bills_maxLid:',
+                uuid: true,
+            });
+            // this.depart = 10;
+            this.tableName = 'payment_safe_bills';
+            this.decimal = { tp: 0, up: 2, qty: 2 };
+        }
+
+        // 继承方法
+        clearParentingData(data) {
+            for (const f of SafeBillsFields.calcFields) {
+                data[f] = 0;
+            }
+        }
+
+        _getDefaultData(data, detail) {
+            data.id = this.uuid.v4();
+            data.safe_id = data.id;
+            data.tender_id = detail.tender_id;
+            data.detail_id = detail.id;
+            data.add_user_id = this.ctx.session.sessionUser.accountId;
+        }
+
+        async init(detail, transaction) {
+            if (!detail || !transaction) throw '安全生产费数据错误';
+
+            const insertData = [];
+            for (const b of defaultBills) {
+                const bills = JSON.parse(JSON.stringify(b));
+                this._getDefaultData(bills, detail);
+                insertData.push(bills);
+            }
+
+            const operate = await transaction.insert(this.tableName, insertData);
+            return operate.affectedRows === insertData.length;
+        }
+
+        /**
+         * 新增数据(供内部或其他service类调用, controller不可直接使用)
+         * @param {Array|Object} data - 新增数据
+         * @param {Number} detailId - 标段id
+         * @param {Object} transaction - 新增事务
+         * @return {Promise<boolean>} - {Promise<是否正确新增成功>}
+         */
+        async innerAdd(data, detailId, transaction) {
+            const datas = data instanceof Array ? data : [data];
+            if (detailId <= 0) {
+                throw '标段id错误';
+            }
+            if (datas.length <= 0) {
+                throw '插入数据为空';
+            }
+            if (!transaction) {
+                throw '内部错误';
+            }
+            // 整理数据
+            const insertData = [];
+            for (const tmp of datas) {
+                tmp[this.setting.id] = tmp.template_id;
+                tmp[this.setting.pid] = tmp.pid;
+                tmp[this.setting.mid] = detailId;
+                delete tmp.template_id;
+                delete tmp.pid;
+                tmp.id = this.uuid.v4();
+                insertData.push(tmp);
+            }
+            const operate = await transaction.insert(this.tableName, insertData);
+            return operate.affectedRows === datas.length;
+        }
+        /**
+         * 新增数据
+         *
+         * @param {Object} data - 新增的数据(可批量)
+         * @param {Number} detailId - 支付审批期id
+         * @return {Boolean} - 返回新增的结果
+         */
+        async add(data, detailId) {
+            this.transaction = await this.db.beginTransaction();
+            let result = false;
+            try {
+                result = await this.innerAdd(data, detailId, this.transaction);
+                if (!result) {
+                    throw '新增数据错误';
+                }
+                await this.transaction.commit();
+            } catch (error) {
+                await this.transaction.rollback();
+                result = false;
+            }
+
+            return result;
+        }
+
+        /**
+         * 根据节点Id获取数据
+         *
+         * @param {Number} detailId - 标段id
+         * @param {Number} nodeId - 项目节/工程量清单节点id
+         * @return {Object} - 返回查询到的节点数据
+         */
+        async getDataByNodeId(detailId, nodeId) {
+            if ((nodeId <= 0) || (detailId <= 0)) {
+                return undefined;
+            }
+            const where = {};
+            where[this.setting.mid] = detailId;
+            where[this.setting.id] = nodeId;
+            const data = await this.db.getDataByCondition(where);
+
+            return data;
+        }
+        /**
+         * 根据节点Id获取数据
+         * @param {Number} detailId - 期Id
+         * @param {Array} nodesIds - 节点Id
+         * @return {Array}
+         */
+        async getDataByNodeIds(detailId, nodesIds) {
+            if (detailId <= 0) {
+                return [];
+            }
+
+            const where = {};
+            where[this.setting.mid] = detailId;
+            where[this.setting.id] = nodesIds;
+            const data = await this.db.getAllDataByCondition({ where });
+
+            return this._.sortBy(data, function(d) {
+                return nodesIds.indexOf(d.ledger_id);
+            });
+        }
+        /**
+         * 根据主键id获取数据
+         * @param {Array|Number} id - 主键id
+         * @return {Promise<*>}
+         */
+        async getDataByIds(id) {
+            if (!id) {
+                return [];
+            }
+            const ids = id instanceof Array ? id : [id];
+            if (ids.length === 0) {
+                return [];
+            }
+
+            const data = await this.db.getAllDataByCondition({ where: { id: ids } });
+
+            return data;
+        }
+
+        /**
+         * 根据 父节点id 获取子节点
+         * @param detailId
+         * @param nodeId
+         * @return {Promise<*>}
+         */
+        async getChildrenByParentId(detailId, nodeId) {
+            if (detailId <= 0 || !nodeId) {
+                return undefined;
+            }
+            const nodeIds = nodeId instanceof Array ? nodeId : [nodeId];
+            if (nodeIds.length === 0) {
+                return [];
+            }
+
+            const where = {};
+            where[this.setting.mid] = detailId;
+            where[this.setting.pid] = nodeIds;
+            const data = await this.getAllDataByCondition({ where, orders: [[this.setting.order, 'ASC']] });
+
+            return data;
+        }
+
+        async pasteBlockData(detailId, targetId, pasteData, defaultData) {
+            const setting = this.setting;
+            if ((detailId <= 0) || (sid <= 0)) return [];
+
+            if (!pasteData || pasteData.length <= 0) throw '复制数据错误';
+            for (const pd of pasteData) {
+                if (!pd || pd.length <= 0) throw '复制数据错误';
+                pd.sort(function (x, y) {
+                    return x[setting.level] - y[setting.level]
+                });
+                if (pd[0][this.setting.pid] !== pasteData[0][0][this.setting.pid]) throw '复制数据错误:仅可操作同层节点';
+            }
+            this.newBills = false;
+            const targetData = await this.getDataByKid(detailId, targetId);
+            if (!targetData) throw '粘贴数据错误';
+
+            const newParentPath = targetData.full_path.replace(targetData.ledger_id, '');
+            const tpDecimal = this.decimal;
+
+            const pasteBillsData = [], leafBillsId = [];
+            let maxId = await this._getMaxLid(this.ctx.paymentTender.id);
+            for (const [i, pd] of pasteData.entries()) {
+                for (const d of pd) {
+                    d.children = pd.filter(function (x) {
+                        return x[setting.pid] === d[setting.id];
+                    });
+                }
+                const pbd = [];
+                for (const [j, d] of pd.entries()) {
+                    const newBills = {
+                        id: this.uuid.v4(),
+                        safe_id: this.uuid.v4(),
+                        b_code: d.b_code,
+                        name: d.name,
+                        unit: d.unit,
+                        unit_price: this.ctx.helper.round(d.unit_price, tpDecimal.up),
+                        memo: d.memo,
+                    };
+                    newBills[setting.mid] = detailId;
+                    newBills[setting.id] = maxId + j + 1;
+                    newBills[setting.pid] = j === 0 ? targetData[setting.pid] : d[setting.pid];
+                    newBills[setting.level] = d[setting.level] + targetData[setting.level] - pd[0][setting.level];
+                    newBills[setting.order] = j === 0 ? targetData[setting.order] + i + 1 : d[setting.order];
+                    newBills[setting.isLeaf] = d[setting.isLeaf];
+
+                    for (const c of d.children) {
+                        c[setting.pid] = newBills[setting.id];
+                    }
+                    newBills.quantity = this.ctx.helper.round(d.quantity, tpDecimal.qty);
+                    newBills.total_price = this.ctx.helper.mul(newBills.quantity, newBills.unit_price, tpDecimal.tp);
+                    if (defaultData) this.ctx.helper._.assignIn(newBills, defaultData);
+                    pbd.push(newBills);
+                }
+                for (const d of pbd) {
+                    const parent = pbd.find(function (x) {
+                        return x[setting.id] === d[setting.pid];
+                    });
+                    d[setting.fullPath] = parent
+                        ? parent[setting.fullPath] + '-' + d[setting.id]
+                        : newParentPath + d[setting.id];
+                    if (defaultData) this.ctx.helper._.assignIn(pbd, defaultData);
+                    pasteBillsData.push(d);
+                }
+                maxId = maxId + pbd.length;
+            }
+
+            this.transaction = await this.db.beginTransaction();
+            try {
+                // 选中节点的所有后兄弟节点,order+粘贴节点个数
+                await this._updateChildrenOrder(tid, targetData[setting.pid], targetData[setting.order] + 1, pasteData.length);
+                // 数据库创建新增节点数据
+                if (pasteBillsData.length > 0) {
+                    const newData = await this.transaction.insert(this.tableName, pasteBillsData);
+                }
+                this._cacheMaxLid(tid, maxId);
+                await this.transaction.commit();
+            } catch (err) {
+                await this.transaction.rollback();
+                throw err;
+            }
+
+            // 查询应返回的结果
+            const updateData = await this.getNextsData(targetData[setting.mid], targetData[setting.pid], targetData[setting.order] + pasteData.length);
+            return { create: pasteBillsData, update: updateData };
+        }
+
+        async addSafeBillsNode(detail, targetId, count) {
+            if (!detail) return null;
+
+            const select = targetId ? await this.getDataByKid(detail.id, targetId) : null;
+            if (targetId && !select) throw '新增节点数据错误';
+
+            this.transaction = await this.db.beginTransaction();
+            try {
+                if (select) await this._updateChildrenOrder(detail.id, select[this.setting.pid], select[this.setting.order] + 1, count);
+                const newDatas = [];
+                const maxId = await this._getMaxLid(detail.id);
+                for (let i = 1; i < count + 1; i++) {
+                    const newData = {};
+                    newData[this.setting.kid] = maxId + i;
+                    newData[this.setting.pid] = select ? select[this.setting.pid] : this.rootId;
+                    newData[this.setting.mid] = detail.id;
+                    newData[this.setting.level] = select ? select[this.setting.level] : 1;
+                    newData[this.setting.order] = select ? select[this.setting.order] + i : i;
+                    newData[this.setting.fullPath] = newData[this.setting.level] > 1
+                        ? select[this.setting.fullPath].replace('-' + select[this.setting.kid], '-' + newData[this.setting.kid])
+                        : newData[this.setting.kid] + '';
+                    newData[this.setting.isLeaf] = true;
+                    this._getDefaultData(newData, detail);
+                    newDatas.push(newData);
+                }
+                const insertResult = await this.transaction.insert(this.tableName, newDatas);
+                this._cacheMaxLid(detail.id, maxId + count);
+
+                if (insertResult.affectedRows !== count) throw '新增节点数据错误';
+                await this.transaction.commit();
+                this.transaction = null;
+            } catch (err) {
+                await this.transaction.rollback();
+                this.transaction = null;
+                throw err;
+            }
+
+            if (select) {
+                const createData = await this.getChildBetween(detail.id, select[this.setting.pid], select[this.setting.order], select[this.setting.order] + count + 1);
+                const updateData = await this.getNextsData(detail.id, select[this.setting.pid], select[this.setting.order] + count);
+                return {create: createData, update: updateData};
+            } else {
+                const createData = await this.getChildBetween(detail.id, -1, 0, count + 1);
+                return {create: createData};
+            }
+        }
+
+        async addStdNodeWithParent(detail, targetId, stdData) {
+            const findPreData = function(list, a) {
+                if (!list || list.length === 0) { return null; }
+                for (let i = 0, iLen = list.length; i < iLen; i++) {
+                    if (billsUtils.compareCode(list[i].b_code, a.b_code) > 0) {
+                        return i > 0 ? list[i - 1] : null;
+                    }
+                }
+                return list[list.length - 1];
+            };
+            let parent = await this.getDataByKid(detail.id, targetId);
+            if (targetId && !parent) throw '新增节点数据错误,请刷新页面重试';
+            let children = await this.getChildrenByParentId(detail.id, targetId);
+            const updateParent = children.length === 0;
+
+            const insertData = [];
+            const maxId = await this._getMaxLid(detail.id);
+            this.transaction = await this.db.beginTransaction();
+            try {
+                if (updateParent) {
+                    const updateData = { id: parent.id };
+                    updateData[this.setting.isLeaf] = false;
+                    this.clearParentingData(updateData);
+                    await this.transaction.update(this.tableName, updateData);
+                }
+                // 从最顶层节点依次查询是否存在,否则添加
+                for (let i = 0, len = stdData.length; i < len; i++) {
+                    const newData = { b_code: stdData[i].b_code, name: stdData[i].name, unit: stdData[i].unit };
+                    newData[this.setting.kid] = maxId + i + 1;
+                    newData[this.setting.pid] = parent ? parent[this.setting.kid] : this.rootId;
+                    newData[this.setting.level] = parent ? parent[this.setting.level] + 1 : 1;
+                    newData[this.setting.fullPath] = parent ? `${parent[this.setting.fullPath]}-${newData[this.setting.kid]}` : `${newData[this.setting.kid]}`;
+                    const pre = findPreData(children, newData);
+                    newData[this.setting.order] = pre ? pre[this.setting.order] + 1 : 1;
+                    if (!pre || children.indexOf(pre) < children.length - 1) {
+                        await this._updateChildrenOrder(detail.id, parent ? parent[this.setting.kid] : this.rootId, pre ? pre[this.setting.order] + 1 : 1);
+                    }
+                    newData[this.setting.isLeaf] = (i === len - 1);
+                    this._getDefaultData(newData, detail);
+                    insertData.push(newData);
+                    parent = newData;
+                    children = [];
+                }
+                await this.transaction.insert(this.tableName, insertData);
+                await this.transaction.commit();
+            } catch (err) {
+                await this.transaction.rollback();
+                throw err;
+            }
+            this._cacheMaxLid(detail.id, maxId + stdData.length);
+
+            // 查询应返回的结果
+            const createData = await this.getDataByFullPath(detail.id, insertData[0][this.setting.fullPath] + '%');
+            const updateData = await this.getNextsData(detail.id, targetId, insertData[0][this.setting.order]);
+            if (updateParent) {
+                updateData.push(await this.getDataByCondition({ id: updateParent.id }));
+            }
+            return { create: createData, update: updateData };
+        }
+
+        async updateCalc(detail, data) {
+            const helper = this.ctx.helper;
+            const decimal = this.decimal;
+            // 简单验证数据
+            if (!detail) throw '安全生产费不存在';
+            if (!data) throw '提交数据错误';
+            const datas = data instanceof Array ? data : [data];
+            const ids = datas.map(x => { return x.id; });
+            const orgData = await this.getAllDataByCondition({ where: { id: ids }});
+            const updateData = [];
+            for (const row of datas) {
+                const oData = orgData.find(x => { return x.id === row.id });
+                if (!oData || oData.detail_id !== detail.id || oData.tree_id !== row.tree_id) throw '提交数据错误';
+
+                let nData = { id: oData.id, tree_id: oData.tree_id, update_user_id: this.ctx.session.sessionUser.accountId };
+
+                // 计算相关
+                if (row.cur_qty !== undefined || row.unit_price !== undefined || row.cur_tp !== undefined) {
+                    nData.unit_price = row.unit_price !== undefined ? helper.round(row.unit_price, decimal.up) : oData.unit_price;
+                    nData.cur_qty = row.cur_qty !== undefined ? helper.round(row.cur_qty, decimal.qty) : oData.cur_qty;
+                    nData.cur_tp = row.cur_tp !== undefined ? helper.round(row.cur_tp, decimal.tp) : helper.mul(nData.unit_price, nData.cur_qty, decimal.tp);
+                    nData.end_qty = helper.add(nData.cur_qty, oData.pre_qty);
+                    nData.end_tp = helper.add(nData.cur_tp, oData.pre_tp);
+                }
+                console.log(nData);
+                for (const field of SafeBillsFields.textFields) {
+                    if (row[field] !== undefined) nData[field] = row[field];
+                }
+                updateData.push(nData);
+            }
+
+            await this.db.updateRows(this.tableName, updateData);
+            return { update: updateData };
+        }
+    }
+
+    return Ledger;
+};

+ 12 - 12
app/service/report.js

@@ -15,6 +15,7 @@ const rptCustomData = require('../lib/rptCustomData');
 const bindData = {
     materialGather: ['mem_material_gather_bills', 'mem_material_gather_xmj', 'mem_material_gather_gl'],
     gatherChange: ['mem_gather_change', 'mem_gather_change_bills'],
+    fjChange: ['mem_fj_change_progress', 'mem_fj_change_sum'],
 };
 const sourceTypeConst = require('../const/source_type');
 
@@ -445,10 +446,6 @@ module.exports = app => {
                         const jhHelper3 = new rptCustomData.jhHelper(this.ctx);
                         rst[filter] = await jhHelper3.gatherBills(memFieldKeys[filter], customDefine.gather_select, customSelect ? customSelect.gather_select : null);
                         break;
-                    case 'mem_fj_change_progress':
-                        const fjHelper = new rptCustomData.fjHelper(this.ctx);
-                        rst[filter] = await fjHelper.getChangeProgressData(params.tender_id, params.stage_id);
-                        break;
                     case 'mem_gather_stage_bills':
                         rst[filter] = await service.rptGatherMemory.getGatherStageBills(memFieldKeys[filter],
                             customDefine.gather_select, customSelect ? customSelect.gather_select : null);
@@ -472,22 +469,25 @@ module.exports = app => {
                 }
             }
             for (const s of specFilters) {
+                let Result;
                 switch (s) {
                     case 'materialGather':
-                        const mgResult = await materialSource.getMaterialGatherBills(params.tender_id, params.material_order);
-                        for (const d in mgResult) {
-                            rst[d] = mgResult[d];
-                        }
+                        Result = await materialSource.getMaterialGatherBills(params.tender_id, params.material_order);
                         break;
                     case 'gatherChange':
-                        const gcResult = await service.rptGatherMemory.getGatherChange(this.getFieldKeys(memFieldKeys, bindData.gatherChange),
+                        Result = await service.rptGatherMemory.getGatherChange(this.getFieldKeys(memFieldKeys, bindData.gatherChange),
                             customDefine.gather_select, customSelect ? customSelect.gather_select : null);
-                        for (const d in gcResult) {
-                            rst[d] = gcResult[d];
-                        }
+                        break;
+                    case 'fjChange':
+                        const fjHelper = new rptCustomData.fjHelper(this.ctx);
+                        Result = await fjHelper.getChangeProgressData(params.tender_id, params.stage_id);
+                        break;
                     default:
                         break;
                 }
+                for (const d in Result) {
+                    rst[d] = Result[d];
+                }
             }
             return rst;
         }

+ 30 - 0
app/service/std_gcl_list.js

@@ -0,0 +1,30 @@
+'use strict';
+
+/**
+ *
+ *
+ * @author Mai
+ * @date 2023/7/3
+ * @version
+ */
+
+module.exports = app => {
+    class SpecPull extends app.BaseService {
+        /**
+         * 构造函数
+         *
+         * @param {Object} ctx - egg全局变量
+         * @return {void}
+         */
+        constructor(ctx) {
+            super(ctx);
+            this.tableName = 'std_gcl_list';
+        }
+
+        async getSafeGcl () {
+            return await this.getAllDataByCondition({ where: { sub_type: 1 } });
+        }
+    }
+
+    return SpecPull;
+};

+ 18 - 2
app/view/payment/list.ejs

@@ -3,11 +3,11 @@
         <div class="title-main  d-flex justify-content-between">
             <div><a class="mr-2" href="/payment"><i class="fa fa-chevron-left"></i> 返回</a><%- ctx.paymentTender.name %></div>
             <div>
-                <% if (ctx.session.sessionUser.accountId === trInfo.uid && trInfo.is_del === 0 && rptMsg) { %>
+                <% if (ctx.session.sessionUser.accountId === trInfo.uid && trInfo.is_del === 0 && (rptMsg || trInfo.type === paymentConst.modes_value_object.safe)) { %>
                 <% if (trDetailList.length === 0 || (trDetailList.length !== 0 && trDetailList[0].status === auditConst.status.checked)) { %>
                 <a href="javascript:void(0);" id="show-add-btn" class="btn btn-sm btn-primary pull-right ml-2">新建审批</a>
                 <% } %>
-                <% if (trDetailList.length === 0 || trDetailList[0].status === auditConst.status.checked || trDetailList[0].status === auditConst.status.uncheck || trDetailList[0].status === auditConst.status.checkNo) { %>
+                <% if ((trDetailList.length === 0 || trDetailList[0].status === auditConst.status.checked || trDetailList[0].status === auditConst.status.uncheck || trDetailList[0].status === auditConst.status.checkNo) && trInfo.type !== paymentConst.modes_value_object.safe) { %>
                 <a href="#set-bdjs" data-toggle="modal" data-target="#set-bdjs" class="btn btn-sm btn-primary pull-right">绑定表单角色</a>
                 <span class="text-danger pull-right" id="first_msg" <% if (trInfo.is_first === 0) { %>style="display:none;" <% } %>>第一次新建审批,请点击右侧的“绑定表单角色” &nbsp; &nbsp; </span>
                 <% } %>
@@ -47,7 +47,11 @@
                                     <% for (const info of trDetailList) { %>
                                     <tr>
                                         <td class="text-center">第<%- info.order %>期</td>
+                                        <% if (info.type) { %>
+                                        <td class="text-center"><a href="/payment/<%- info.tender_id %>/safe/<%- info.id %>/bills"><%- info.code %></a></td>
+                                        <% } else { %>
                                         <td class="text-center"><a href="/payment/<%- info.tender_id %>/detail/<%- info.id %>"><%- info.code %></a></td>
+                                        <% } %>
                                         <td class="text-center"><%- info.user_name %></td>
                                         <td class="text-center"><%- info.s_time %></td>
                                         <td class="<%- auditConst.auditProgressClass[info.status] %>">
@@ -58,11 +62,23 @@
                                         </td>
                                         <td class="text-center">
                                             <% if (info.status === auditConst.status.uncheck && info.uid === ctx.session.sessionUser.accountId) { %>
+                                                <% if (info.type) { %>
+                                                <a href="<%- '/payment/' + ctx.paymentTender.id + '/safe/' + info.id + '/bills' %>" class="btn <%- auditConst.statusButtonClass[info.status] %> btn-sm"><%- auditConst.statusButton[info.status] %></a>
+                                                <% } else { %>
                                                 <a href="<%- '/payment/' + ctx.paymentTender.id + '/detail/' + info.id %>" class="btn <%- auditConst.statusButtonClass[info.status] %> btn-sm"><%- auditConst.statusButton[info.status] %></a>
+                                                <% } %>
                                             <% } else if (info.status === auditConst.status.checkNo && info.curAuditor && info.uid === ctx.session.sessionUser.accountId) { %>
+                                                <% if (info.type) { %>
+                                                <a href="<%- '/payment/' + ctx.paymentTender.id + '/safe/' + info.id + '/bills' %>" class="btn <%- auditConst.statusButtonClass[info.status] %> btn-sm"><%- auditConst.statusButton[info.status] %></a>
+                                                <% } else { %>
                                                 <a href="<%- '/payment/' + ctx.paymentTender.id + '/detail/' + info.id %>" class="btn <%- auditConst.statusButtonClass[info.status] %> btn-sm"><%- auditConst.statusButton[info.status] %></a>
+                                                <% } %>
                                             <% } else if (info.status === auditConst.status.checking && info.curAuditor && info.curAuditor.aid === ctx.session.sessionUser.accountId) { %>
+                                                <% if (info.type) { %>
+                                                <a href="<%- '/payment/' + ctx.paymentTender.id + '/safe/' + info.id + '/bills' %>" class="btn <%- auditConst.statusButtonClass[info.status] %> btn-sm"><%- auditConst.statusButton[info.status] %></a>
+                                                <% } else { %>
                                                 <a href="<%- '/payment/' + ctx.paymentTender.id + '/detail/' + info.id %>" class="btn <%- auditConst.statusButtonClass[info.status] %> btn-sm"><%- auditConst.statusButton[info.status] %></a>
+                                                <% } %>
                                             <% } else { %>
                                                 <span class="<%- auditConst.auditProgressClass[info.status] %>"><%- auditConst.auditProgress[info.status] %></span>
                                             <% } %>

+ 50 - 0
app/view/payment/list_modal.ejs

@@ -112,6 +112,56 @@
     </div>
 </div>
 <% } %>
+<% if (trInfo.type === paymentConst.modes_value_object.safe) {%>
+<!-- 弹窗新建目录 -->
+<div class="modal fade" id="add-tips" data-backdrop="static">
+    <div class="modal-dialog" role="document">
+        <div class="modal-content">
+            <div class="modal-header">
+                <h5 class="modal-title">新建审批</h5>
+            </div>
+            <div class="modal-body">
+                <h5>第<%= trDetailList && trDetailList[0] ? trDetailList[0].order : 1 %>期存在人员报表未签署,新增期后将无法进行签署,请确认是否新增?</h5>
+            </div>
+            <div class="modal-footer">
+                <button type="button" class="btn btn-sm btn-secondary" data-dismiss="modal">取消</button>
+                <a class="btn btn-sm btn-primary" data-dismiss="modal" href="#add-catalogue" data-toggle="modal" data-target="#add-catalogue">确定</a>
+            </div>
+        </div>
+    </div>
+</div>
+<div class="modal fade" id="add-catalogue" data-backdrop="static">
+    <div class="modal-dialog" role="document">
+        <div class="modal-content">
+            <div class="modal-header">
+                <h5 class="modal-title">新建审批</h5>
+            </div>
+            <div class="modal-body">
+                <form>
+                    <div class="form-group">
+                        <!--<a href="#" class="float-right"><i class="fa fa-cog"></i></a>-->
+                        <label for="add-detail-code">期数</label>
+                        <input type="text" class="form-control form-control-sm"  value="第<%- (!trDetailList || trDetailList.length === 0 ? '1' : (trDetailList && trDetailList.length >= 1 ? trDetailList[0].order + 1 : '')) %>期" readonly>
+                    </div>
+                    <div class="form-group">
+                        <!--<a href="#" class="float-right"><i class="fa fa-cog"></i></a>-->
+                        <label for="add-detail-code">编号</label>
+                        <input type="text" class="form-control form-control-sm" name="add_code" id="add-detail-code" placeholder="请输入编号" value="">
+                    </div>
+                    <div class="form-group">
+                        <label for="add-detail-time">日期</label>
+                        <input type="date" class="form-control form-control-sm" name="add_time" id="add-detail-time" placeholder="" value="<%- ctx.helper.dateTran(new Date(), 'YYYY-MM-DD')%>">
+                    </div>
+                </form>
+            </div>
+            <div class="modal-footer">
+                <button type="button" class="btn btn-sm btn-secondary" data-dismiss="modal">取消</button>
+                <button type="button" class="btn btn-sm btn-primary" id="add-detail-btn">确定添加</button>
+            </div>
+        </div>
+    </div>
+</div>
+<% } %>
 <% if (trDetailList && trDetailList.length >= 1) { %>
     <!--删除期-->
     <div class="modal fade" id="del-qi" data-backdrop="static">

+ 30 - 0
app/view/payment_safe/compare.ejs

@@ -0,0 +1,30 @@
+<% include ./sub_menu.ejs %>
+<div class="panel-content">
+    <div class="panel-title">
+        <div class="title-main d-flex">
+            <% include ./sub_mini_menu.ejs %>
+            <div>
+                <div class="d-inline-block">
+                    <div class="dropdown">
+                        <button class="btn btn-sm btn-light dropdown-toggle text-primary" type="button" id="dropdownMenuButton" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
+                            <i class="fa fa-list-ol"></i> 显示层级
+                        </button>
+                        <div class="dropdown-menu" aria-labelledby="dropdownMenuButton">
+                            <a class="dropdown-item" name="showLevel" tag="1" href="javascript: void(0);">第一层</a>
+                            <a class="dropdown-item" name="showLevel" tag="2" href="javascript: void(0);">第二层</a>
+                            <a class="dropdown-item" name="showLevel" tag="3" href="javascript: void(0);">第三层</a>
+                            <a class="dropdown-item" name="showLevel" tag="4" href="javascript: void(0);">第四层</a>
+                            <a class="dropdown-item" name="showLevel" tag="last" href="javascript: void(0);">最底层</a>
+                        </div>
+                    </div>
+                </div>
+            </div>
+        </div>
+    </div>
+    <div class="content-wrap">
+        <div class="c-header p-0"></div>
+        <div class="c-body" style="width: 100%">
+            <div id="bills-spread" class="sjs-height-1"></div>
+        </div>
+    </div>
+</div>

+ 179 - 0
app/view/payment_safe/index.ejs

@@ -0,0 +1,179 @@
+<% include ./sub_menu.ejs %>
+<div class="panel-content">
+    <div class="panel-title">
+        <div class="title-main d-flex">
+            <% include ./sub_mini_menu.ejs %>
+            <div>
+                <div class="d-inline-block">
+                    <div class="dropdown">
+                        <button class="btn btn-sm btn-light dropdown-toggle text-primary" type="button" id="dropdownMenuButton" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
+                            <i class="fa fa-list-ol"></i> 显示层级
+                        </button>
+                        <div class="dropdown-menu" aria-labelledby="dropdownMenuButton">
+                            <a class="dropdown-item" name="showLevel" tag="1" href="javascript: void(0);">第一层</a>
+                            <a class="dropdown-item" name="showLevel" tag="2" href="javascript: void(0);">第二层</a>
+                            <a class="dropdown-item" name="showLevel" tag="3" href="javascript: void(0);">第三层</a>
+                            <a class="dropdown-item" name="showLevel" tag="4" href="javascript: void(0);">第四层</a>
+                            <a class="dropdown-item" name="showLevel" tag="last" href="javascript: void(0);">最底层</a>
+                        </div>
+                    </div>
+                </div>
+                <div class="d-inline-block">
+                    <a href="javascript: void(0);" name="base-opr" type="add" class="btn btn-sm btn-light text-primary" 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 btn-light text-primary" 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 btn-light text-primary" 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 btn-light text-primary" 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 btn-light text-primary" 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 btn-light text-primary" data-toggle="tooltip" data-placement="bottom" title="" data-original-title="上移"><i class="fa fa-arrow-up" aria-hidden="true"></i></a>
+                </div>
+            </div>
+        </div>
+    </div>
+    <div class="content-wrap row pr-46">
+        <div class="c-header p-0 col-12">
+        </div>
+        <!--核心内容(两栏)-->
+        <div class="row w-100 sub-content">
+            <!--左栏-->
+            <div class="c-body" id="left-view" style="width: 100%">
+                <div id="bills-spread" class="sjs-height-1"></div>
+            </div>
+            <div class="c-body" id="right-view" style="display: none; width: 33%;">
+                <div class="resize-x" id="right-spr" r-Type="width" div1="#left-view" div2="#right-view" title="调整大小" a-type="percent"><!--调整左右高度条--></div>
+                <div class="tab-content">
+                    <div id="std-gcl" class="tab-pane tab-select-show">
+                    </div>
+                    <div id="fujian" class="tab-pane tab-select-show">
+                        <div class="sjs-bar">
+                            <ul class="nav nav-tabs">
+                                <li class="nav-item">
+                                    <a class="nav-link active" data-toggle="tab" href="#dqjiedian" role="tab" fujian-content="dqjiedian">当前节点</a>
+                                </li>
+                                <li class="nav-item">
+                                    <a class="nav-link" data-toggle="tab" href="#syfujian" role="tab" fujian-content="syfujian">所有附件</a>
+                                </li>
+                                <li class="nav-item ml-auto pt-1">
+                                    <button  id="bach-download" class="btn btn-sm btn-primary" type="curr">批量下载</button>
+                                    <!--所有附件 翻页-->
+                                    <span id="showPage" style="display: none"><a href="javascript:void(0);" class="page-select ml-3" content="pre"><i class="fa fa-chevron-left"></i></a> <span id="currentPage">1</span>/<span id="totalPage">10</span> <a href="javascript:void(0);" class="page-select mr-3" content="next"><i class="fa fa-chevron-right"></i></a></span>
+                                    <a href="#upload" data-toggle="modal" data-target="#upload"  class="btn btn-sm btn-outline-primary ml-3">上传</a>
+                                </li>
+                            </ul>
+                        </div>
+                        <a href="javascript: void(0);" id="zipDown" download style="display: none;"></a>
+                        <div class="tab-content">
+                            <div class="tab-pane active" id="dqjiedian">
+                                <div class="sjs-height-3" style="overflow:auto; overflow-x:hidden;">
+                                    <table class="table table-sm table-bordered table-hover" style="word-break:break-all; table-layout: fixed">
+                                        <tr><th width="25"><input type="checkbox" class="check-all-file"><th>文件名</th><th width="80">上传</th></tr>
+                                        <tbody id="nodelist-table" class="list-table">
+                                        </tbody>
+                                    </table>
+                                </div>
+                            </div>
+                            <div class="tab-pane" id="syfujian">
+                                <div class="sjs-height-3" style="overflow:auto; overflow-x:hidden;">
+                                    <table class="table table-sm table-bordered table-hover" style="word-break:break-all; table-layout: fixed">
+                                        <tr><th width="25"><input type="checkbox" class="check-all-file"></th><th>文件名</th><th width="80">上传</th></tr>
+                                        <tbody id="alllist-table" class="list-table">
+                                        </tbody>
+                                    </table>
+                                </div>
+                            </div>
+                            <div class="sjs-bottom">
+                                <div class="resize-y" id="file-spr" r-Type="height" div1=".sjs-height-3" div2=".sjs-bottom" title="调整大小"><!--调整上下高度条--></div>
+                                <br>
+                                <div class="tab-content" id="showAttachment" style="display: none" file-id="">
+                                    <div class="sjs-bottom-2">
+                                        <a href="javascript:void(0);" target="_blank" style="display: none" id="load-file"></a>
+                                        <div class="d-flex justify-content-end mb-1" id="btn-att">
+                                            <a href="javascript:void(0);" content="location" class="btn btn-sm btn-outline-primary" style="margin-right: 5px">定位</a>
+                                            <a href="javascript:void(0);" content="view" class="btn btn-sm btn-outline-primary" style="margin-right: 5px">查看</a>
+                                            <!--默认 有删除权限-->
+                                            <a href="javascript:void(0);" content="del" class="btn btn-sm text-danger" style="display: none; margin-right: 5px">删除</a>
+                                            <!--默认 有编辑权限-->
+                                            <a href="javascript:void(0);" content="edit" class="btn btn-sm btn-outline-primary" style="display: none; margin-right: 5px">编辑</a>
+                                            <!--编辑模式-->
+                                            <a href="javascript:void(0);" content="save" class="btn btn-sm btn-outline-success mr-1" style="display: none; margin-right: 5px">保存</a>
+                                            <a href="javascript:void(0);" content="cancel" class="btn btn-sm btn-outline-secondary" style="display: none; margin-right: 5px">取消</a>
+                                        </div>
+                                        <!--显示信息-->
+                                        <table class="table table-sm table-bordered" id="show-att" style="word-break:break-all; table-layout: fixed">
+                                            <tbody>
+                                            <tr><th>文件名</th><td colspan="3">asdasd.jpg</td></tr>
+                                            <tr><th>所在节点</th><td colspan="3" id="show-att-node">1 第一部分 建筑安装工程非</td></tr>
+                                            <!-- <tr><td colspan="4"><a href="javascript:void(0);" target="_blank"><span>下载附件</span></a></td></tr> -->
+                                            <tr><th>上传者</th><td>张三</td><th>上传时间</th><td>2018-10-20</td></tr>
+                                            <tr><th>备注</th><td colspan="3"></td></tr>
+                                            </tbody>
+                                        </table>
+                                        <div id="edit-att" style="display: none">
+                                            <!--编辑模式-->
+                                            <div class="form-group">
+                                                <div class="input-group input-group-sm">
+                                                    <div class="input-group-prepend">
+                                                        <span class="input-group-text">文件名</span>
+                                                    </div>
+                                                    <input type="text" class="form-control form-control-sm" value="asdasd">
+                                                    <div class="input-group-append">
+                                                        <span class="input-group-text">.jpg</span>
+                                                    </div>
+                                                </div>
+                                            </div>
+                                            <div class="form-group">
+                                                <div class="input-group input-group-sm">
+                                                    <div class="input-group-prepend">
+                                                        <span class="input-group-text">所在节点</span>
+                                                    </div>
+                                                    <input type="text" class="form-control form-control-sm" value="1 第一部分 建筑安装工程非" readonly="">
+                                                </div>
+                                            </div>
+                                            <div class="form-group">
+                                                <div class="input-group input-group-sm">
+                                                    <div class="input-group-prepend">
+                                                        <span class="input-group-text">上传时间</span>
+                                                    </div>
+                                                    <input type="text" class="form-control form-control-sm" value="2018-10-20" readonly="">
+                                                </div>
+                                            </div>
+                                            <div class="form-group">
+                                                <div class="input-group input-group-sm">
+                                                    <div class="input-group-prepend">
+                                                        <span class="input-group-text">备注</span>
+                                                    </div>
+                                                    <input type="text" class="form-control form-control-sm" value="">
+                                                </div>
+                                            </div>
+                                            <div class="form-group">
+                                                <label>替换文件</label>
+                                                <div class="custom-file">
+                                                    <input type="file" class="custom-file-input" id="change-att-btn">
+                                                    <label class="custom-file-label" data-browse="浏览" for="customFile">选择文件</label>
+                                                </div>
+                                            </div>
+                                        </div>
+                                    </div>
+                                </div>
+                            </div>
+                        </div>
+                    </div>
+                </div>
+            </div>
+        </div>
+        <!--右侧菜单-->
+        <div class="side-menu">
+            <ul class="nav flex-column right-nav" id="side-menu">
+                <li class="nav-item">
+                    <a class="nav-link" content="#std-gcl" href="javascript: void(0);">工程量清单</a>
+                </li>
+                <li class="nav-item">
+                    <a class="nav-link" content="#fujian" href="javascript: void(0);">附件</a>
+                </li>
+            </ul>
+        </div>
+    </div>
+</div>
+<script>
+    const readOnly = false;
+    const stdBills = JSON.parse(unescape('<%- escape(JSON.stringify(stdBills)) %>'));
+</script>

+ 1 - 0
app/view/payment_safe/modal.ejs

@@ -0,0 +1 @@
+<% include ../shares/delete_hint_modal.ejs %>

+ 14 - 0
app/view/payment_safe/sub_menu.ejs

@@ -0,0 +1,14 @@
+<div class="panel-sidebar" id="sub-menu">
+    <div class="sidebar-title" data-toggle="tooltip" data-placement="right" data-original-title="<%- ctx.paymentTender.name %>">
+        <%- (ctx.paymentTender.name.length > 11 ? ctx.paymentTender.name.substring(0,11) + '...' : ctx.paymentTender.name) %>
+    </div>
+    <div class="scrollbar-auto">
+        <% include ./sub_menu_list.ejs %>
+        <div class="side-fold"><a href="javascript: void(0)" data-toggle="tooltip" data-placement="top" data-original-title="折叠侧栏" id="to-mini-menu"><i class="fa fa-upload fa-rotate-270"></i></a></div>
+    </div>
+    <script>
+        new Vue({
+            el: '.scrollbar-auto',
+        });
+    </script>
+</div>

+ 4 - 0
app/view/payment_safe/sub_menu_list.ejs

@@ -0,0 +1,4 @@
+<nav-menu title="返回" url="/payment/<%- ctx.paymentTender.id  %>/list" tclass="text-primary" ml="1" icon="fa-chevron-left"></nav-menu>
+<nav-menu title="安全生产费" url="/payment/<%- ctx.paymentTender.id %>/safe/<%= ctx.detail.id %>/bills" ml="3" active="<%= ctx.url.indexOf('bills') %>"></nav-menu>
+<nav-menu title="审核比较" url="/payment/<%- ctx.paymentTender.id %>/safe/<%= ctx.detail.id %>/compare" ml="3" active="<%= ctx.url.indexOf('compare') %>"></nav-menu>
+<nav-menu title="输出报表" url="/payment/<%- ctx.paymentTender.id %>/safe/<%= ctx.detail.id %>/report" ml="3" active="<%= ctx.url.indexOf('report') %>"></nav-menu>

+ 16 - 0
app/view/payment_safe/sub_mini_menu.ejs

@@ -0,0 +1,16 @@
+<!--折起的菜单-->
+<div class="min-side" id="sub-mini-menu" style="display: none;">
+    <div id="sub-mini-hint" class="side-switch" data-container="body" data-toggle="popover" data-placement="bottom" data-content="这里打开收起的菜单栏"></div>
+    <div class="side-switch">
+        <i class="fa fa-bars"></i>
+    </div>
+    <div class="side-menu" id="mini-menu-list" style="display: none">
+        <% include ./sub_menu_list.ejs %>
+        <div class="side-fold"><a href="javascript: void(0);" data-toggle="tooltip" data-placement="top" data-original-title="展开侧栏" id="to-menu"><i class="fa fa-upload fa-rotate-90"></i></a></div>
+    </div>
+</div>
+<script>
+    new Vue({
+        el: '.side-menu',
+    });
+</script>

+ 0 - 1
app/view/stage/compare.ejs

@@ -43,7 +43,6 @@
         </div>
     </div>
     <div class="content-wrap">
-        <div class="c-header p-0"></div>
         <div class="c-body">
             <div class="sjs-height-1" id="ledger-spread">
             </div>

+ 45 - 0
config/web.js

@@ -1175,6 +1175,51 @@ const JsFiles = {
                 ],
                 mergeFile: 'payment_detail',
             },
+            safe: {
+                files: [
+                    '/public/js/js-xlsx/xlsx.full.min.js',
+                    '/public/js/js-xlsx/xlsx.utils.js',
+                    '/public/js/spreadjs/sheets/v11/gc.spread.sheets.all.11.2.2.min.js',
+                    '/public/js/spreadjs/sheets/v11/interop/gc.spread.excelio.11.2.2.min.js',
+                    '/public/js/decimal.min.js',
+                    '/public/js/math.min.js',
+                    '/public/js/axios/axios.min.js', '/public/js/js-xlsx/jszip.min.js',
+                    '/public/js/file-saver/FileSaver.js',
+                    '/public/js/component/menu.js',
+                ],
+                mergeFiles: [
+                    '/public/js/sub_menu.js',
+                    '/public/js/div_resizer.js',
+                    '/public/js/spreadjs_rela/spreadjs_zh.js',
+                    '/public/js/shares/sjs_setting.js',
+                    '/public/js/shares/cs_tools.js',
+                    '/public/js/zh_calc.js',
+                    '/public/js/zip_oss.js',
+                    '/public/js/path_tree.js',
+                    '/public/js/std_lib.js',
+                    '/public/js/payment_safe.js',
+                ],
+                mergeFile: 'payment_safe',
+            },
+            compare: {
+                files: [
+                    '/public/js/spreadjs/sheets/v11/gc.spread.sheets.all.11.2.2.min.js',
+                    '/public/js/spreadjs/sheets/v11/interop/gc.spread.excelio.11.2.2.min.js',
+                    '/public/js/decimal.min.js',
+                    '/public/js/math.min.js',
+                    '/public/js/component/menu.js',
+                ],
+                mergeFiles: [
+                    '/public/js/sub_menu.js',
+                    '/public/js/spreadjs_rela/spreadjs_zh.js',
+                    '/public/js/shares/sjs_setting.js',
+                    '/public/js/shares/cs_tools.js',
+                    '/public/js/zh_calc.js',
+                    '/public/js/path_tree.js',
+                    '/public/js/payment_compare.js',
+                ],
+                mergeFile: 'payment_compare',
+            }
         },
     },
 };

+ 103 - 0
sql/update20230627.sql

@@ -0,0 +1,103 @@
+--临时更新
+--task 4529 使用ex_memo字段做市政-项目特征,三个扩展备注放宽至1000
+--广东-谢金鑫
+
+ALTER TABLE `zh_ledger_0`
+MODIFY COLUMN `ex_memo1`  varchar(1000) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '备注1' AFTER `check_calc`,
+MODIFY COLUMN `ex_memo2`  varchar(1000) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '备注2' AFTER `ex_memo1`,
+MODIFY COLUMN `ex_memo3`  varchar(1000) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '备注3' AFTER `ex_memo2`;
+
+ALTER TABLE `zh_ledger_1`
+MODIFY COLUMN `ex_memo1`  varchar(1000) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '备注1' AFTER `check_calc`,
+MODIFY COLUMN `ex_memo2`  varchar(1000) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '备注2' AFTER `ex_memo1`,
+MODIFY COLUMN `ex_memo3`  varchar(1000) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '备注3' AFTER `ex_memo2`;
+
+ALTER TABLE `zh_ledger_2`
+MODIFY COLUMN `ex_memo1`  varchar(1000) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '备注1' AFTER `check_calc`,
+MODIFY COLUMN `ex_memo2`  varchar(1000) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '备注2' AFTER `ex_memo1`,
+MODIFY COLUMN `ex_memo3`  varchar(1000) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '备注3' AFTER `ex_memo2`;
+
+ALTER TABLE `zh_ledger_3`
+MODIFY COLUMN `ex_memo1`  varchar(1000) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '备注1' AFTER `check_calc`,
+MODIFY COLUMN `ex_memo2`  varchar(1000) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '备注2' AFTER `ex_memo1`,
+MODIFY COLUMN `ex_memo3`  varchar(1000) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '备注3' AFTER `ex_memo2`;
+
+ALTER TABLE `zh_ledger_4`
+MODIFY COLUMN `ex_memo1`  varchar(1000) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '备注1' AFTER `check_calc`,
+MODIFY COLUMN `ex_memo2`  varchar(1000) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '备注2' AFTER `ex_memo1`,
+MODIFY COLUMN `ex_memo3`  varchar(1000) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '备注3' AFTER `ex_memo2`;
+
+ALTER TABLE `zh_ledger_5`
+MODIFY COLUMN `ex_memo1`  varchar(1000) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '备注1' AFTER `check_calc`,
+MODIFY COLUMN `ex_memo2`  varchar(1000) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '备注2' AFTER `ex_memo1`,
+MODIFY COLUMN `ex_memo3`  varchar(1000) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '备注3' AFTER `ex_memo2`;
+
+ALTER TABLE `zh_ledger_6`
+MODIFY COLUMN `ex_memo1`  varchar(1000) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '备注1' AFTER `check_calc`,
+MODIFY COLUMN `ex_memo2`  varchar(1000) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '备注2' AFTER `ex_memo1`,
+MODIFY COLUMN `ex_memo3`  varchar(1000) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '备注3' AFTER `ex_memo2`;
+
+ALTER TABLE `zh_ledger_7`
+MODIFY COLUMN `ex_memo1`  varchar(1000) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '备注1' AFTER `check_calc`,
+MODIFY COLUMN `ex_memo2`  varchar(1000) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '备注2' AFTER `ex_memo1`,
+MODIFY COLUMN `ex_memo3`  varchar(1000) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '备注3' AFTER `ex_memo2`;
+
+ALTER TABLE `zh_ledger_8`
+MODIFY COLUMN `ex_memo1`  varchar(1000) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '备注1' AFTER `check_calc`,
+MODIFY COLUMN `ex_memo2`  varchar(1000) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '备注2' AFTER `ex_memo1`,
+MODIFY COLUMN `ex_memo3`  varchar(1000) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '备注3' AFTER `ex_memo2`;
+
+ALTER TABLE `zh_ledger_9`
+MODIFY COLUMN `ex_memo1`  varchar(1000) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '备注1' AFTER `check_calc`,
+MODIFY COLUMN `ex_memo2`  varchar(1000) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '备注2' AFTER `ex_memo1`,
+MODIFY COLUMN `ex_memo3`  varchar(1000) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '备注3' AFTER `ex_memo2`;
+
+ALTER TABLE `zh_revise_bills_0`
+MODIFY COLUMN `ex_memo1`  varchar(1000) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '备注1' AFTER `check_calc`,
+MODIFY COLUMN `ex_memo2`  varchar(1000) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '备注2' AFTER `ex_memo1`,
+MODIFY COLUMN `ex_memo3`  varchar(1000) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '备注3' AFTER `ex_memo2`;
+
+ALTER TABLE `zh_revise_bills_1`
+MODIFY COLUMN `ex_memo1`  varchar(1000) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '备注1' AFTER `check_calc`,
+MODIFY COLUMN `ex_memo2`  varchar(1000) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '备注2' AFTER `ex_memo1`,
+MODIFY COLUMN `ex_memo3`  varchar(1000) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '备注3' AFTER `ex_memo2`;
+
+ALTER TABLE `zh_revise_bills_2`
+MODIFY COLUMN `ex_memo1`  varchar(1000) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '备注1' AFTER `check_calc`,
+MODIFY COLUMN `ex_memo2`  varchar(1000) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '备注2' AFTER `ex_memo1`,
+MODIFY COLUMN `ex_memo3`  varchar(1000) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '备注3' AFTER `ex_memo2`;
+
+ALTER TABLE `zh_revise_bills_3`
+MODIFY COLUMN `ex_memo1`  varchar(1000) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '备注1' AFTER `check_calc`,
+MODIFY COLUMN `ex_memo2`  varchar(1000) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '备注2' AFTER `ex_memo1`,
+MODIFY COLUMN `ex_memo3`  varchar(1000) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '备注3' AFTER `ex_memo2`;
+
+ALTER TABLE `zh_revise_bills_4`
+MODIFY COLUMN `ex_memo1`  varchar(1000) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '备注1' AFTER `check_calc`,
+MODIFY COLUMN `ex_memo2`  varchar(1000) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '备注2' AFTER `ex_memo1`,
+MODIFY COLUMN `ex_memo3`  varchar(1000) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '备注3' AFTER `ex_memo2`;
+
+ALTER TABLE `zh_revise_bills_5`
+MODIFY COLUMN `ex_memo1`  varchar(1000) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '备注1' AFTER `check_calc`,
+MODIFY COLUMN `ex_memo2`  varchar(1000) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '备注2' AFTER `ex_memo1`,
+MODIFY COLUMN `ex_memo3`  varchar(1000) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '备注3' AFTER `ex_memo2`;
+
+ALTER TABLE `zh_revise_bills_6`
+MODIFY COLUMN `ex_memo1`  varchar(1000) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '备注1' AFTER `check_calc`,
+MODIFY COLUMN `ex_memo2`  varchar(1000) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '备注2' AFTER `ex_memo1`,
+MODIFY COLUMN `ex_memo3`  varchar(1000) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '备注3' AFTER `ex_memo2`;
+
+ALTER TABLE `zh_revise_bills_7`
+MODIFY COLUMN `ex_memo1`  varchar(1000) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '备注1' AFTER `check_calc`,
+MODIFY COLUMN `ex_memo2`  varchar(1000) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '备注2' AFTER `ex_memo1`,
+MODIFY COLUMN `ex_memo3`  varchar(1000) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '备注3' AFTER `ex_memo2`;
+
+ALTER TABLE `zh_revise_bills_8`
+MODIFY COLUMN `ex_memo1`  varchar(1000) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '备注1' AFTER `check_calc`,
+MODIFY COLUMN `ex_memo2`  varchar(1000) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '备注2' AFTER `ex_memo1`,
+MODIFY COLUMN `ex_memo3`  varchar(1000) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '备注3' AFTER `ex_memo2`;
+
+ALTER TABLE `zh_revise_bills_9`
+MODIFY COLUMN `ex_memo1`  varchar(1000) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '备注1' AFTER `check_calc`,
+MODIFY COLUMN `ex_memo2`  varchar(1000) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '备注2' AFTER `ex_memo1`,
+MODIFY COLUMN `ex_memo3`  varchar(1000) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '备注3' AFTER `ex_memo2`;