Browse Source

结算审批相关

MaiXinRong 1 year ago
parent
commit
f4393acd12

+ 1 - 0
app/const/audit.js

@@ -945,6 +945,7 @@ const pushType = {
     changeProject: 7,
     changeApply: 8,
     changePlan: 9,
+    settle: 10,
 };
 
 module.exports = {

+ 2 - 0
app/const/shenpi.js

@@ -13,6 +13,7 @@ const sp_type = {
     ledger: 2,
     revise: 3,
     stage: 4,
+    settle: 7,
     change: 5,
     material: 6,
 };
@@ -29,6 +30,7 @@ const sp_lc = [
     { code: 'ledger', type: sp_type.ledger, name: '台账审批' },
     { code: 'revise', type: sp_type.revise, name: '台账修订' },
     { code: 'stage', type: sp_type.stage, name: '计量期审批' },
+    { code: 'settle', type: sp_type.settle, name: '结算期审批' },
     { code: 'change', type: sp_type.change, name: '工程变更审批' },
     { code: 'material', type: sp_type.material, name: '材料调差审批' },
 ];

+ 2 - 0
app/const/sms_alitemplate.js

@@ -25,6 +25,8 @@ const smsTemplate = {
     revise_result: 'SMS_193140537', // 项目:${project},标段:${number},台账修订审批${status},请登录系统处理。
     revise_result2: 'SMS_192985611', // 项目:${project},标段:${number},台账修订审批${status}。
     revise_report: 'SMS_192985614', // 项目:${project},标段:${number},台账修订已上报。
+    // todo 结算期短信通知
+    // settle_check: 'SMS_193244645', // 项目:${project},标段:${number},第${qi}期,需要您审批。在线审批https://scn.ink/${code}
 };
 const smsStatus = {
     back: '退回',

+ 11 - 0
app/const/sms_type.js

@@ -14,6 +14,7 @@ const smsConst = {
     XD: 'XD',
     TC: 'TC',
     YFK: 'YFK',
+    JS: 'JS',
 };
 const judgeConst = {
     approval: 1,
@@ -80,6 +81,16 @@ const smsType = {
             { title: '审批结果', value: 2 },
         ],
     },
+    JS: {
+        name: '结算审批',
+        path: '',
+        wechat: true,
+        sms: true,
+        children: [
+            { title: '需要我审批', value: 1 },
+            { title: '审批结果', value: 2 },
+        ],
+    },
 };
 
 module.exports = {

+ 2 - 0
app/const/spec_3f.js

@@ -12,6 +12,8 @@ const pushTiming = [
     { value:'ledger.checked', name: '台账-审批通过' },
     { value:'stage.checked', name: '期-审批通过' },
     { value:'stage.flow', name: '期-上报/审批' },
+    { value:'settle.checked', name: '结算期-审批通过' },
+    { value:'settle.flow', name: '结算期-上报/审批' },
     { value:'revise.checked', name: '台账修订-审批通过' },
     { value:'change.checked', name: '变更令-审批通过' },
     { value:'report.file', name: '报表-推送归档' },

+ 1 - 0
app/const/tender_info.js

@@ -178,6 +178,7 @@ const defaultInfo = {
         ledger: 1,
         revise: 1,
         stage: 1,
+        settle: 1,
         change: 1,
         material: 1,
     },

+ 3 - 1
app/const/wechat_template.js

@@ -14,6 +14,7 @@ const template = {
     revise: 3,
     material: 4,
     advance: 5,
+    settle: 6,
 };
 const templateId = {
     stage: '5vU3WmR90yDajbs4LWIWH4OQhunYlS1HXTiesIGxrsk',
@@ -22,6 +23,7 @@ const templateId = {
     revise: '9Ul3KFxvYQGfT6wbGGjqqnGR0zHtx0BHKs9sXj4Ii44',
     material: 'Y9ov80rev3LHcoM2hOQE6jrK_5xZsqE-PZ0Z6HS9VGA',
     advance: 'rgZHkyiLzrqaSGnQ2nSCCrOdUz2RJJZ_JA34L_MnQik',
+    settle: '5vU3WmR90yDajbs4LWIWH4OQhunYlS1HXTiesIGxrsk', // todo 暂使用计量期模板
 };
 const status = {
     check: '待审批',
@@ -42,7 +44,7 @@ const changeType = {
     project: '变更立项',
     apply: '变更申请',
     plan: '变更方案',
-}
+};
 
 module.exports = {
     template,

+ 147 - 20
app/controller/settle_controller.js

@@ -56,12 +56,12 @@ module.exports = app => {
                     if (s.status === auditConst.settle.status.uncheck) {
                         s.curAuditors = [];
                     } else if (s.status === auditConst.settle.status.checkNo) {
-                        s.curAuditors = await ctx.service.settleAudit.getAuditorsByStatus(sid, s.status, s.times - 1);
+                        s.curAuditors = await ctx.service.settleAudit.getAuditorsByStatus(sid, s.status, s.audit_times - 1);
                     } else {
-                        s.curAuditors = await ctx.service.settleAudit.getAuditorsByStatus(s.id, s.status, s.times);
+                        s.curAuditors = await ctx.service.settleAudit.getAuditorsByStatus(s.id, s.status, s.audit_times);
                     }
                     if (s.status === auditConst.settle.status.checkNoPre) {
-                        s.curAuditorsPre = await ctx.service.stageAudit.getAuditorsByStatus(s.id, auditConst.settle.status.checking, s.times);
+                        s.curAuditorsPre = await ctx.service.settleAudit.getAuditorsByStatus(s.id, auditConst.settle.status.checking, s.audit_times);
                     }
                 }
                 renderData.checkedStageCount = await ctx.service.stage.count({ tid: ctx.tender.id, status: auditConst.stage.status.checked });
@@ -169,11 +169,6 @@ module.exports = app => {
             const pa = await ctx.service.projectAccount.getDataById(ctx.session.sessionUser.accountId);
             data.authMobile = pa.auth_mobile;
 
-            const loadAccount = ctx.session.sessionUser.is_admin
-                ? true
-                : ctx.session.sessionUser.accountId === ctx.settle.user_id && ([auditConst.settle.status.uncheck, auditConst.settle.status.checkNo].indexOf(ctx.settle.audit_status) >= 0);
-            if (!loadAccount) return data;
-
             // 获取所有项目参与者
             const accountList = await ctx.service.projectAccount.getAllDataByCondition({
                 where: { project_id: ctx.session.sessionProject.id, enable: 1 },
@@ -203,7 +198,6 @@ module.exports = app => {
                 renderData.whiteList = this.ctx.app.config.multipart.whitelist;
                 await this.layout('settle/select.ejs', renderData, 'settle/select_modal.ejs');
             } catch(err) {
-                console.log(err);
                 ctx.log(err);
                 ctx.redirect('/tender/' + ctx.tender.id + '/settle');
             }
@@ -218,7 +212,6 @@ module.exports = app => {
                 renderData.whiteList = this.ctx.app.config.multipart.whitelist;
                 await this.layout('settle/index.ejs', renderData, 'settle/modal.ejs');
             } catch(err) {
-                console.log(err);
                 ctx.log(err);
                 ctx.redirect('/tender/' + ctx.tender.id + '/settle');
             }
@@ -239,19 +232,29 @@ module.exports = app => {
                 'id', 'tender_id', 'ledger_id', 'ledger_pid', 'level', 'order', 'full_path', 'is_leaf',
                 'code', 'b_code', 'name', 'unit', 'unit_price',
                 'quantity', 'total_price', 'memo', 'drawing_code', 'node_type'];
+            this.ledgerExtraColumn = [];
+            if (this.ctx.session.sessionProject.gxby) this.ledgerExtraColumn.push('gxby_status', 'gxby_url', 'gxby_limit');
+            if (this.ctx.session.sessionProject.dagl) this.ledgerExtraColumn.push('dagl_status', 'dagl_url', 'dagl_limit');
             const ledgerData = await ctx.service.ledger.getAllDataByCondition({ columns: this.ledgerColumn, where: { tender_id: ctx.tender.id } });
             const endStageData = await ctx.service.stageBillsFinal.getAllDataByCondition({ where: { sid: ctx.settle.latestStage.id } });
+            const extraData = this.ledgerExtraColumn.length > 0 ? await ctx.service.ledgerExtra.getData(ctx.tender.id, this.ledgerExtraColumn) : [];
             this.ctx.helper.assignRelaData(ledgerData, [
                 { data: endStageData, fields: ['contract_qty', 'contract_tp', 'qc_qty', 'qc_tp', 'qc_minus_qty'], prefix: 'end_', relaId: 'lid' },
+                { data: extraData, fields: this.ledgerExtraColumn, prefix: '', relaId: 'id' },
             ]);
             return ledgerData;
         }
         async _getStagePosData(ctx) {
             this.posColumn = ['id', 'tid', 'lid', 'name', 'position', 'porder', 'quantity', 'add_stage_order', 'drawing_code'];
+            this.posExtraColumn = [];
+            if (this.ctx.session.sessionProject.gxby) this.posExtraColumn.push('gxby_status', 'gxby_url', 'gxby_limit');
+            if (this.ctx.session.sessionProject.dagl) this.posExtraColumn.push('dagl_status', 'dagl_url', 'dagl_limit');
             const posData = await ctx.service.pos.getAllDataByCondition({ columns: this.posColumn, where: { tid: ctx.tender.id } });
             const endStageData = await ctx.service.stagePosFinal.getAllDataByCondition({ where: { sid: ctx.settle.latestStage.id } });
+            const extraData = this.posExtraColumn.length > 0 ? await ctx.service.posExtra.getData(ctx.tender.id, this.posExtraColumn) : [];
             this.ctx.helper.assignRelaData(posData, [
                 { data: endStageData, fields: ['contract_qty', 'qc_qty', 'qc_minus_qty'], prefix: 'end_', relaId: 'pid' },
+                { data: extraData, fields: this.posExtraColumn, prefix: '', relaId: 'id'},
             ]);
             return posData;
         }
@@ -263,6 +266,33 @@ module.exports = app => {
                 [this.pretreadSettle.settleBills, this.pretreadSettle.settlePos] = await pretreadObj.doSettle(this.ctx.settle);
             }
         }
+        async _loadSettleBills(ctx) {
+            const billsData = this.checkNeedPretreadData(ctx)
+                ? this.pretreadSettle.settleBills
+                : await ctx.service.settleBills.getAllDataByCondition({ where: { settle_id: ctx.settle.id } });
+
+            this.ledgerExtraColumn = [];
+            if (this.ctx.session.sessionProject.gxby) this.ledgerExtraColumn.push('gxby_status', 'gxby_url', 'gxby_limit');
+            if (this.ctx.session.sessionProject.dagl) this.ledgerExtraColumn.push('dagl_status', 'dagl_url', 'dagl_limit');
+            const extraData = this.ledgerExtraColumn.length > 0 ? await ctx.service.ledgerExtra.getData(ctx.tender.id, this.ledgerExtraColumn) : [];
+            this.ctx.helper.assignRelaData(billsData, [
+                { data: extraData, fields: this.ledgerExtraColumn, prefix: '', relaId: 'id'},
+            ], 'lid');
+            return billsData;
+        }
+        async _loadSettlePos(ctx) {
+            const posData = this.checkNeedPretreadData(ctx)
+                ? this.pretreadSettle.settlePos
+                : await ctx.service.settlePos.getAllDataByCondition({ where: { settle_id: ctx.settle.id } });
+            this.posExtraColumn = [];
+            if (this.ctx.session.sessionProject.gxby) this.posExtraColumn.push('gxby_status', 'gxby_url', 'gxby_limit');
+            if (this.ctx.session.sessionProject.dagl) this.posExtraColumn.push('dagl_status', 'dagl_url', 'dagl_limit');
+            const extraData = this.posExtraColumn.length > 0 ? await ctx.service.posExtra.getData(ctx.tender.id, this.posExtraColumn) : [];
+            this.ctx.helper.assignRelaData(posData, [
+                { data: extraData, fields: this.posExtraColumn, prefix: '', relaId: 'id'},
+            ], 'pid');
+            return posData;
+        }
         async _loadSettleDataByKey(ctx, key, hpack) {
             switch (key) {
                 case 'stageBills':
@@ -279,15 +309,11 @@ module.exports = app => {
                     return hpack ? [ctx.helper.hpackArr(settleChange), key] : [settleChange, ''];
                 case 'settleBills':
                     await this._loadPretreatSettleData(ctx);
-                    const settleBills = this.checkNeedPretreadData(ctx)
-                        ? this.pretreadSettle.settleBills
-                        : await ctx.service.settleBills.getAllDataByCondition({ where: { settle_id: ctx.settle.id } });
+                    const settleBills = await this._loadSettleBills(ctx);
                     return hpack ? [ctx.helper.hpackArr(settleBills), 'settleBills'] : [settleBills, ''];
                 case 'settlePos':
                     await this._loadPretreatSettleData(ctx);
-                    const settlePos = this.checkNeedPretreadData(ctx)
-                        ? this.pretreadSettle.settlePos
-                        : await ctx.service.settlePos.getAllDataByCondition({ where: { settle_id: ctx.settle.id } });
+                    const settlePos = await this._loadSettlePos(ctx);
                     return hpack ? [ctx.helper.hpackArr(settlePos), 'settlePos'] : [settlePos, ''];
                 case 'settleSelect':
                     const settleSelect = await ctx.service.settleSelect.getAllDataByCondition({ where: { settle_id: ctx.settle.id } });
@@ -302,7 +328,6 @@ module.exports = app => {
                     return [null, ''];
             }
         }
-
         async loadSettleData(ctx) {
             try {
                 const data = JSON.parse(ctx.request.body.data);
@@ -322,6 +347,15 @@ module.exports = app => {
             }
         }
 
+        _checkSettleCanModify(ctx, isCheck = false) {
+            // 检查登录用户,是否可操作
+            if (ctx.settle.readOnly) {
+                if (!isCheck || !ctx.settle.canCheck) throw '该结算期当前您无权操作';
+            }
+            // if (ctx.settle.assist && ctx.settle.assist.confirm) {
+            //     throw '协同数据已确认,如需修改,请撤销上报或重新审批';
+            // }
+        }
         async updateSelect(ctx) {
             try {
                 const data = JSON.parse(ctx.request.body.data);
@@ -386,7 +420,6 @@ module.exports = app => {
                 ctx.ajaxErrorBody(err, '上传文件失败');
             }
         }
-
         async deleteFile(ctx) {
             try {
                 const data = JSON.parse(ctx.request.body.data);
@@ -408,7 +441,6 @@ module.exports = app => {
                 ctx.ajaxErrorBody(err, '删除文件失败');
             }
         }
-
         async saveFile(ctx) {
             let stream;
             try {
@@ -443,6 +475,102 @@ module.exports = app => {
             }
         }
 
+        /**
+         * 添加审批人
+         * @param ctx
+         * @return {Promise<void>}
+         */
+        async addAudit(ctx) {
+            try {
+                const data = JSON.parse(ctx.request.body.data);
+                const id = this.app._.toInteger(data.auditorId);
+                if (isNaN(id) || id <= 0) throw '参数错误';
+
+                // 检查权限等
+                if (ctx.settle.user_id !== ctx.session.sessionUser.accountId) throw '您无权添加审核人';
+                if (ctx.settle.audit_status !== auditConst.settle.status.uncheck && ctx.settle.audit_status !== auditConst.settle.status.checkNo) {
+                    throw '当前不允许添加审核人';
+                }
+
+                // 检查审核人是否已存在
+                const exist = await ctx.service.settleAudit.getDataByCondition({ settle_id: ctx.settle.id, audit_times: ctx.settle.audit_times, audit_id: id });
+                if (exist) throw '该审核人已存在,请勿重复添加';
+
+                const auditorInfo = await this.ctx.service.projectAccount.getDataById(id);
+                if (!auditorInfo) throw '添加的审批人不存在';
+
+                const shenpiInfo = await ctx.service.shenpiAudit.getDataByCondition({ tid: ctx.tender.id, sp_type: shenpiConst.sp_type.settle, sp_status: shenpiConst.sp_status.gdzs });
+                const is_gdzs = shenpiInfo && ctx.tender.info.shenpi.settle === shenpiConst.sp_status.gdzs ? 1 : 0;
+                const result = await ctx.service.settleAudit.addAuditor(ctx.settle.id, auditorInfo, ctx.settle.audit_times, is_gdzs);
+                if (!result) throw '添加审核人失败';
+
+                const auditors = await ctx.service.settleAudit.getAuditorGroup(ctx.settle.id, ctx.settle.audit_times);
+                ctx.body = { err: 0, msg: '', data: auditors };
+            } catch (err) {
+                this.log(err);
+                ctx.body = { err: 1, msg: err.toString(), data: null };
+            }
+        }
+        /**
+         * 移除审批人
+         * @param ctx
+         * @return {Promise<void>}
+         */
+        async deleteAudit(ctx) {
+            try {
+                const data = JSON.parse(ctx.request.body.data);
+                const id = data.auditorId instanceof Number ? data.auditorId : this.app._.toNumber(data.auditorId);
+                if (isNaN(id) || id <= 0) throw '参数错误';
+
+                const result = await ctx.service.settleAudit.deleteAuditor(ctx.settle.id, id, ctx.settle.audit_times);
+                if (!result) throw '移除审核人失败';
+
+                const auditors = await ctx.service.settleAudit.getAuditors(ctx.settle.id, ctx.settle.audit_times);
+                ctx.body = { err: 0, msg: '', data: auditors };
+            } catch (err) {
+                ctx.body = { err: 1, msg: err.toString(), data: null };
+            }
+        }
+        async _checkSettleSelect(ctx) {
+            const Check = require('../lib/settle');
+            const checkObj = new Check(ctx);
+            const result = await checkObj.checkSettle(ctx.settle);
+            if (!result) throw '您勾选了不可结算的数据,请检查后再上报';
+        }
+        async auditStart(ctx) {
+            try {
+                if (ctx.settle.user_id !== ctx.session.sessionUser.accountId) throw '您无权上报该期数据';
+                if (ctx.settle.audit_status !== auditConst.settle.status.uncheck && ctx.settle.audit_status !== auditConst.settle.status.checkNo) throw '该期数据当前无法上报';
+                await this._checkSettleSelect(ctx);
+
+                await ctx.service.settleAudit.start(ctx.settle);
+                //ctx.redirect('/tender/' + ctx.tender.id + '/settle/' + newSettle.settle_order);
+                ctx.redirect(ctx.request.header.referer);
+            } catch (err) {
+                ctx.log(err);
+                ctx.postError(err, '上报失败');
+                ctx.redirect(`/tender/${ctx.settle.tid}/settle/${ctx.settle.settle_order}/select`);
+            }
+        }
+        async auditCheck(ctx) {
+            try {
+                if (!ctx.settle || (ctx.settle.audit_status !== auditConst.settle.status.checking && ctx.settle.audit_status !== auditConst.settle.status.checkNoPre)) {
+                    throw '当前期数据有误';
+                }
+                if (ctx.settle.curAuditorIds.indexOf(ctx.session.sessionUser.accountId) < 0) {
+                    throw '您无权进行该操作';
+                }
+
+                const checkType = parseInt(ctx.request.body.checkType);
+                const opinion = ctx.request.body.opinion.replace(/\r\n/g, '<br/>').replace(/\n/g, '<br/>').replace(/\s/g, ' ');
+                await ctx.service.settleAudit.check(ctx.settle, checkType, opinion);
+            } catch (err) {
+                ctx.log(err);
+                ctx.postError(err, '审批失败');
+            }
+            ctx.redirect(ctx.request.header.referer);
+        }
+
         async loadGatherData(ctx) {
             try {
                 const settle = await this.ctx.service.settle.getLatestCompleteSettle(ctx.tender.id);
@@ -454,7 +582,6 @@ module.exports = app => {
                 ctx.ajaxErrorBody(err, '获取结算汇总数据错误');
             }
         }
-
         async gather(ctx) {
             try {
                 const renderData = {

+ 10 - 6
app/extend/helper.js

@@ -839,7 +839,7 @@ module.exports = {
      * @param {Array} main - 主数据
      * @param {Array[]}rela - 相关数据 {data, fields, prefix, relaId}
      */
-    assignRelaData(main, rela) {
+    assignRelaData(main, rela, mainKey = 'id') {
         const index = {},
             indexPre = 'id_';
         const loadFields = function(datas, fields, prefix, relaId) {
@@ -856,7 +856,7 @@ module.exports = {
             }
         };
         for (const m of main) {
-            index[indexPre + m.id] = m;
+            index[indexPre + m[mainKey]] = m;
             for (const r of rela) {
                 if (r.defaultData) _.assignIn(m, r.defaultData);
             }
@@ -1616,11 +1616,15 @@ module.exports = {
         });
         const Group = [];
         for (const a of auditors) {
-            if (a[orderField] > 0) {
-                if (Group[a[orderField] - 1]) {
-                    Group[a[orderField] - 1].push(a);
+            if (a[orderField] !== undefined) {
+                if (Group.length === 0) {
+                    Group.push([a])
                 } else {
-                    Group.push([a]);
+                    if (Group[Group.length-1][0][orderField] === a[orderField]) {
+                        Group[Group.length-1].push(a);
+                    } else {
+                        Group.push([a]);
+                    }
                 }
             } else {
                 Group.push([a]);

+ 87 - 8
app/lib/settle.js

@@ -50,9 +50,9 @@ class Settle {
         for (const s of select) {
             if (s.pid) {
                 const sp = this.stagePos.getPos(s.pid);
-                // console.log(s.pid, sp, this.stagePos.items);
                 if (!sp) continue;
                 sp.is_settle = true;
+                sp.selected = true;
                 const sb = this.stageTree.nodes.find(x => { return x.id === sp.lid });
                 sb.is_settle = true;
                 const parents = this.stageTree.getAllParents(sb);
@@ -61,6 +61,7 @@ class Settle {
             if (s.lid) {
                 const sb = this.stageTree.nodes.find(x => { return x.id === s.lid });
                 sb.is_settle = true;
+                sb.selected = true;
                 const parents = this.stageTree.getAllParents(sb);
                 parents.forEach(p => { p.is_settle = true; });
                 const posterity = this.stageTree.getPosterity(sb);
@@ -73,6 +74,18 @@ class Settle {
         }
     }
 
+    async _loadStageChange() {
+        this.stageChange = await this.ctx.service.stageChangeFinal.getUnSettleChangeData(this.settle.latestStage);
+        for (const change of this.stageChange) {
+            const sb = this.stageTree.datas.find(x => { return x.id === change.gcl_id });
+            if (sb) sb.undoneChange = true;
+            if (change.mx_id) {
+                const sp = this.stagePos.getPos(change.mx_id);
+                if (sp) sp.undoneChange = true;
+            }
+        }
+    }
+
     async _loadPreSettle() {
         if (this.settle.settle_order <= 1) return;
         const prePos = await this.ctx.service.settlePos.getAllDataByCondition({ where: { tid: this.settle.tid, settle_order: this.settle.settle_order - 1 } });
@@ -117,7 +130,7 @@ class Settle {
                 p.end_qc_qty = helper.add(p.cur_qc_qty, p.pre_qc_qty);
                 p.end_qc_minus_qty = helper.add(p.cur_qc_minus_qty, p.pre_qc_minus_qty);
             }
-            if (helper.checkNumEqual(p.end_contract_qty, p.quantity)) {
+            if (helper.numEqual(p.end_contract_qty, p.quantity)) {
                 p.settle_status = 2;
                 p.settle_done_order = p.settle_done_order || settle.settle_order;
             }
@@ -152,14 +165,14 @@ class Settle {
                 b.end_qc_minus_qty = helper.add(b.cur_qc_minus_qty, b.pre_qc_minus_qty);
             }
             if (b.is_tp) {
-                if (helper.checkNumEqual(b.end_contract_tp, b.total_price)) {
+                if (helper.numEqual(b.end_contract_tp, b.total_price)) {
                     b.settle_status = 2;
                     b.settle_done_order = b.settle_done_order || settle.settle_order;
                 } else {
                     b.settle_status = 1;
                 }
             } else {
-                if (helper.checkNumEqual(b.end_contract_qty, b.quantity)) {
+                if (helper.numEqual(b.end_contract_qty, b.quantity)) {
                     b.settle_status = 2;
                     b.settle_done_order = b.settle_done_order || settle.settle_order;
                 } else {
@@ -171,6 +184,7 @@ class Settle {
 
     getSettleData() {
         const settleBills = [];
+        const sum = {};
         for (const node of this.stageTree.nodes) {
             if (!node.is_settle && !node.pre_settle) continue;
             settleBills.push({
@@ -187,24 +201,27 @@ class Settle {
                 end_contract_qty: node.end_contract_qty || 0, end_contract_tp: node.end_contract_tp || 0,
                 end_qc_qty: node.end_qc_qty || 0, end_qc_tp: node.end_qc_tp || 0, end_qc_minus_qty: node.end_qc_minus_qty || 0,
                 is_settle: node.is_settle ? 1 : 0, pre_settle: node.pre_settle ? 1 : 0,
-                settle_status: node.settle_status || 0, settle_done_order: node.settle_done_order,
+                settle_status: node.settle_status || 0, settle_done_order: node.settle_done_order || 0,
             });
+            sum.contract_tp = this.ctx.helper.add(sum.contract_tp, node.cur_contract_tp);
+            sum.qc_tp = this.ctx.helper.add(sum.qc_tp, node.cur_qc_tp);
         }
+        sum.tp = this.ctx.helper.add(sum.contract_tp, sum.qc_tp);
         const settlePos = [];
         for (const pos of this.stagePos.datas) {
             if (!pos.is_settle && !pos.pre_settle) continue;
             settlePos.push({
                 tid: this.settle.tid, settle_id: this.settle.id, settle_order: this.settle.settle_order,
                 lid: pos.lid, pid: pos.id,
-                name: pos.name || '', drawing_code: pos.drawing_code || '', position: pos.position || '',
+                name: pos.name || '', drawing_code: pos.drawing_code || '', position: pos.position || '', porder: pos.porder || 1,
                 cur_contract_qty: pos.cur_contract_qty || 0, cur_qc_qty: pos.cur_qc_qty || 0, cur_qc_minus_qty: pos.cur_qc_minus_qty || 0,
                 pre_contract_qty: pos.pre_contract_qty || 0, pre_qc_qty: pos.pre_qc_qty || 0, pre_qc_minus_qty: pos.pre_qc_minus_qty || 0,
                 end_contract_qty: pos.end_contract_qty || 0, end_qc_qty: pos.end_qc_qty || 0, end_qc_minus_qty: pos.end_qc_minus_qty || 0,
                 is_settle: pos.is_settle ? 1 : 0, pre_settle: pos.pre_settle ? 1 : 0,
-                settle_status: pos.settle_status || 0, settle_done_order: pos.settle_done_order,
+                settle_status: pos.settle_status || 0, settle_done_order: pos.settle_done_order || 0,
             });
         }
-        return [settleBills, settlePos];
+        return [settleBills, settlePos, sum];
     }
 
     async doSettle(settle) {
@@ -216,6 +233,68 @@ class Settle {
         this.calculateSettle();
         return this.getSettleData();
     }
+
+    _initPosUndone(pos) {
+        pos.undoneDeal = pos.quantity ? !this.ctx.helper.numEqual(pos.settle_contract_qty, pos.quantity) : false;
+        pos.undone = !!pos.undoneDeal || !!pos.undoneChange;
+    }
+
+    _initNodeUndone(node) {
+        if (node.undoneDeal === undefined) node.undoneDeal = false;
+        if (node.undoneChange === undefined) node.undoneChange = false;
+        if (node.children && node.children.length > 0) {
+            for (const child of node.children) {
+                this._initNodeUndone(child);
+                if (child.undoneDeal) node.undoneDeal = true;
+                if (child.undoneChange) node.undoneChange = true;
+            }
+            node.undone = !!node.undoneDeal || !!node.undoneChange;
+        } else {
+            const posRange = this.stagePos.getLedgerPos(node.id);
+            if (posRange && posRange.length > 0) {
+                for (const pos of posRange) {
+                    this._initPosUndone(pos);
+                    if (pos.undoneDeal) node.undoneDeal = true;
+                    if (pos.undoneChange) node.undoneChange = true;
+                }
+                node.undone = !!node.undoneDeal || !!node.undoneChange;
+            } else {
+                node.undoneDeal = node.settle_contract_qty
+                    ? !this.ctx.helper.numEqual(node.settle_contract_qty, node.quantity)
+                    : !this.ctx.helper.numEqual(node.settle_contract_tp, node.total_price);
+                node.undone = !!node.undoneDeal || !!node.undoneChange;
+            }
+        }
+    }
+
+    _checkNodeError(node) {
+        const billsError = !!node.selected && node.undone;
+        if (billsError) return billsError;
+        const posRange = !node.children || node.children.length === 0 ? (this.stagePos.getLedgerPos(node.id) || []) : [];
+        for (const pos of posRange) {
+            if (!!pos.selected && pos.undone) return true;
+        }
+        return false;
+    }
+
+    checkAllSelect() {
+        for (const node of this.stageTree.children) {
+            this._initNodeUndone(node);
+        }
+        this.error = [];
+        for (const node of this.stageTree.nodes) {
+            if (this._checkNodeError(node)) this.error.push(node);
+        }
+        return this.error.length === 0;
+    }
+
+    async checkSettle(settle) {
+        this.settle = settle;
+        await this._loadLatestStageData(settle);
+        await this._loadSettleSelect();
+        await this._loadStageChange();
+        return this.checkAllSelect();
+    }
 }
 
 module.exports = Settle;

+ 1 - 1
app/middleware/settle_check.js

@@ -27,7 +27,7 @@ module.exports = options => {
             yield this.service.settle.doCheckSettle(settle);
             // 获取最新的期数
             settle.highOrder = yield this.service.settle.count({ tid: this.tender.id });
-
+            yield this.service.settle.checkSettleShenpi(settle);
             this.settle = settle;
             yield next;
         } catch (err) {

+ 4 - 1
app/middleware/tender_check.js

@@ -64,6 +64,8 @@ module.exports = options => {
                 return x.user_id === tender.data.user_id || stageAuditorsId.indexOf(x.user_id) >= 0;
             });
             const auditAssistsId = this.helper._.map(auditAssists, 'ass_user_id');
+            const settleAuditors = yield this.service.settleAudit.getAllAuditors(tender.id);
+            const settleAuditorsId = this.helper._.map(settleAuditors, 'audit_id');
             const changeAuditors = yield this.service.changeAudit.getAllAuditors(tender.id);
             const changeAuditorsId = this.helper._.map(changeAuditors, 'uid');
             const reviseAuditors = yield this.service.reviseAudit.getAllAuditors(tender.id);
@@ -86,7 +88,8 @@ module.exports = options => {
             tender.touristPermission = yield this.service.tenderTourist.getTouristPermission(isTenderTourist);
             if (auditorsId.indexOf(accountId) === -1 && tender.data.user_id !== accountId &&
                 (tenderPermission === null || tenderPermission === undefined || tenderPermission.indexOf('2') === -1) &&
-                stageAuditorsId.indexOf(accountId) === -1 && auditAssistsId.indexOf(accountId) === -1 && changeAuditorsId.indexOf(accountId) === -1 &&
+                stageAuditorsId.indexOf(accountId) === -1 && auditAssistsId.indexOf(accountId) === -1 &&
+                settleAuditorsId.indexOf(accountId) === -1 && changeAuditorsId.indexOf(accountId) === -1 &&
                 reviseAuditorsId.indexOf(accountId) === -1 && materialAuditorsId.indexOf(accountId) === -1 &&
                 changeProjectAuditorsId.indexOf(accountId) === -1 && changeProjectXsAuditorsId.indexOf(accountId) === -1 &&
                 changeApplyAuditorsId.indexOf(accountId) === -1 && changePlanAuditorsId.indexOf(accountId) === -1 &&

+ 52 - 0
app/public/js/settle_select.js

@@ -80,6 +80,25 @@ $(document).ready(() => {
         {field: 'gxby', getValue: getGxbyText, url_field: 'gxby_url'},
         {field: 'dagl', getValue: getDaglText, url_field: 'dagl_url'},
     ]);
+    billsSpreadSetting.getColor = function (sheet, data, row, col, defaultColor) {
+        if (data) {
+            if (col.field === 'gxby') {
+                const def = thirdParty.gxby.find(function (x) {
+                    return x.value === data.gxby_status;
+                });
+                if (def && def.color) return def.color;
+            } else if (col.field === 'dagl') {
+                const def = thirdParty.dagl.find(function (x) {
+                    return x.value === data.dagl_status;
+                });
+                if (def && def.color) return def.color;
+            }
+
+            return data.selected && data.undone ? spreadColor.stage.over : defaultColor;
+        } else {
+            return defaultColor;
+        }
+    };
     SpreadJsObj.initSheet(slSheet, billsSpreadSetting);
 
     const spSpread = SpreadJsObj.createNewSpread($('#settle-pos')[0]);
@@ -105,6 +124,25 @@ $(document).ready(() => {
         {field: 'gxby', getValue: getGxbyText, url_field: 'gxby_url'},
         {field: 'dagl', getValue: getDaglText, url_field: 'dagl_url'},
     ]);
+    posSpreadSetting.getColor = function (sheet, data, row, col, defaultColor) {
+        if (data) {
+            if (col.field === 'gxby') {
+                const def = thirdParty.gxby.find(function (x) {
+                    return x.value === data.gxby_status;
+                });
+                if (def && def.color) return def.color;
+            } else if (col.field === 'dagl') {
+                const def = thirdParty.dagl.find(function (x) {
+                    return x.value === data.dagl_status;
+                });
+                if (def && def.color) return def.color;
+            }
+
+            return data.selected && data.undone ? spreadColor.stage.over : defaultColor;
+        } else {
+            return defaultColor;
+        }
+    };
     SpreadJsObj.initSheet(spSheet, posSpreadSetting);
 
     const settleCheck = {
@@ -365,6 +403,20 @@ $(document).ready(() => {
                     afterLocated: function () {
                         settlePosObj.loadCurPosData();
                     },
+                    customSearch: [
+                        {
+                            key: 'err', title: '结算错误', valid: true, parent: true,
+                            check: function (node) {
+                                const billsError = !!node.selected && node.undone;
+                                if (billsError) return billsError;
+                                const posRange = !node.children || node.children.length === 0 ? (settlePos.getLedgerPos(node.id) || []) : [];
+                                for (const pos of posRange) {
+                                    if (!!pos.selected && pos.undone) return true;
+                                }
+                                return false;
+                            }
+                        },
+                    ],
                 });
                 searchLedger.spread.refresh();
             }

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

@@ -582,7 +582,7 @@ const showSelectTab = function(select, spread, afterShow) {
             const cs = setting.customSearch.find(function (x) {return x.key === key});
             return cs ? cs.check : null;
         };
-        const getParantFun = function (key) {
+        const getParentFun = function (key) {
             const cs = setting.customSearch.find(function (x) {return x.key === key});
             return cs && cs.parent !== undefined ? cs.parent : false;
         };
@@ -592,9 +592,9 @@ const showSelectTab = function(select, spread, afterShow) {
             const checkFun = getCheckFun(key);
             searchResult = [];
             const sortData = SpreadJsObj.getSortData(searchSheet);
-            const parantFun = getParantFun(key);
+            const parentFun = getParentFun(key);
             for (const node of sortData) {
-                if (node.children && node.children.length > 0 && !parantFun) continue;
+                if (node.children && node.children.length > 0 && !parentFun) continue;
                 if (checkFun && checkFun(node)) {
                     if (!keyword ||
                         (node.code && node.code.indexOf(keyword) > -1) ||

+ 452 - 0
app/public/js/shares/settle_audit.js

@@ -0,0 +1,452 @@
+'use strict';
+
+/**
+ *
+ *
+ * @author Mai
+ * @date 2019/2/27
+ * @version
+ */
+
+// 检查上报情况
+function checkAuditorFrom () {
+    if ($('#auditors li').length === 0) {
+        if(shenpi_status === shenpiConst.sp_status.gdspl) {
+            toastr.error('请联系管理员添加审批人');
+        } else {
+            toastr.error('请先选择审批人,再上报数据');
+        }
+        return false;
+    }
+    $('#hide-all').show();
+    return true;
+}
+// 点击验证码
+function codeSuccess(btn) {
+    let counter = 60;
+    btn.addClass('disabled').text('重新获取 ' + counter + 'S');
+    btn.parent().siblings('input').removeAttr('readonly').attr('placeholder', '输入短信中的6位验证码');
+    const bindBtn = $("#bind-btn");
+    bindBtn.removeClass('btn-secondary disabled').addClass('btn-primary');
+
+    const countDown = setInterval(function() {
+        const countString = counter - 1 <= 0 ? '' : ' ' + (counter - 1) + 'S';
+        // 倒数结束后
+        if (countString === '') {
+            clearInterval(countDown);
+            btn.removeClass('disabled');
+        }
+        const text = '重新获取' + countString;
+        btn.text(text);
+        counter -= 1;
+    }, 1000);
+}
+
+$(document).ready(function () {
+    let timer = null;
+    let oldSearchVal = null;
+
+    // 搜索审批人
+    $('#gr-search').bind('input propertychange', function(e) {
+        oldSearchVal = e.target.value;
+        timer && clearTimeout(timer);
+        timer = setTimeout(() => {
+            const newVal = $('#gr-search').val();
+            let html = '';
+            if (newVal && newVal === oldSearchVal) {
+                accountList.filter(item => item && cur_uid !== item.id && (item.name.indexOf(newVal) !== -1 || (item.mobile && item.mobile.indexOf(newVal) !== -1))).forEach(item => {
+                    html += `<dd class="border-bottom p-2 mb-0" data-id="${item.id}">
+                        <p class="mb-0 d-flex"><span class="text-primary">${item.name}</span><spanclass="ml-auto">${item.mobile || ''}</span></p>
+                        <span class="text-muted">${item.role || ''}</span>
+                    </dd>`;
+                });
+                $('.book-list').empty();
+                $('.book-list').append(html);
+            } else {
+                if (!$('.acc-btn').length) {
+                    accountGroup.forEach((group, idx) => {
+                        if (!group) return;
+                        html += `<dt><a href="javascript: void(0);" class="acc-btn" data-groupid="${idx}" data-type="hide"><i class="fa fa-plus-square"></i></a> ${group.groupName}</dt><div class="dd-content" data-toggleid="${idx}">`;
+                        group.groupList.forEach(item => {
+                            if (item.id !== cur_uid) {
+                                html += `<dd class="border-bottom p-2 mb-0" data-id="${item.id}">
+                                    <p class="mb-0 d-flex"><span class="text-primary">${item.name}</span><span class="ml-auto">${item.mobile || ''}</span></p>
+                                    <span class="text-muted">${item.role || ''}</span>
+                                </dd>`;
+                            }
+                        });
+                        html += '</div>';
+                    });
+                    $('.book-list').empty();
+                    $('.book-list').append(html);
+                }
+            }
+        }, 400);
+    });
+
+    // 添加审批流程按钮逻辑
+    $('body').on('click', '.book-list dt', function () {
+        const idx = $(this).find('.acc-btn').attr('data-groupid');
+        const type = $(this).find('.acc-btn').attr('data-type');
+        if (type === 'hide') {
+            $(this).parent().find(`div[data-toggleid="${idx}"]`).show(() => {
+                $(this).children().find('i').removeClass('fa-plus-square').addClass('fa-minus-square-o');
+                $(this).find('.acc-btn').attr('data-type', 'show');
+
+            })
+        } else {
+            $(this).parent().find(`div[data-toggleid="${idx}"]`).hide(() => {
+                $(this).children().find('i').removeClass('fa-minus-square-o').addClass('fa-plus-square');
+                $(this).find('.acc-btn').attr('data-type', 'hide');
+            })
+        }
+        return false
+    });
+
+    // 添加到审批流程
+    $('#book-list').on('click', 'dd', function () {
+        const id = parseInt($(this).data('id'));
+        if (id !== 0) {
+            postData('audit/add', { auditorId: id }, (datas) => {
+                const html = [];
+                // 如果是重新上报,添加到重新上报列表中
+                const auditorshtml = [];
+                for (const [index, data] of datas.entries()) {
+                    if (index !== 0) {
+                        html.push('<li class="list-group-item d-flex" auditorId="'+ data[0].audit_id +'">');
+                        html.push(`<div class="col-auto">${index}</div>`);
+                        html.push('<div class="col">');
+                        for (const auditor of data) {
+                            html.push(`<div class="d-inline-block mx-1"><i class="fa fa-user text-muted"></i> ${auditor.name} <small class="text-muted">${auditor.role}</small></div>`);
+                        }
+                        html.push('</div>');
+                        html.push('<div class="col-auto">');
+                        // todo 添加会签或签时
+                        // html.push('<span class="badge badge-pill badge-primary badge-bg-small"><small></small></span>');
+                        if (shenpi_status === shenpiConst.sp_status.sqspr || (shenpi_status === shenpiConst.sp_status.gdzs && index+1 !== datas.length)) {
+                            html.push('<a href="javascript: void(0)" class="text-danger pull-right">移除</a>');
+                        }
+                        html.push('</div>');
+                        html.push('</li>');
+                    }
+                    auditorshtml.push('<li class="list-group-item" data-auditorid="' + data.audit_id + '">');
+                    auditorshtml.push('<i class="fa ' + (index+1 === datas.length ? 'fa-stop-circle' : 'fa-chevron-circle-down') + '"></i> ');
+                    auditorshtml.push(data.name + ' <small class="text-muted">' + data.role + '</small>');
+                    if (index === 0) {
+                        auditorshtml.push('<span class="pull-right">原报</span>');
+                    } else if (index+1 === datas.length) {
+                        auditorshtml.push('<span class="pull-right">终审</span>');
+                    } else {
+                        auditorshtml.push('<span class="pull-right">'+ transFormToChinese(index) +'审</span>');
+                    }
+                    auditorshtml.push('</li>');
+                }
+                $('#auditors').html(html.join(''));
+                $('#auditors-list').html(auditorshtml.join(''));
+            });
+        }
+    });
+    // 删除审批人
+    $('body').on('click', '#auditors li a', function () {
+        const li = $(this).parents('li');
+        const data = {
+            auditorId: parseInt(li.attr('auditorId')),
+        };
+        postData('audit/delete', data, (result) => {
+            li.remove();
+            for (const rst of result) {
+                const aLi = $('li[auditorId=' + rst.audit_id + ']');
+                $('span', aLi).text(rst.order + ' ' + rst.name + ' ');
+            }
+
+            // 如果是重新上报
+            // 令最后一个图标转换
+            $('#auditors-list li[data-auditorid="' + data.auditorId + '"]').remove();
+            if ($('#auditors-list li').length !== 0 && !$('#auditors-list li i').hasClass('fa-stop-circle')) {
+                $('#auditors-list li').eq($('#auditors-list li').length-1).children('i')
+                    .removeClass('fa-chevron-circle-down').addClass('fa-stop-circle');
+            }
+            for (let i = 0; i < $('#auditors-list li').length; i++) {
+                $('#auditors-list li').eq(i).find('.pull-right').text(i === 0 ? '原报' : (i+1 === $('#auditors-list li').length ? '终' : transFormToChinese(i)) + '审');
+                // $('#auditors-list2 li').eq(i).find('.pull-right').text((i+1 === $('#auditors-list2 li').length ? '终' : transFormToChinese(i+1)) + '审');
+            }
+        });
+    });
+    // 退回选择修改审批人流程
+    $('#hideSp').click(function () {
+        $('#sp-list').modal('hide');
+    });
+    $('a[f-target]').click(function () {
+        $($(this).attr('f-target')).modal('show');
+    });
+
+    // 多层modal关闭后的滚动bug修复
+    $('#sp-list').on('hidden.bs.modal', function (e) {
+        $(document.body).addClass('modal-open');
+    });
+
+    // 重新审批获取手机验证码
+    // 获取验证码
+    let isPosting = false;
+    $("#get-code").click(function() {
+        if (isPosting) return false;
+
+        const btn = $(this);
+        $.ajax({
+            url: '/profile/code',
+            type: 'post',
+            data: { mobile: authMobile, type: 'shenpi' },
+            dataTye: 'json',
+            error: function() {
+                isPosting = false;
+                let csrfToken = Cookies.get('csrfToken_j');
+                xhr.setRequestHeader('x-csrf-token', csrfToken);
+            },
+            beforeSend: function() {
+                isPosting = true;
+            },
+            success: function(response) {
+                isPosting = false;
+                if (response.err === 0) {
+                    codeSuccess(btn);
+                    $("input[name='code']").removeAttr('readonly');
+                    $("#re-shenpi-btn").removeAttr('disabled');
+                } else {
+                    toast(response.msg, 'error');
+                }
+            }
+        });
+    });
+
+    // 管理员更改审批流程js部分
+    let timer2 = null;
+    let oldSearchVal2 = null;
+    $('body').on('input propertychange', '.gr-search', function(e) {
+        oldSearchVal2 = e.target.value;
+        timer2 && clearTimeout(timer2);
+        timer2 = setTimeout(() => {
+            const newVal = $(this).val();
+            const code = $(this).attr('data-code');
+            let html = '';
+            if (newVal && newVal === oldSearchVal2) {
+                accountList.filter(item => item && item.id !== stage_uid && (item.name.indexOf(newVal) !== -1 || (item.mobile && item.mobile.indexOf(newVal) !== -1))).forEach(item => {
+                    html += `<dd class="border-bottom p-2 mb-0 " data-id="${item.id}" >
+                        <p class="mb-0 d-flex"><span class="text-primary">${item.name}</span><span
+                                class="ml-auto">${item.mobile || ''}</span></p>
+                        <span class="text-muted">${item.role || ''}</span>
+                    </dd>`
+                });
+                $('#' + code + '_dropdownMenu .book-list').empty();
+                $('#' + code + '_dropdownMenu .book-list').append(html);
+            } else {
+                if (!$('#' + code + '_dropdownMenu .acc-btn').length) {
+                    accountGroup.forEach((group, idx) => {
+                        if (!group) return;
+                        html += `<dt><a href="javascript: void(0);" class="acc-btn" data-groupid="${idx}" data-type="hide"><i class="fa fa-plus-square"></i>
+                        </a> ${group.groupName}</dt>
+                        <div class="dd-content" data-toggleid="${idx}">`;
+                        group.groupList.forEach(item => {
+                            if (item.id !== stage_uid) {
+                                html += `<dd class="border-bottom p-2 mb-0 " data-id="${item.id}" >
+                                    <p class="mb-0 d-flex"><span class="text-primary">${item.name}</span><span
+                                            class="ml-auto">${item.mobile || ''}</span></p>
+                                    <span class="text-muted">${item.role || ''}</span>
+                                </dd>`;
+                            }
+                        });
+                        html += '</div>';
+                    });
+                    $('#' + code + '_dropdownMenu .book-list').empty();
+                    $('#' + code + '_dropdownMenu .book-list').append(html);
+                }
+            }
+        }, 400);
+    });
+
+    const getAdminEditShenpiListHtml = function(auditGroup) {
+        const html = [];
+        for (const [i, group] of auditGroup.entries()) {
+            if (i === 0) continue;
+            for (const [j, auditor] of group.entries()) {
+                html.push('<tr>');
+                if (j === 0) {
+                    const auditTypeHtml = auditor.type === auditType.key.common ? '' : `<span class="ml-2 badge badge-pill badge-${auditType.info[auditor.audit_type].class} p-1"><small>${auditType.info[auditor.audit_type].short}</small></span>`;
+                    html.push(`<td class="text-left d-flex">${i + '审'}${auditTypeHtml}</td>`);
+                } else {
+                    html.push(`<td class="text-left d-flex"></td>`);
+                }
+                html.push(`<td></span> ${auditor.name} <small class="text-muted">${auditor.role}</small></td>`);
+                html.push(`<td style="text-align: center"><span class="${auditConst.auditStringClass[auditor.audit_status]}">${ auditor.audit_status !== auditConst.status.uncheck ? auditConst.auditString[auditor.audit_status] : '待审批' }</span></td>`);
+                html.push('<td style="text-align: center">');
+                if (auditor.audit_status === auditConst.status.checking && j === group.length - 1) {
+                    html.push('<span class="dropdown mr-2">',
+                        `<a href="javascript: void(0)" class="add-audit" id="${auditor.audit_id}_add_dropdownMenuButton" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">增加</a>`,
+                        makeSelectAudit(auditor.audit_id+'_add', auditor.audit_id, 'add'),'</div>', '</span>');
+                }
+                if (auditor.audit_status === auditConst.status.uncheck) {
+                    if (j === group.length - 1) {
+                        html.push('<span class="dropdown mr-2">',
+                            `<a href="javascript: void(0)" class="add-audit" id="${auditor.audit_id}_add_dropdownMenuButton" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">增加</a>`,
+                            makeSelectAudit(auditor.audit_id+'_add', auditor.audit_id, 'add'),'</div>', '</span>');
+                        if (auditor.audit_type !== auditType.key.common) {
+                            html.push('<span class="dropdown mr-2">',
+                                `<a href="javascript: void(0)" class="add-audit" id="${auditor.audit_id}_add_dropdownMenuButton" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">平级</a>`,
+                                makeSelectAudit(auditor.audit_id+'_add-sibling', auditor.audit_id, 'add-sibling'),'</div>', '</span>');
+                        }
+                    }
+                    html.push('<span class="dropdown mr-2">',
+                        `<a href="javascript: void(0)" class="add-audit" id="${auditor.audit_id}_add_dropdownMenuButton" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">更换</a>`,
+                        makeSelectAudit(auditor.audit_id+'_change', auditor.audit_id, 'change'),'</div>', '</span>');
+                    html.push(`<span class="dropdown">
+                                    <a href="javascript: void(0)" class="text-danger" title="移除" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">移除</a>
+                                    <div class="dropdown-menu">
+                                        <span class="dropdown-item" href="javascript:void(0);">确认移除审批人?</span>
+                                        <div class="dropdown-divider"></div>
+                                        <div class="px-2 py-1 text-center">
+                                            <button class="remove-audit btn btn-sm btn-danger" data-id="${auditor.audit_id}">移除</button>
+                                            <button class="btn btn-sm btn-secondary">取消</button>
+                                        </div>
+                                    </div>
+                                    </span>`);
+                }
+                html.push('</td>');
+                html.push('</tr>');
+            }
+        }
+        return html.join('');
+    };
+
+    $('body').on('click', '#admin-edit-shenpi dl dd', function () {
+        const id = parseInt($(this).attr('data-id'));
+        if (!id) return;
+
+        let this_aid = parseInt($(this).parents('.book-list').attr('data-aid'));
+        let this_operate = $(this).parents('.book-list').attr('data-operate');
+        const user = _.find(accountList, function (item) {
+            return item.id === id;
+        });
+        const auditorIndex = _.findIndex(auditorList, { audit_id: id });
+        if (auditorIndex !== -1) {
+            toastr.warning('该审核人已存在,请勿重复添加');
+            return;
+        }
+        const prop = {
+            operate: this_operate,
+            old_aid: this_aid,
+            new_aid: user.id,
+        };
+        postData('audit/save', prop, (datas) => {
+            $('#admin-edit-shenpi-list').html(getAdminEditShenpiListHtml(datas));
+            changeLiucheng(datas);
+        });
+    });
+
+    // 管理员移除审批人
+    $('body').on('click', '.remove-audit', function () {
+        const id = parseInt($(this).attr('data-id'));
+        const prop = {
+            operate: 'del',
+            old_aid: id,
+        };
+        postData('audit/save', prop, (datas) => {
+            $('#admin-edit-shenpi-list').html(getAdminEditShenpiListHtml(datas));
+            changeLiucheng(datas);
+        });
+    });
+
+    const getAuditTypeText = function (type) {
+        if (type === auditType.key.common) return '';
+        return `<span class="text-${auditType.info[type].class}">${auditType.info[type].long}</span>`;
+    };
+
+    function changeLiucheng(datas) {
+        const auditorshtml = [];
+        let lastAuditorHtml = [];
+        for (const [index,data] of datas.entries()) {
+            auditorshtml.push('<li class="list-group-item d-flex justify-content-between align-items-center">');
+            if (index === 0) {
+                auditorshtml.push('<span class="mr-1"><i class="fa fa fa-play-circle fa-rotate-90"></i></span>');
+            } else if (index+1 === datas.length) {
+                auditorshtml.push('<span class="mr-1"><i class="fa fa fa-stop-circle"></i></span>');
+            } else {
+                auditorshtml.push('<span class="mr-1"><i class="fa fa-chevron-circle-down"></i></span>');
+            }
+            auditorshtml.push('<span class="text-muted">');
+            for (const u of data) {
+                auditorshtml.push(`<small class="d-inline-block text-dark mx-1" title="${u.role}" data-auditorId="${u.audit_id}">${u.name}</small>`);
+            }
+            auditorshtml.push('</span>');
+            auditorshtml.push('<div class="d-flex ml-auto">');
+            if (data[0].audit_type !== auditType.key.common) auditorshtml.push(`<span class="badge badge-pill badge-${auditType.info[data[0].audit_type].class} p-1"><small>${auditType.info[data[0].audit_type].short}</small></span>`);
+            if (index === 0) {
+                auditorshtml.push('<span class="badge badge-light badge-pill ml-auto"><small>原报</small></span>');
+            } else if (index+1 === datas.length) {
+                auditorshtml.push('<span class="badge badge-light badge-pill"><small>终审</small></span>');
+            } else {
+                auditorshtml.push('<span class="badge badge-light badge-pill"><small>'+ transFormToChinese(index) +'审</small></span>');
+            }
+            auditorshtml.push('</div>');
+            auditorshtml.push('</li>');
+            if (data[0].audit_status === sam_auditConst.status.uncheck) {
+                lastAuditorHtml.push('<li class="timeline-list-item pb-2 is_uncheck">');
+                if (index < datas.length - 1) {
+                    lastAuditorHtml.push('<div class="timeline-item-tail"></div>');
+                }
+                lastAuditorHtml.push('<div class="timeline-item-icon bg-secondary text-light"></div>');
+
+                lastAuditorHtml.push('<div class="timeline-item-content">');
+                lastAuditorHtml.push(`<div class="py-1">
+                        <span class="text-black-50">
+                        ${ !index === datas.length - 1 ? data[0].audit_order + '' : '终' }审 ${getAuditTypeText(data[0].audit_type)}
+                        </span>
+                    </div>`);
+                lastAuditorHtml.push('<div class="card"><div class="card-body px-3 py-0">');
+                for (const [i, auditor] of data.entries()) {
+                    lastAuditorHtml.push(`<div class="card-text p-2 py-3 row ${ ( i > 0 ? 'border-top' : '') }">`);
+                    lastAuditorHtml.push(`<div class="col"><span class="h6">${auditor.name}</span><span class="text-muted ml-1">${auditor.role}</span></div>`);
+                    lastAuditorHtml.push('<div class="col">');
+                    lastAuditorHtml.push('</div>');
+                    lastAuditorHtml.push('</div>');
+                }
+                lastAuditorHtml.push('</div></div>');
+                lastAuditorHtml.push('</div>');
+                lastAuditorHtml.push('</li>');
+            }
+        }
+        $('.last-auditor-list .is_uncheck').remove();
+        $('.last-auditor-list').append(lastAuditorHtml.join(''));
+        $('.auditors-list').html(auditorshtml.join(''));
+
+    }
+
+    // 审批流程-选择审批人html 生成
+    function makeSelectAudit(code, aid, status) {
+        let divhtml = '';
+        accountGroup.forEach((group, idx) => {
+            let didivhtml = '';
+            if(group) {
+                group.groupList.forEach(item => {
+                    didivhtml += item.id !== stage_uid ? '<dd class="border-bottom p-2 mb-0 " data-id="' + item.id + '" >\n' +
+                        '<p class="mb-0 d-flex"><span class="text-primary">' + item.name + '</span><span\n' +
+                        '                                                                                class="ml-auto">' + item.mobile + '</span></p>\n' +
+                        '                                                                    <span class="text-muted">' + item.role + '</span>\n' +
+                        '                                                                    </dd>\n' : '';
+                });
+                divhtml += '<dt><a href="javascript: void(0);" class="acc-btn" data-groupid="' + idx + '" data-type="hide"><i class="fa fa-plus-square"></i></a> ' + group.groupName + '</dt>\n' +
+                    '                                                                <div class="dd-content" data-toggleid="' + idx + '">\n' + didivhtml +
+                    '                                                                </div>\n';
+            }
+        });
+        let html = '<div class="dropdown-menu dropdown-menu-right" id="' + code + '_dropdownMenu" aria-labelledby="' + code + '_dropdownMenuButton" style="width:220px">\n' +
+            '                                                        <div class="mb-2 p-2"><input class="form-control form-control-sm gr-search"\n' +
+            '                                                                                     placeholder="姓名/手机 检索" autocomplete="off" data-code="' + code + '"></div>\n' +
+            '                                                        <dl class="list-unstyled book-list" data-aid="'+ aid +'" data-operate="'+ status +'">\n' + divhtml +
+            '                                                        </dl>\n' +
+            '                                                    </div>\n' +
+            '                                                </div>\n' +
+            '                                            </span>\n' +
+            '                                        </span>\n' +
+            '                                        </li>';
+        return html;
+    }
+});

+ 5 - 0
app/router.js

@@ -436,6 +436,11 @@ module.exports = app => {
     app.post('/tender/:id/settle/:sorder/file/upload', sessionAuth, tenderCheck, uncheckTenderCheck, settleCheck, 'settleController.uploadFile');
     app.post('/tender/:id/settle/:sorder/file/delete', sessionAuth, tenderCheck, uncheckTenderCheck, settleCheck, 'settleController.deleteFile');
     app.post('/tender/:id/settle/:sorder/file/save', sessionAuth, tenderCheck, uncheckTenderCheck, settleCheck, 'settleController.saveFile');
+    // 结算审批
+    app.post('/tender/:id/settle/:sorder/audit/add', sessionAuth, tenderCheck, uncheckTenderCheck, settleCheck, 'settleController.addAudit');
+    app.post('/tender/:id/settle/:sorder/audit/delete', sessionAuth, tenderCheck, uncheckTenderCheck, settleCheck, 'settleController.deleteAudit');
+    app.post('/tender/:id/settle/:sorder/audit/start', sessionAuth, tenderCheck, uncheckTenderCheck, settleCheck, 'settleController.auditStart');
+    app.post('/tender/:id/settle/:sorder/audit/check', sessionAuth, tenderCheck, uncheckTenderCheck, settleCheck, 'settleController.auditCheck');
     // 结算汇总
     app.get('/tender/:id/settle/gather', sessionAuth, tenderCheck, uncheckTenderCheck, 'settleController.gather');
     app.get('/tender/:id/settle/gather/load', sessionAuth, tenderCheck, uncheckTenderCheck, 'settleController.loadGatherData');

+ 72 - 29
app/service/settle.js

@@ -10,6 +10,7 @@
 
 const auditConst = require('../const/audit');
 const projectLogConst = require('../const/project_log');
+const shenpiConst = require('../const/shenpi');
 
 module.exports = app => {
     class Settle extends app.BaseService {
@@ -36,7 +37,7 @@ module.exports = app => {
             });
             if (settles.length !== 0 && !this.ctx.session.sessionUser.is_admin) {
                 const last = settles[0];
-                if (last.status === auditConst.settle.status.uncheck && !this.ctx.tender.isTourist) {
+                if (last.audit_status === auditConst.settle.status.uncheck && !this.ctx.tender.isTourist) {
                     if (last.user_id !== this.ctx.session.sessionUser.accountId) {
                         settles.splice(0, 1);
                     }
@@ -44,7 +45,6 @@ module.exports = app => {
             }
             return settles;
         }
-
         async getLatestCompleteSettle(tenderId) {
             const settles = await this.getAllDataByCondition({
                 where: { tid: tenderId, status: auditConst.settle.status.checked },
@@ -67,7 +67,7 @@ module.exports = app => {
                 order: ['order'],
             });
             const pre = settles[settles.length - 1];
-            if (settles.length > 0 && pre.status !== auditConst.settle.status.checked) {
+            if (settles.length > 0 && pre.audit_status !== auditConst.settle.status.checked) {
                 throw '上一期未审批通过,请等待上一期审批通过后,再新增数据';
             }
             const checkedStage = await this.ctx.service.stage.getLastestCompleteStage(tenderId);
@@ -109,7 +109,6 @@ module.exports = app => {
 
             return newSettle;
         }
-
         /**
          * 编辑结算期
          *
@@ -125,7 +124,6 @@ module.exports = app => {
                 settle_period: period,
             }, { where: { tid: tenderId, settle_order: order } });
         }
-
         /**
          * 删除结算期
          * @param id
@@ -154,14 +152,14 @@ module.exports = app => {
 
             settle.user = await this.ctx.service.projectAccount.getAccountInfoById(settle.user_id);
             settle.auditors = await this.ctx.service.settleAudit.getAuditors(settle.id, settle.audit_times); // 全部参与的审批人
-            settle.auditorIds = this._.map(settle.auditors, 'aid');
-            settle.curAuditors = settle.auditors.filter(x => { return x.status === status.checking; }); // 当前流程中审批中的审批人
-            settle.curAuditorIds = this._.map(settle.curAuditors, 'aid');
-            settle.flowAuditors = settle.curAuditors.length > 0 ? settle.auditors.filter(x => { return x.order === settle.curAuditors[0].order; }) : []; // 当前流程中参与的审批人(包含会签时,审批通过的人)
-            settle.flowAuditorIds = this._.map(settle.flowAuditors, 'aid');
+            settle.auditorIds = this._.map(settle.auditors, 'audit_id');
+            settle.curAuditors = settle.auditors.filter(x => { return x.audit_status === status.checking; }); // 当前流程中审批中的审批人
+            settle.curAuditorIds = this._.map(settle.curAuditors, 'audit_id');
+            settle.flowAuditors = settle.curAuditors.length > 0 ? settle.auditors.filter(x => { return x.active_order === settle.curAuditors[0].active_order; }) : []; // 当前流程中参与的审批人(包含会签时,审批通过的人)
+            settle.flowAuditorIds = this._.map(settle.flowAuditors, 'audit_id');
             settle.auditorGroups = this.ctx.helper.groupAuditors(settle.auditors);
             settle.userGroups = this.ctx.helper.groupAuditorsUniq(settle.auditorGroups);
-            settle.finalAuditorIds = settle.userGroups[settle.userGroups.length - 1].map(x => { return x.aid; });
+            settle.finalAuditorIds = settle.userGroups[settle.userGroups.length - 1].map(x => { return x.audit_id; });
 
             // 协作相关
             settle.assists = []; //await this.service.settleAuditAss.getData(settle); // 全部协同人
@@ -180,12 +178,11 @@ module.exports = app => {
                 ? [settle.user_id, ...settle.userAssistIds]
                 : [settle.user_id, ...settle.userAssistIds, ...settle.auditorIds, ...settle.auditAssistIds];
         }
-
         async loadAuditViewData(settle) {
-            const times = settle.audit_status === auditConst.settle.status.checkNo ? settle.audit_times - 1 : settle.audit_times;
+            const auditTimes = settle.audit_status === auditConst.settle.status.checkNo ? settle.audit_times - 1 : settle.audit_times;
 
             if (!settle.user) settle.user = await this.ctx.service.projectAccount.getAccountInfoById(settle.user_id);
-            settle.auditHistory = await this.ctx.service.settleAudit.getAuditorHistory(settle.id, times);
+            settle.auditHistory = await this.ctx.service.settleAudit.getAuditorHistory(settle.id, auditTimes);
             // 获取审批流程中左边列表
             if (settle.audit_status === auditConst.settle.status.checkNo && settle.user_id !== this.ctx.session.sessionUser.accountId) {
                 const auditors = await this.ctx.service.settleAudit.getAuditors(settle.id, settle.audit_times - 1); // 全部参与的审批人
@@ -217,7 +214,7 @@ module.exports = app => {
             const accountId = this.ctx.session.sessionUser.accountId;
             if (settle.audit_status !== status.checkNo) {
                 // 找出当前操作人上一个审批人,包括审批完成的和退回上一个审批人的,同时当前操作人为第一人时,就是则为原报
-                if (settle.flowAuditors.find(x => { return x.status !== status.checking}) && settle.flowAuditorIds.indexOf(accountId) < 0) return; // 当前流程存在审批人审批通过时,不可撤回
+                if (settle.flowAuditors.find(x => { return x.audit_status !== status.checking}) && settle.flowAuditorIds.indexOf(accountId) < 0) return; // 当前流程存在审批人审批通过时,不可撤回
                 const flowAssists = settle.auditAssists.filter(x => { return settle.flowAuditorIds.indexOf(x.user_id) >= 0; });
                 if (flowAssists.find(x => { return x.confirm; })) return; //当前流程存在协审人确认时,不可撤回
                 if (settle.curAuditorIds.indexOf(accountId) < 0 && settle.flowAuditorIds.indexOf(accountId) >= 0) {
@@ -226,16 +223,16 @@ module.exports = app => {
                 }
 
                 const preAuditors = settle.curAuditors[0] && settle.curAuditors[0].order !== 1 ? settle.auditors.filter(x => { return x.order === settle.curAuditors[0].order - 1; }) : [];
-                const preAuditorCheckAgain = preAuditors.find(pa => { return pa.status === status.checkAgain; });
-                const preAuditorCheckCancel = preAuditors.find(pa => { return pa.status === status.checkCancel; });
+                const preAuditorCheckAgain = preAuditors.find(pa => { return pa.audit_status === status.checkAgain; });
+                const preAuditorCheckCancel = preAuditors.find(pa => { return pa.audit_status === status.checkCancel; });
                 const preAuditorHasOld = preAuditors.find(pa => { return pa.is_old === 1; });
-                const preAuditorIds = (preAuditorCheckAgain ? [] : preAuditors.map(x => { return x.aid })); // 重审不可撤回
+                const preAuditorIds = (preAuditorCheckAgain ? [] : preAuditors.map(x => { return x.audit_id })); // 重审不可撤回
                 if ((this._.isEqual(settle.flowAuditorIds, preAuditorIds) && preAuditorCheckCancel) || preAuditorHasOld) {
                     return; // 不可以多次撤回
                 }
 
-                const preAuditChecked = preAuditors.find(pa => { return pa.status === status.checked && pa.aid === accountId; });
-                const preAuditCheckNoPre = preAuditors.find(pa => { return pa.status === status.checkNoPre && pa.aid === accountId; });
+                const preAuditChecked = preAuditors.find(pa => { return pa.audit_status === status.checked && pa.audit_id === accountId; });
+                const preAuditCheckNoPre = preAuditors.find(pa => { return pa.audit_status === status.checkNoPre && pa.audit_id === accountId; });
                 if (preAuditorIds.indexOf(accountId) >= 0) {
                     if (preAuditChecked) {
                         settle.cancancel = 2;// 审批人撤回审批通过
@@ -247,9 +244,9 @@ module.exports = app => {
                     settle.cancancel = 1;// 原报撤回
                 }
             } else {
-                const lastAuditors = await this.service.settleAudit.getAuditors(settle.id, settle.times - 1);
-                const onAuditor = _.findLast(lastAuditors, { status: status.checkNo });
-                if (onAuditor.aid === accountId) {
+                const lastAuditors = await this.ctx.service.settleAudit.getAuditors(settle.id, settle.audit_times - 1);
+                const onAuditor = this._.findLast(lastAuditors, { audit_status: status.checkNo });
+                if (onAuditor.audit_id === accountId) {
                     settle.cancancel = 4;// 审批人撤回退回原报
                     settle.preAuditors = lastAuditors.filter(x => { return x.order === onAuditor.order });
                 }
@@ -267,18 +264,18 @@ module.exports = app => {
                 settle.readOnly = true;
             }
             // 读取数据相关
-            settle.curTimes = settle.audit_status === status.checkNo && settle.readOnly ? settle.times - 1 : settle.times;
+            settle.curTimes = settle.audit_status === status.checkNo && settle.readOnly ? settle.audit_times - 1 : settle.audit_times;
             // 协作人相关
 
             if (settle.audit_status === status.uncheck) {
                 if (!settle.readOnly) {
                     settle.assist = settle.userAssists.find(x => { return x.ass_user_id === accountId; });
                 }
-                settle.curTimes = settle.times;
+                settle.curTimes = settle.audit_times;
             } else if (settle.audit_status === status.checkNo) {
                 if (!settle.readOnly) {
                     settle.assist = settle.userAssists.find(x => { return x.ass_user_id === accountId; });
-                    settle.curTimes = settle.times;
+                    settle.curTimes = settle.audit_times;
                 }
             } else {
                 const ass = settle.auditAssists.find(x => { return settle.flowAuditorIds.indexOf(x.user_id) >= 0 && x.ass_user_id === accountId; });
@@ -286,7 +283,7 @@ module.exports = app => {
                     settle.assist = ass;
                 }
                 if (!settle.readOnly) {
-                    settle.readOnly = !_.isEqual(settle.flowAuditorIds, settle.curAuditorIds);
+                    settle.readOnly = !this._.isEqual(settle.flowAuditorIds, settle.curAuditorIds);
                     settle.canCheck = true;
                 }
             }
@@ -317,9 +314,55 @@ module.exports = app => {
             // 可否撤回,是哪一种撤回
             await this._doCheckSettleCanCancel(settle);
         }
+        async checkSettleShenpi(settle) {
+            const status = auditConst.settle.status;
+            const info = this.ctx.tender.info;
+            const shenpi_status = info.shenpi.settle;
+            if ((settle.audit_status === status.uncheck || settle.audit_status === status.checkNo) && shenpi_status !== shenpiConst.sp_status.sqspr) {
+                // 进一步比较审批流是否与审批流程设置的相同,不同则替换为固定审批流或固定的终审
+                const auditList = await this.ctx.service.settleAudit.getAllDataByCondition({ where: { settle_id: settle.id, audit_times: settle.audit_times }, orders: [['audit_order', 'asc']] });
+                auditList.shift();
+                if (shenpi_status === shenpiConst.sp_status.gdspl) {
+                    const shenpiList = await this.ctx.service.shenpiAudit.getAllDataByCondition({ where: { tid: settle.tid, sp_type: shenpiConst.sp_type.settle, sp_status: shenpi_status } });
+                    // 判断2个id数组是否相同,不同则删除原审批流,切换成固定的审批流
+                    let sameAudit = auditList.length === shenpiList.length;
+                    if (sameAudit) {
+                        for (const audit of auditList) {
+                            const shenpi = shenpiList.find(x => { return x.audit_id === audit.audit_id; });
+                            if (!shenpi || shenpi.audit_order !== audit.audit_order || shenpi.audit_type !== audit.audit_type) {
+                                sameAudit = false;
+                                break;
+                            }
+                        }
+                    }
+                    if (!sameAudit) {
+                        await this.ctx.service.settleAudit.updateNewAuditList(settle, shenpiList);
+                        await this.loadRelaUser(settle);
+                    }
+                } else if (shenpi_status === shenpiConst.sp_status.gdzs) {
+                    const shenpiInfo = await this.ctx.service.shenpiAudit.getDataByCondition({ tid: settle.tid, sp_type: shenpiConst.sp_type.settle, sp_status: shenpi_status });
+                    // 判断最后一个id是否与固定终审id相同,不同则删除原审批流中如果存在的id和添加终审
+                    const lastAuditors = auditList.filter(x => { x.order === auditList.order; });
+                    if (shenpiInfo && (lastAuditors.length === 0 || (lastAuditors.length > 1 || shenpiInfo.audit_id !== lastAuditors[0].audit_id))) {
+                        await this.ctx.service.settleAudit.updateLastAudit(settle, auditList, shenpiInfo.audit_id);
+                        await this.loadRelaUser(settle);
+                    } else if (!shenpiInfo) {
+                        // 不存在终审人的状态下这里恢复为授权审批人
+                        this.tender.info.shenpi.settle = shenpiConst.sp_status.sqspr;
+                    }
+                }
+            }
+        }
 
-        async doSettle(settle) {
-
+        async doSettle(transaction, settle) {
+            const SettleObj = require('../lib/settle');
+            const settleObj = new SettleObj(this.ctx);
+            const [settleBills, settlePos, settleSum] = await settleObj.doSettle(settle);
+            await transaction.delete(this.ctx.service.settleBills.tableName, { settle_id: settle.id });
+            await transaction.insert(this.ctx.service.settleBills.tableName, settleBills);
+            await transaction.delete(this.ctx.service.settlePos.tableName, { settle_id: settle.id });
+            await transaction.insert(this.ctx.service.settlePos.tableName, settlePos);
+            return settleSum;
         }
     }
 

+ 574 - 27
app/service/settle_audit.js

@@ -10,6 +10,9 @@
 
 const auditConst = require('../const/audit');
 const auditType = auditConst.auditType;
+const pushType = auditConst.pushType;
+const smsTypeConst = require('../const/sms_type');
+const wxConst = require('../const/wechat_template');
 
 module.exports = app => {
     class SettleAudit extends app.BaseService {
@@ -24,43 +27,44 @@ module.exports = app => {
             this.tableName = 'settle_audit';
         }
 
-        async getAuditors(settleId, times) {
-            return await this.getAllDataByCondition({ where: { settle_id: settleId, audit_times: times } }); // 全部参与的审批人
+        // ***** 查询审批人相关
+        // 获取全部参与人
+        async getAuditors(settleId, auditTimes) {
+            return await this.getAllDataByCondition({ where: { settle_id: settleId, audit_times: auditTimes } }); // 全部参与的审批人
         }
-
-        async getAuditorGroup(settleId, times) {
-            const auditors = await this.getAuditors(settleId, times); // 全部参与的审批人
+        // 获取全部参与人 分组
+        async getAuditorGroup(settleId, auditTimes) {
+            const auditors = await this.getAuditors(settleId, auditTimes); // 全部参与的审批人
             return this.ctx.helper.groupAuditors(auditors, 'active_order');
         }
-
+        // 获取全部参与人 去重
         async getUniqAuditors(settle) {
             const auditors = await this.getAuditors(settle.id, settle.audit_times); // 全部参与的审批人
             const result = [];
             auditors.forEach(x => {
-                if (result.findIndex(r => { return x.aid === r.aid && x.audit_order === x.audit_order; }) < 0) {
+                if (result.findIndex(r => { return x.audit_id === r.audit_id && x.audit_order === x.audit_order; }) < 0) {
                     result.push(x);
                 }
             });
             return result;
         }
-
-        // 去重
-        async getUniqAuditorsGroup(settleId, times) {
-            const group = await this.getAuditorGroup(settleId, times);
+        // 获取全部参与人 分组 去重
+        async getUniqAuditorsGroup(settleId, auditTimes) {
+            const group = await this.getAuditorGroup(settleId, auditTimes);
             return this.ctx.helper.groupAuditorsUniq(group);
         }
-
-        async getAuditorsByStatus(settleId, status, times) {
-            const cur = await this.db.queryOne(`SELECT * From ${this.tableName} where settle_id = ? AND audit_times = ? AND audit_status = ? ORDER By audit_times DESC, audit_order DESC `, [settleId, times, status]);
+        // 获取某个状态的审批人
+        async getAuditorsByStatus(settleId, auditStatus, auditTimes) {
+            const cur = await this.db.queryOne(`SELECT * From ${this.tableName} where settle_id = ? AND audit_times = ? AND audit_status = ? ORDER By audit_times DESC, audit_order DESC `, [settleId, auditTimes, auditStatus]);
             if (!cur) return [];
 
-            return await this.getAllDataByCondition({ where: { settle_id: settleId, audit_times: times, audit_order: cur.audit_order}});
+            return await this.getAllDataByCondition({ where: { settle_id: settleId, audit_times: auditTimes, audit_order: cur.audit_order}});
         }
-
-        async getAuditorHistory(settleId, times, reverse = false) {
+        // 获取全部审批历史
+        async getAuditorHistory(settleId, auditTimes, reverse = false) {
             const history = [];
-            if (times >= 1) {
-                for (let i = 1; i <= times; i++) {
+            if (auditTimes >= 1) {
+                for (let i = 1; i <= auditTimes; i++) {
                     const auditors = await this.getAuditors(settleId, i);
                     const group = this.ctx.helper.groupAuditors(auditors);
                     const historyGroup = [];
@@ -71,19 +75,17 @@ module.exports = app => {
                             audit_type: g[0].audit_type, audit_order: g[0].audit_order,
                             auditors: g
                         };
-                        if (his.audit_type === auditType.key.common) {
-                            his.name = g[0].name;
-                        } else {
-                            his.name = this.ctx.helper.transFormToChinese(his.audit_order) + '审';
-                        }
                         his.is_final = his.audit_order === max_order;
+                        his.auditName = his.audit_order === 0 ? '原报' : (his.is_final ? '终审' : his.audit_order + '审');
+                        his.auditCnName = his.audit_order === 0 ? '原报' : (his.is_final ? '终审' : this.ctx.helper.transFormToChinese(his.audit_order) + '审');
+                        his.name = his.audit_type === auditType.key.common ? g[0].name : his.auditName;
                         let audit_time;
                         g.forEach(x => {
-                            if (x.status === auditConst.settle.status.checkSkip) return;
-                            if (!his.status || x.status === auditConst.settle.status.checking) his.audit_status = x.audit_status;
+                            if (x.audit_status === auditConst.settle.status.checkSkip) return;
+                            if (!his.audit_status || x.audit_status === auditConst.settle.status.checking) his.audit_status = x.audit_status;
                             if (x.audit_time && (!audit_time || x.audit_time > audit_time)) {
                                 audit_time = x.audit_time;
-                                if (his.status !== auditConst.settle.status.checking) his.audit_status = x.audit_status;
+                                if (his.audit_status !== auditConst.settle.status.checking) his.audit_status = x.audit_status;
                             }
                         });
                         if (audit_time) {
@@ -104,7 +106,125 @@ module.exports = app => {
             }
             return history;
         }
+        async getAllAuditors(tenderId) {
+            const sql =
+                'SELECT sa.audit_id, sa.tid FROM ' + this.tableName + ' sa' +
+                '  LEFT JOIN ' + this.ctx.service.tender.tableName + ' t On sa.tid = t.id' +
+                '  WHERE t.id = ?' +
+                '  GROUP BY sa.audit_id';
+            const sqlParam = [tenderId];
+            return this.db.query(sql, sqlParam);
+        }
+        // ***** 查询审批人相关
 
+        // ***** 修改审批人相关
+        /**
+         * 获取 最新审核顺序
+         *
+         * @param {Number} settleId - 期id
+         * @param {Number} auditTimes - 第几次审批
+         * @return {Promise<number>}
+         */
+        async getNewOrder(settleId, auditTimes = 1) {
+            const sql = `SELECT Max(active_order) As max_order, Max(audit_order) As max_audit_order FROM ${this.tableName} Where settle_id = ? and audit_times = ?`;
+            const result = await this.db.queryOne(sql, [settleId, auditTimes]);
+            return result && result.max_order ? [result.max_order + 1, result.max_audit_order + 1] : [1, 1];
+        }
+        /**
+         * 同步审核人order
+         * @param transaction - 事务
+         * @param {Number} settleId - 结算期id
+         * @param {Number} auditOrder - 审核顺序(实际顺序)
+         * @param {Number} auditTimes - 第几次审批
+         * @param {String} selfOperate - 操作符(+/-)
+         * @return {Promise<*>}
+         * @private
+         */
+        async _syncAuditOrder(transaction, settleId, auditOrder, auditTimes, selfOperate = '-') {
+            this.initSqlBuilder();
+            this.sqlBuilder.setAndWhere('settle_id', {
+                value: settleId,
+                operate: '=',
+            });
+            this.sqlBuilder.setAndWhere('active_order', {
+                value: auditOrder,
+                operate: '>=',
+            });
+            this.sqlBuilder.setAndWhere('audit_times', {
+                value: auditTimes,
+                operate: '=',
+            });
+            this.sqlBuilder.setUpdateData('active_order', {
+                value: 1,
+                selfOperate: selfOperate,
+            });
+            this.sqlBuilder.setUpdateData('audit_order', {
+                value: 1,
+                selfOperate: selfOperate,
+            });
+            const [sql, sqlParam] = this.sqlBuilder.build(this.tableName, 'update');
+            const data = await transaction.query(sql, sqlParam);
+
+            return data;
+        }
+        /**
+         * 新增审核人
+         *
+         * @param {Number} settleId - 期id
+         * @param {Number} auditor - 审核人
+         * @param {Number} auditTimes - 第几次审批
+         * @param {Number} is_gdzs - 是否固定终审
+         * @return {Promise<number>}
+         */
+        async addAuditor(settleId, auditor, auditTimes = 1, is_gdzs = 0) {
+            let [newOrder, newAuditOrder] = await this.getNewOrder(settleId, auditTimes);
+            // 判断是否存在固定终审,存在则newOrder - 1并使终审order+1
+            newOrder = is_gdzs === 1 ? newOrder - 1 : newOrder;
+            newAuditOrder = is_gdzs === 1 ? newAuditOrder - 1 : newAuditOrder;
+
+            const transaction = await this.db.beginTransaction();
+            try {
+                if (is_gdzs) await this._syncAuditOrder(transaction, settleId, newOrder, auditTimes, '+');
+                const data = {
+                    tid: this.ctx.tender.id, settle_id: settleId, audit_id: auditor.id,
+                    name: auditor.name, company: auditor.company, role: auditor.role, mobile: auditor.mobile,
+                    audit_times: auditTimes, active_order: newOrder, audit_status: auditConst.settle.status.uncheck,
+                    audit_order: newAuditOrder, audit_type: auditType.key.common,
+                };
+                const result = await transaction.insert(this.tableName, data);
+                await transaction.commit();
+                return result.effectRows = 1;
+            } catch (err) {
+                await transaction.rollback();
+                throw err;
+            }
+            return false;
+        }
+        /**
+         * 移除审核人
+         *
+         * @param {Number} settleId - 期id
+         * @param {Number} auditorId - 审核人id
+         * @param {Number} auditTimes - 第几次审批
+         * @return {Promise<boolean>}
+         */
+        async deleteAuditor(settleId, auditorId, auditTimes = 1) {
+            const transaction = await this.db.beginTransaction();
+            try {
+                const condition = { settle_id: settleId, audit_id: auditorId, audit_times: auditTimes };
+                const auditor = await this.getDataByCondition(condition);
+                if (!auditor) throw '审批人不存在';
+                // 移除整个流程的人
+                await transaction.delete(this.tableName, { settle_id: settleId, active_order: auditor.active_order, audit_times: auditTimes});
+                await this._syncAuditOrder(transaction, settleId, auditor.active_order, auditTimes);
+                await transaction.commit();
+            } catch (err) {
+                await transaction.rollback();
+                throw err;
+            }
+            return true;
+        }
+        // 拷贝上一期审批流程
         async copyPreAuditors(transaction, preSettle, newSettle) {
             const auditors = preSettle ? await this.getUniqAuditors(preSettle) : [];
             const newAuditors = [];
@@ -130,7 +250,434 @@ module.exports = app => {
             const result = await transaction.insert(this.tableName, newAuditors);
             if (result.affectedRows !== newAuditors.length) throw '初始化审批流程错误';
         }
+        // 固定审批流-更新
+        async updateNewAuditList(settle, newList) {
+            const newAuditsInfo = await this.ctx.service.projectAccount.getAllDataByCondition({ where: { id: newList.map(x => { return x.audit_id; })} });
+            const transaction = await this.db.beginTransaction();
+            try {
+                // 先删除旧的审批流,再添加新的
+                await transaction.query(`DELETE FROM ${this.tableName} where settle_id = ? and audit_times = ? and audit_order > 0`, [settle.id, settle.audit_times]);
+                // todo 协同
+                //await transaction.delete(this.ctx.service.settleAuditAss.tableName, { settle_id: settle.id, audit_times: settle.audit_times });
+                const newAuditors = [];
+                for (const auditor of newList) {
+                    const auditorInfo = newAuditsInfo.find(x => { return x.id === auditor.audit_id; });
+                    if (!auditorInfo) throw '配置的审批人不存在';
+                    newAuditors.push({
+                        tid: settle.tid, settle_id: settle.id, audit_id: auditor.audit_id,
+                        name: auditorInfo.name, company: auditorInfo.company, role: auditorInfo.role, mobile: auditorInfo.mobile,
+                        audit_times: settle.audit_times, active_order: auditor.audit_order, audit_status: auditConst.settle.status.uncheck,
+                        audit_type: auditor.audit_type, audit_order: auditor.audit_order,
+                    });
+                }
+                if(newAuditors.length > 0) await transaction.insert(this.tableName, newAuditors);
+                await transaction.commit();
+            } catch (err) {
+                await transaction.rollback();
+                throw err;
+            }
+        }
+        // 固定终审-更新
+        async updateLastAudit(settle, auditList, lastId) {
+            const lastUser = await this.ctx.service.projectAccount.getDataById(lastId);
+            const transaction = await this.db.beginTransaction();
+            try {
+                // 先判断auditList里的aid是否与lastId相同,相同则删除并重新更新order
+                const existAudit = auditList.find(x => { return x.audit_id === lastId });
+                let auditOrder = auditList.length > 0 ? auditList.reduce((rst, a) => { return Math.max(rst, a.active_order)}, 0) + 1 : 1; // 最大值 + 1
+                if (existAudit) {
+                    await transaction.delete(this.tableName, { settle_id: settle.id, audit_times: settle.audit_times, audit_id: lastId });
+                    const sameOrder = auditList.filter(x => { return x.active_order === existAudit.active_order });
+                    if (sameOrder.length === 1) {
+                        const updateData = [];
+                        auditList.forEach(x => {
+                            if (x.active_order <= existAudit.active_order) return;
+                            updateData.push({id: x.id, active_order: x.active_order - 1, audit_order: x.audit_order - 1});
+                        });
+                        if (updateData.length > 0) {
+                            await transaction.updateRows(updateData);
+                        }
+                        auditOrder = auditOrder - 1;
+                    }
+                }
+                // 添加终审
+                const newAuditor = {
+                    tid: settle.tid, settle_id: settle.id, audit_id: lastId,
+                    name: lastUser.name, company: lastUser.company, role: lastUser.role, mobile: lastUser.mobile,
+                    audit_times: settle.audit_times, active_order: auditOrder, audit_status: auditConst.settle.status.uncheck,
+                    audit_type: auditType.key.common, audit_order: auditOrder,
+                };
+                await transaction.insert(this.tableName, newAuditor);
+                await transaction.commit();
+            } catch (err) {
+                await transaction.rollback();
+                throw err;
+            }
+        }
+        // ***** 修改审批人相关
+
+        // ***** 审批操作
+        /**
+         * 用于添加推送所需的content内容
+         * @param {Number} pid 项目id
+         * @param {Number} tid 台账id
+         * @param {Number} sid 期id
+         * @param {Number} uid 审核人id
+         */
+        async _getNoticeContent(pid, tid, sid, uid, opinion = '') {
+            const noticeSql =
+                'SELECT * FROM (SELECT ' +
+                '  t.`id` As `tid`, t.`name`, s.`settle_order` as `order`, pa.`name` As `su_name`, pa.role As `su_role`' +
+                `  FROM (SELECT * FROM ${this.ctx.service.tender.tableName} WHERE id = ? ) As t` +
+                `  LEFT JOIN ${this.ctx.service.settle.tableName} As s On s.id =  ?` +
+                `  LEFT JOIN ${this.ctx.service.projectAccount.tableName} As pa ON pa.id = ?` +
+                '  WHERE  t.`project_id` = ? ) as new_t GROUP BY new_t.`tid`';
+            const noticeSqlParam = [tid, sid, uid, pid];
+            const content = await this.db.query(noticeSql, noticeSqlParam);
+            if (content.length) {
+                content[0].opinion = opinion;
+            }
+            return content.length ? JSON.stringify(content[0]) : '';
+        }
+        async start(settle) {
+            const audits = await this.getAllDataByCondition({ where: { settle_id: settle.id, audit_times: settle.audit_times, audit_order: 1 } });
+            if (audits.length === 0) {
+                if(this.ctx.tender.info.shenpi.settle === shenpiConst.sp_status.gdspl) {
+                    throw '请联系管理员添加审批人';
+                } else {
+                    throw '请先选择审批人,再上报数据';
+                }
+            }
+
+            const transaction = await this.db.beginTransaction();
+            try {
+                const audit_time = new Date();
+                // 更新原报数据
+                await transaction.update(this.tableName, { audit_status: auditConst.settle.status.checked, audit_time }, { where: { settle_id: settle.id, audit_times: settle.audit_times, audit_order: 0 } });
+                // 更新下一审批人数据
+                const updateData = audits.map(x => { return { id: x.id, audit_status: auditConst.settle.status.checking } });
+                await transaction.updateRows(this.tableName, updateData);
+                // 生成结算台账
+                const settleSum = await this.ctx.service.settle.doSettle(transaction, settle);
+                // todo 计算部分缓存汇总数据
+                await transaction.update(this.ctx.service.settle.tableName, {
+                    id: settle.id, audit_status: auditConst.settle.status.checking,
+                    ...settleSum,
+                });
+                // 多人协同,取消下一审批人存在的锁定
+                // await this.ctx.service.settleAuditAss.cancelLock(settle, audits.map(x => { return x.audit_id; }), transaction);
+
+                // todo 添加短信通知-需要审批提醒功能
+                const shenpiUrl = await this.ctx.helper.urlToShort(this.ctx.protocol + '://' + this.ctx.host + '/wap/tender/' + settle.tid + '/settle/' + settle.settle_order);
+                const auditIds = this._.map(audits, 'audit_id');
+                const users = this._.map(settle.auditAssists.filter(x => { return auditIds.indexOf(x.user_id) >= 0; }), 'ass_user_id');
+                users.push(...auditIds);
+                // await this.ctx.helper.sendAliSms(users, smsTypeConst.const.JS, smsTypeConst.judge.approval.toString(), SmsAliConst.template.settle_check, {
+                //     qi: settle.audit_order,
+                //     code: shenpiUrl,
+                // });
+                // 微信模板通知
+                const wechatData = {
+                    wap_url: shenpiUrl,
+                    qi: settle.settle_order,
+                    status: wxConst.status.check,
+                    tips: wxConst.tips.check,
+                    code: this.ctx.session.sessionProject.code,
+                };
+                await this.ctx.helper.sendWechat(users, smsTypeConst.const.JS, smsTypeConst.judge.approval.toString(), wxConst.template.settle, wechatData);
+
+                // todo 上报/审批 - 检查三方特殊推送
+                // await this.ctx.service.specMsg.addSettleMsg(transaction, this.ctx.session.sessionProject.id, settle, pushOperate.settle.flow);
+                await transaction.commit();
+            } catch (err) {
+                await transaction.rollback();
+                throw err;
+            }
+        }
+        async _checked(settle, opinion) {
+            const accountId = this.ctx.session.sessionUser.accountId;
+            const time = new Date();
+            const selfAudit = settle.curAuditors.find(x => { return x.audit_id === accountId; });
+            if (!selfAudit) throw '当前标段您无权审批';
+            const nextAudits = await this.getAllDataByCondition({ where: { settle_id: settle.id, audit_times: settle.audit_times, active_order: selfAudit.active_order + 1 } });
+
+            const transaction = await this.db.beginTransaction();
+            try {
+                // 添加通知
+                const noticeContent = await this._getNoticeContent(this.ctx.session.sessionProject.id, settle.tid, settle.id, accountId, opinion);
+                const defaultNoticeRecord = {
+                    pid: this.ctx.session.sessionProject.id,
+                    type: pushType.settle,
+                    status: auditConst.settle.status.checked,
+                    content: noticeContent,
+                };
+                const records = settle.userIds.map(x => {
+                    return { uid: x, ...defaultNoticeRecord };
+                });
+                await transaction.insert('zh_notice', records);
+
+                // 更新本人审批状态
+                await transaction.update(this.tableName, {
+                    id: selfAudit.id,
+                    audit_status: auditConst.settle.status.checked, opinion: opinion,
+                    audit_time: time,
+                });
+                let auditStatus;
+                if (settle.curAuditors.length === 1 || selfAudit.audit_type !== auditType.key.and) {
+                    // 或签更新他人审批状态
+                    if (selfAudit.audit_type === auditType.key.or) {
+                        const updateOther = [];
+                        for (const audit of settle.curAuditors) {
+                            if (audit.audit_id === selfAudit.audit_id) continue;
+                            updateOther.push({
+                                id: audit.id,
+                                audit_status: auditConst.settle.status.checkSkip,
+                                opinion: '',
+                                audit_time: time,
+                            });
+                        }
+                        if (updateOther.length > 0) transaction.updateRows(this.tableName, updateOther);
+                    }
+                    // 无下一审核人表示,审核结束
+                    auditStatus = nextAudits.length > 0 ? auditConst.settle.status.checking : auditConst.settle.status.checked;
+                    if (nextAudits.length > 0) {
+                        // 流程至下一审批人
+                        const updateData = nextAudits.map(x => { return { id: x.id, audit_status: auditConst.settle.status.checking }; });
+                        await transaction.updateRows(this.tableName, updateData);
+                        // todo 多人协同,取消下一审批人存在的锁定
+                        // await this.ctx.service.settleAuditAss.cancelLock(settle, nextAudits.map(x => { return x.audit_id; }), transaction);
+
+                        // todo 添加短信通知-需要审批提醒功能
+                        const shenpiUrl = await this.ctx.helper.urlToShort(this.ctx.protocol + '://' + this.ctx.host + '/wap/tender/' + this.ctx.tender.id + '/settle/' + settle.settle_order);
+                        const nextAuditIds = this._.map(nextAudits, 'audit_id');
+                        const users = this._.map(this._.filter(this.ctx.auditAssists, function (x) { return nextAuditIds.indexOf(x.user_id) >= 0; }), 'ass_user_id');
+                        users.push(...nextAuditIds);
+                        // await this.ctx.helper.sendAliSms(users, smsTypeConst.const.JS, smsTypeConst.judge.approval.toString(), SmsAliConst.template.settle_check, {
+                        //     qi: settle.settle_order,
+                        //     code: shenpiUrl,
+                        // });
+                        // 微信模板通知
+                        const wechatData = {
+                            wap_url: shenpiUrl,
+                            qi: settle.settle_order,
+                            status: wxConst.status.check,
+                            tips: wxConst.tips.check,
+                            code: this.ctx.session.sessionProject.code,
+                        };
+                        await this.ctx.helper.sendWechat(users, smsTypeConst.const.JS, smsTypeConst.judge.approval.toString(), wxConst.template.settle, wechatData);
+                    } else {
+                        // 添加短信通知-审批通过提醒功能
+                        const users = this._.uniq(settle.userIds);
+                        // await this.ctx.helper.sendAliSms(users, smsTypeConst.const.JS, smsTypeConst.judge.result.toString(), SmsAliConst.template.settle_result, {
+                        //     qi: settle.settle_order,
+                        //     status: SmsAliConst.status.success,
+                        // });
 
+                        // 微信模板通知
+                        const shenpiUrl = await this.ctx.helper.urlToShort(this.ctx.protocol + '://' + this.ctx.host + '/wap/tender/' + this.ctx.tender.id + '/settle/' + settle.settle_order);
+                        const wechatData = {
+                            wap_url: shenpiUrl,
+                            qi: settle.settle_order,
+                            status: wxConst.status.success,
+                            tips: wxConst.tips.success,
+                            code: this.ctx.session.sessionProject.code,
+                        };
+                        await this.ctx.helper.sendWechat(users, smsTypeConst.const.JS, smsTypeConst.judge.result.toString(), wxConst.template.settle, wechatData);
+                        // todo 审批通过 - 检查三方特殊推送
+                        // await this.ctx.service.specMsg.addSettleMsg(transaction, this.ctx.session.sessionProject.id, settle, pushOperate.settle.checked);
+                    }
+                    // todo 上报/审批 - 检查三方特殊推送
+                    // await this.ctx.service.specMsg.addSettleMsg(transaction, this.ctx.session.sessionProject.id, settle, pushOperate.settle.flow);
+                } else {
+                    auditStatus = auditConst.settle.status.checking;
+                }
+                // 同步 期信息
+                await transaction.update(this.ctx.service.settle.tableName, { id: settle.id, audit_status: auditStatus });
+                await transaction.commit();
+            } catch (err) {
+                await transaction.rollback();
+                throw err;
+            }
+        }
+        async _checkNo(settle, opinion) {
+            const pid = this.ctx.session.sessionProject.id;
+            const accountId = this.ctx.session.sessionUser.accountId;
+            const time = new Date();
+            // 整理当前流程审核人状态更新
+            const selfAudit = settle.curAuditors.find(x => { return x.audit_id === accountId; });
+            if (!selfAudit) throw '当前标段您无权审批';
+            const auditors = await this.getUniqAuditors(settle); // 全部参与的审批人
+            const newAuditors = auditors.map(x => {
+                return {
+                    tid: settle.tid, settle_id: settle.id, audit_id: x.audit_id,
+                    audit_times: settle.audit_times + 1, audit_order: x.audit_order, audit_type: x.audit_type,
+                    active_order: x.audit_order, audit_status: auditConst.settle.status.uncheck,
+                    name: x.name, company: x.company, role: x.role, mobile: x.mobile,
+                }
+            });
+
+            const transaction = await this.db.beginTransaction();
+            try {
+                // 添加提醒
+                const noticeContent = await this._getNoticeContent(pid, selfAudit.tid, settle.id, selfAudit.audit_id, opinion);
+                const defaultNoticeRecord = { pid, type: pushType.settle, status: auditConst.settle.status.checkNo, content: noticeContent };
+                const records = settle.userIds.map(x => {
+                    return { uid: x, ...defaultNoticeRecord };
+                });
+                await transaction.insert(this.ctx.service.noticePush.tableName, records);
+
+                // 更新审批人状态数据
+                const updateData = [];
+                settle.curAuditors.forEach(x => {
+                    if (x.audit_id === selfAudit.audit_id) {
+                        updateData.push({ id: x.id, audit_status: auditConst.settle.status.checkNo, opinion: opinion, audit_time: time});
+                    } else {
+                        updateData.push({ id: x.id, audit_status: auditConst.settle.status.checkSkip, opinion: '', audit_time: null});
+                    }
+                });
+                await transaction.updateRows(this.tableName, updateData);
+                // 更新 期信息
+                await transaction.update(this.ctx.service.settle.tableName, {
+                    id: settle.id, audit_status: auditConst.settle.status.checkNo, audit_times: settle.audit_times + 1,
+                });
+                // 拷贝新一次审核流程列表
+                await transaction.insert(this.tableName, newAuditors);
+                // todo 锁定本人数据,保留锁定数据相关确认状态
+                // await this.ctx.service.settleAuditAss.lockConfirm4CheckNo(settle, curAuditorIds, auditors, transaction);
+
+                // todo 添加短信通知-审批退回提醒功能
+                const users = this._.uniq(settle.userIds);
+                // await this.ctx.helper.sendAliSms(users, smsTypeConst.const.JS, smsTypeConst.judge.result.toString(), SmsAliConst.template.settle_result, {
+                //     qi: settle.settle_order, status: SmsAliConst.status.back,
+                // });
+                // 微信模板通知
+                const shenpiUrl = await this.ctx.helper.urlToShort(this.ctx.protocol + '://' + this.ctx.host + '/wap/tender/' + settle.tid + '/settle/' + settle.settle_order);
+                const wechatData = {
+                    wap_url: shenpiUrl,
+                    qi: settle.settle_order,
+                    status: wxConst.status.back,
+                    tips: wxConst.tips.back,
+                    code: this.ctx.session.sessionProject.code,
+                };
+                await this.ctx.helper.sendWechat(users, smsTypeConst.const.JS, smsTypeConst.judge.result.toString(), wxConst.template.settle, wechatData);
+                // todo 上报/审批 - 检查三方特殊推送
+                // await this.ctx.service.specMsg.addSettleMsg(transaction, pid, settle, pushOperate.settle.flow);
+                await transaction.commit();
+            } catch (err) {
+                await transaction.rollback();
+                throw err;
+            }
+        }
+        async _checkNoPre(settle, opinion) {
+            const pid = this.ctx.session.sessionProject.id;
+            const accountId = this.ctx.session.sessionUser.accountId;
+            const time = new Date();
+            // 整理当前流程审核人状态更新
+            const selfAudit = settle.curAuditors.find(x => { return x.audit_id === accountId; });
+            if (!selfAudit) throw '当前标段您无权审批';
+            const preAuditors = settle.userGroups.find(x => { return x[0].audit_order === selfAudit.audit_order - 1; });
+
+            const transaction = await this.db.beginTransaction();
+            try {
+                // 添加通知
+                const noticeContent = await this._getNoticeContent(pid, settle.tid, settle.id, selfAudit.audit_id, opinion);
+                const defaultNoticeRecord = {
+                    pid, type: pushType.settle, status: auditConst.settle.status.checkNoPre, content: noticeContent,
+                };
+                const records = settle.userIds.map(x => {
+                    return { uid: x, ...defaultNoticeRecord };
+                });
+                await transaction.insert('zh_notice', records);
+
+                // 更新同一流程所有审批人状态
+                const updateData = [];
+                for (const audit of settle.curAuditors) {
+                    if (audit.audit_id === selfAudit.audit_id) {
+                        updateData.push({
+                            id: audit.id, audit_status: auditConst.settle.status.checkNoPre, opinion, audit_time: time,
+                        });
+                    } else {
+                        updateData.push({
+                            id: audit.id, audit_status: auditConst.settle.status.checkSkip, opinion: '', audit_time: null,
+                        });
+                    }
+                }
+                await transaction.updateRows(this.tableName, updateData);
+
+                // 顺移其后审核人流程顺序
+                const sql = 'UPDATE ' + this.tableName + ' SET `active_order` = `active_order` + 2 WHERE settle_id = ? AND audit_times = ? AND `active_order` > ?';
+                await transaction.query(sql, [settle.id, selfAudit.audit_times, selfAudit.active_order]);
+                // 上一审批人,当前审批人 再次添加至流程
+                const newAuditors = [];
+                preAuditors.forEach(x => {
+                    newAuditors.push({
+                        tid: x.tid, settle_id: x.settle_id, audit_id: x.audit_id,
+                        audit_times: x.audit_times, audit_order: selfAudit.audit_order - 1,
+                        audit_status: auditConst.settle.status.checking,
+                        audit_type: x.audit_type, active_order: selfAudit.active_order + 1,
+                        name: x.name, company: x.company, role: x.role, mobile: x.mobile,
+                    });
+                });
+                settle.flowAuditors.forEach(x => {
+                    newAuditors.push({
+                        tid: x.tid, settle_id: x.settle_id, audit_id: x.audit_id,
+                        audit_times: x.audit_times, active_order: selfAudit.active_order + 2,
+                        audit_status: auditConst.settle.status.uncheck,
+                        audit_type: x.audit_type, audit_order: x.audit_order,
+                        name: x.name, company: x.company, role: x.role, mobile: x.mobile,
+                    });
+                });
+                await transaction.insert(this.tableName, newAuditors);
+
+                const preAuditorIds = preAuditors.map(x => { return x.audit_id; });
+                // todo 锁定本人数据,保留锁定数据相关确认状态
+                // await this.ctx.service.settleAuditAss.lockConfirm4CheckNoPre(settle, settle.curAuditorIds, preAuditorIds, transaction);
+
+                // 同步 期信息
+                await transaction.update(this.ctx.service.settle.tableName, {
+                    id: settle.id, audit_status: auditConst.settle.status.checkNoPre,
+                });
+                const shenpiUrl = await this.ctx.helper.urlToShort(this.ctx.protocol + '://' + this.ctx.host + '/wap/tender/' + settle.tid + '/settle/' + settle.settle_order);
+                const users = this._.map(settle.auditAssists.filter(x => {return preAuditorIds.indexOf(x.user_id) >= 0 }), 'ass_user_id');
+                users.push(...preAuditorIds);
+                // await this.ctx.helper.sendAliSms(users, smsTypeConst.const.JS, smsTypeConst.judge.approval.toString(), SmsAliConst.template.settle_check, {
+                //     qi: settle.settle_order,
+                //     code: shenpiUrl,
+                // });
+                // 微信模板通知
+                const wechatData = {
+                    wap_url: shenpiUrl,
+                    qi: settle.settle_order,
+                    status: wxConst.status.check,
+                    tips: wxConst.tips.check,
+                    code: this.ctx.session.sessionProject.code,
+                };
+                await this.ctx.helper.sendWechat(users, smsTypeConst.const.JS, smsTypeConst.judge.approval.toString(), wxConst.template.settle, wechatData);
+
+                // todo 上报/审批 - 检查三方特殊推送
+                // await this.ctx.service.specMsg.addSettleMsg(transaction, pid, settle, pushOperate.settle.flow);
+                await transaction.commit();
+            } catch (err) {
+                await transaction.rollback();
+                throw err;
+            }
+        }
+        async check(settle, checkType, opinion = '') {
+            switch (checkType) {
+                case auditConst.settle.status.checked:
+                    await this._checked(settle, opinion);
+                    break;
+                case auditConst.settle.status.checkNo:
+                    await this._checkNo(settle, opinion);
+                    break;
+                case auditConst.settle.status.checkNoPre:
+                    await this._checkNoPre(settle, opinion);
+                    break;
+                default:
+                    throw '无效审批操作';
+            }
+        }
+        // ***** 审批操作
     }
 
     return SettleAudit;

+ 187 - 0
app/service/settle_audit_ass.js

@@ -0,0 +1,187 @@
+'use strict';
+
+/**
+ *  奖罚金
+ *
+ * @author Mai
+ * @date
+ * @version
+ */
+
+const auditConst = require('../const/audit');
+
+module.exports = app => {
+    class SettleAuditAss extends app.BaseService {
+        /**
+         * 构造函数
+         *
+         * @param {Object} ctx - egg全局变量
+         * @return {void}
+         */
+        constructor(ctx) {
+            super(ctx);
+            this.tableName = 'settle_audit_ass';
+        }
+
+        async getData(settle) {
+            const settleData = await this.getAllDataByCondition({ where: { settle_id: settle.id, audit_times: settle.audit_times } });
+            if (settle.status === auditConst.settle.status.checked) return settleData;
+
+            const result = [];
+            const data = await this.ctx.service.auditAss.getData(settle.tid);
+            for (const sd of settleData) {
+                const index = data.findIndex(x => { return x.user_id === sd.user_id && x.ass_user_id === sd.ass_user_id; });
+                if (index >= 0) {
+                    data.splice(index, 1);
+                    result.push(sd);
+                }
+            }
+            result.push(...data);
+            return result;
+        }
+
+        async getUserAssist(settle, user_id) {
+            const settleData = await this.getAllDataByCondition({ where: { settle_id: settle.id, audit_times: settle.audit_times, user_id } });
+            if (settle.status === auditConst.settle.status.checked) return settleData;
+
+            const result = [];
+            const data = await this.ctx.service.auditAss.getUserData(settle.tid, user_id);
+            for (const sd of settleData) {
+                const index = data.findIndex(x => { return x.user_id === sd.user_id && x.ass_user_id === sd.ass_user_id; });
+                if (index >= 0) {
+                    data.splice(index, 1);
+                    result.push(sd);
+                }
+            }
+            result.push(...data);
+            return result;
+        }
+
+        async updateData(data) {
+            if (!data.user_id || !data.ass_user_id || data.confirm === undefined) throw '缺少参数';
+            const settleData = await this.getDataByCondition({ settle_id: this.ctx.settle.id, audit_times: this.ctx.settle.audit_times, user_id: data.user_id, ass_user_id: data.ass_user_id });
+            if (data.confirm) {
+                if (settleData && settleData.confirm) throw '数据错误';
+                const auditAss = await this.ctx.service.auditAss.getDataByCondition({ tid: this.ctx.tender.id, user_id: data.user_id, ass_user_id: data.ass_user_id });
+                if (settleData) {
+                    await this.defaultUpdate({ id: settleData.id, confirm: 1, ass_ledger_id: auditAss.ass_ledger_id });
+                    settleData.confirm = 1;
+                    return settleData;
+                } else {
+                    const assInfo = await this.ctx.service.projectAccount.getDataById(data.ass_user_id);
+                    const insertData = {
+                        pid: this.ctx.session.sessionProject.id, tid: this.ctx.tender.id,
+                        settle_id: this.ctx.settle.id, audit_times: this.ctx.settle.audit_times,
+                        user_id: data.user_id, ass_user_id: data.ass_user_id, ass_ledger_id: auditAss.ass_ledger_id,
+                        name: assInfo.name, company: assInfo.company, role: assInfo.role,
+                        confirm: 1,
+                    };
+                    const result = await this.db.insert(this.tableName, insertData);
+                    insertData.id = result.insertId;
+                    return insertData;
+                }
+            } else {
+                if (!settleData && !settle.confirm) throw '数据错误';
+                await this.defaultUpdate({ id: settleData.id, confirm: 0, locked: 0 });
+                settleData.confirm = 0;
+                settleData.locked = 0;
+                return settleData;
+            }
+        }
+
+        async lockConfirm4CheckNoPre(settle, uid, pre_uid, transaction) {
+            if (!transaction) throw '缺少参数';
+
+            const locked = await this.getAllDataByCondition({ where: { settle_id: settle.id, locked: 1, audit_times: settle.curTimes } });
+            locked.forEach(x => {
+                x.locked_ledger_id = x.ass_ledger_id.split(',');
+            });
+            // 审批人数据锁定
+            const locking = await this.getAllDataByCondition({ where: { settle_id: settle.id, audit_times: settle.curTimes, user_id: uid, confirm: 1 } });
+            locking.forEach(x => {
+                x.locked_ledger_id = x.ass_ledger_id.split(',');
+            });
+            if (locking.length > 0) {
+                const updateLock = locking.map(x => { return { id: x.id, locked: 1 }; });
+                await transaction.updateRows(this.tableName, updateLock);
+            }
+            const preConfirm = await this.getAllDataByCondition({ where: { settle_id: settle.id, audit_times: settle.curTimes, user_id: pre_uid, confirm: 1 } });
+            // 前审批人数据,与被锁定数据相关数据确认状态保留
+            locked.push(...locking);
+            if (preConfirm.length > 0) {
+                const delConfirm = [];
+                for (const pc of preConfirm) {
+                    const bills = await this.ctx.service.ledger.getDataByCondition({ tender_id: settle.tid, ledger_id: pc.ledger_id });
+                    const billsOwner = bills ? bills.full_path.split('-') : [];
+                    const exist = locked.find(x => {
+                        for (const id of x.locked_ledger_id) {
+                            if (billsOwner.indexOf(id) >= 0) return true;
+                        }
+                        return false;
+                    });
+                    if (!exist) delConfirm.push(pc.id);
+                }
+                if (delConfirm.length > 0) await transaction.delete(this.tableName, { id: delConfirm });
+            }
+        }
+
+        async lockConfirm4CheckNo(settle, uid, auditors, transaction) {
+            if (!transaction) throw '缺少参数';
+            // 审批人数据锁定
+            const lockConfirm = await this.getAllDataByCondition({ where: { settle_id: settle.id, audit_times: settle.curTimes, user_id: uid, confirm: 1 } });
+            if (lockConfirm.length === 0) return;
+
+            const insertData = [];
+            lockConfirm.forEach(x => {
+                delete x.id;
+                x.locked = 1;
+                x.audit_times = x.audit_times + 1;
+                insertData.push(x);
+            });
+            // 审批人前所有角色,与锁定数据相关确认状态保留
+            const keepUid = [ settle.user_id ];
+            for (const a of auditors) {
+                if (a.aid === uid) break;
+                keepUid.push(a.aid);
+            }
+            const keepConfirm = await this.getAllDataByCondition({ where: { settle_id: settle.id, audit_times: settle.curTimes, user_id: keepUid, confirm: 1 } });
+            for (const kc of keepConfirm) {
+                const bills = await this.ctx.service.ledger.getDataByCondition({ tender_id: settle.tid, ledger_id: kc.ledger_id });
+                const billsOwner = bills ? bills.full_path.split('-') : [];
+                const exist = lockConfirm.find(x => {
+                    const lid = x.ass_ledger_id.split(',');
+                    for (const id of lid) {
+                        if (billsOwner.indexOf(id) >= 0) return true;
+                    }
+                    return false;
+                });
+                if (exist) {
+                    delete kc.id;
+                    kc.locked = 0;
+                    kc.audit_times = kc.audit_times + 1;
+                    insertData.push(kc);
+                }
+            }
+            if (insertData.length > 0) await transaction.insert(this.tableName, insertData);
+        }
+
+        async cancelLock(settle, uid, transaction) {
+            await transaction.update(this.tableName, { locked: 0 }, { where: { settle_id: settle.id, audit_times: settle.audit_times, user_id: uid } });
+        }
+
+        async getLockedId(settle) {
+            return await this.getAllDataByCondition({ where: { settle_id: settle.id, audit_times: settle.audit_times, locked: 1 } });
+        }
+
+        async getReportData(settle_id, audit_times) {
+            const sql = 'SELECT saa.tid, saa.settle_id, saa.audit_times, saa.user_id, saa.ass_ledger_id, saa.ass_user_id, saa.name, saa.company, saa.role, pa.sign_path, pa.stamp_path' +
+                `   FROM ${this.tableName} saa LEFT JOIN ${this.ctx.service.projectAccount.tableName} pa ON saa.ass_user_id = pa.id` +
+                `   WHERE saa.settle_id = ? AND saa.audit_times = ?`;
+            const result = await this.db.query(sql, [settle_id, audit_times]);
+            result.forEach(x => { x.ass_ledger_id = x.ass_ledger_id ? x.ass_ledger_id.split(',') : []});
+            return result;
+        }
+    }
+
+    return SettleAuditAss;
+};

+ 6 - 0
app/service/spec_msg.js

@@ -59,6 +59,12 @@ module.exports = app => {
             await transaction.insert(this.tableName, { pid, tid: stage.tid, sid: stage.id, timing });
         }
 
+        async addSettleMsg(transaction, pid, settle, timing) {
+            const needMsg = await this.tenderNeedMsg(pid, settle.tid, timing);
+            if (!needMsg) return;
+            await transaction.insert(this.tableName, { pid, tid: settle.tid, settle_id: settle.id, timing });
+        }
+
         async addChangeMsg(transaction, pid, change, timing) {
             const needMsg = await this.tenderNeedMsg(pid, change.tid, timing);
             if (!needMsg) return;

+ 1 - 0
app/service/stage_audit.js

@@ -22,6 +22,7 @@ const measureType = require('../const/tender').measureType;
 const RevisePrice = require('../lib/revise_price');
 const pushOperate = require('../const/spec_3f').pushOperate;
 
+
 module.exports = app => {
     class StageAudit extends app.BaseService {
         /**

+ 4 - 0
app/service/tender.js

@@ -139,6 +139,9 @@ module.exports = app => {
                     // 参与协审
                     '    OR (t.`ledger_status` = ' + auditConst.ledger.status.checked + ' AND ' +
                     '        t.id IN ( SELECT sa.`tid` FROM ?? As sa WHERE sa.`ass_user_id` = ? GROUP BY sa.`tid`))' +
+                    // 参与审批 结算期 的标段
+                    '    OR (t.`ledger_status` = ' + auditConst.ledger.status.checked + ' AND ' +
+                    '        t.id IN ( SELECT sa.`tid` FROM ?? As sa WHERE sa.`audit_id` = ? GROUP BY sa.`tid`))' +
                     // 参与审批 变更令 的标段
                     '    OR (t.`ledger_status` = ' + auditConst.ledger.status.checked + ' AND ' +
                     '        t.id IN ( SELECT ca.`tid` FROM ?? AS ca WHERE ca.`uid` = ? GROUP BY ca.`tid`))' +
@@ -160,6 +163,7 @@ module.exports = app => {
                     this.ctx.service.ledgerAudit.tableName, session.sessionUser.accountId,
                     this.ctx.service.stageAudit.tableName, session.sessionUser.accountId,
                     this.ctx.service.auditAss.tableName, session.sessionUser.accountId,
+                    this.ctx.service.settleAudit.tableName, session.sessionUser.accountId,
                     this.ctx.service.changeAudit.tableName, session.sessionUser.accountId,
                     this.ctx.service.reviseAudit.tableName, session.sessionUser.accountId,
                     this.ctx.service.materialAudit.tableName, session.sessionUser.accountId,

+ 867 - 0
app/view/settle/audit_modal.ejs

@@ -0,0 +1,867 @@
+<% if (ctx.settle && (ctx.settle.audit_status === auditConst.status.uncheck || ctx.settle.audit_status === auditConst.status.checkNo) && ctx.session.sessionUser.accountId === ctx.settle.user_id) { %>
+<!--上报审批-->
+<div class="modal fade" id="sub-sp" 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">
+                <div class="dropdown text-right">
+                    <% if (ctx.tender.info.shenpi.settle !== shenpiConst.sp_status.gdspl && ctx.session.sessionUser.accountId === ctx.settle.user_id) { %>
+                    <button class="btn btn-outline-primary btn-sm dropdown-toggle" type="button" id="dropdownMenuButton"
+                            data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
+                        添加审批流程
+                    </button>
+                    <div class="dropdown-menu dropdown-menu-right" aria-labelledby="dropdownMenuButton" style="width:220px">
+                        <div class="mb-2 p-2"><input class="form-control form-control-sm" placeholder="姓名/手机 检索"
+                                                     id="gr-search" autocomplete="off"></div>
+                        <dl class="list-unstyled book-list" id="book-list">
+                            <% accountGroup.forEach((group, idx) => { %>
+                            <dt><a href="javascript: void(0);" class="acc-btn" data-groupid="<%- idx %>"
+                                   data-type="hide"><i class="fa fa-plus-square"></i></a> <%- group.groupName %></dt>
+                            <div class="dd-content" data-toggleid="<%- idx %>">
+                                <% group.groupList.forEach(item => { %>
+                                <% if (item.id !== ctx.session.sessionUser.accountId) { %>
+                                <dd class="border-bottom p-2 mb-0 " data-id="<%- item.id %>">
+                                    <p class="mb-0 d-flex"><span class="text-primary"><%- item.name %></span><span
+                                                class="ml-auto"><%- item.mobile %></span></p>
+                                    <span class="text-muted"><%- item.role %></span>
+                                </dd>
+                                <% } %>
+                                <% });%>
+                            </div>
+                            <% }) %>
+                        </dl>
+                    </div>
+                    <% } %>
+                </div>
+                <div class="card mt-3">
+                    <div class="card-header">
+                        审批流程
+                    </div>
+                    <div class="modal-height-500" style="overflow: auto">
+                        <ul class="list-group list-group-flush" id="auditors">
+                            <% for (let i = 0, iLen = ctx.settle.auditorGroups.length; i < iLen; i++) { %>
+                            <% if (ctx.settle.auditorGroups[i][0].audit_order === 0) continue; %>
+                            <li class="list-group-item d-flex" auditorId="<%- ctx.settle.auditorGroups[i][0].audit_id %>">
+                                <div class="col-auto"><%- ctx.settle.auditorGroups[i][0].audit_order %></div>
+                                <div class="col">
+                                    <% for (const auditor of ctx.settle.auditorGroups[i]) { %>
+                                    <div class="d-inline-block mx-1" auditorId="<%- auditor.audit_id %>">
+                                        <i class="fa fa-user text-muted"></i> <%- auditor.name %> <small class="text-muted"><%- auditor.role %></small>
+                                    </div>
+                                    <% } %>
+                                </div>
+                                <div class="col-auto">
+                                    <% if (ctx.settle.auditorGroups[i][0].audit_type !== auditType.key.common) { %>
+                                    <span class="badge badge-pill badge-<%- auditType.info[ctx.settle.auditorGroups[i][0].audit_type].class %> badge-bg-small"><small><%- auditType.info[ctx.settle.auditorGroups[i][0].audit_type].long%></small></span>
+                                    <% } %>
+                                    <% if ((ctx.tender.info.shenpi.settle === shenpiConst.sp_status.sqspr ||
+                                                    (ctx.tender.info.shenpi.settle === shenpiConst.sp_status.gdzs && i+1 !== iLen)) && ctx.session.sessionUser.accountId === ctx.settle.user_id && !ctx.tender.isTourist) { %>
+                                    <a href="javascript: void(0)" class="text-danger pull-right">移除</a>
+                                    <% } %>
+                                </div>
+                            </li>
+                            <% } %>
+                        </ul>
+                    </div>
+                </div>
+            </div>
+            <form class="modal-footer" method="post" action="/tender/<%- ctx.tender.id %>/settle/<%- ctx.settle.settle_order %>/audit/start" name="settle-start">
+                <button type="button" class="btn btn-secondary btn-sm" data-dismiss="modal">关闭</button>
+                <input type="hidden" name="_csrf_j" value="<%= ctx.csrf %>">
+                <% if (ctx.session.sessionUser.accountId === ctx.settle.user_id) { %>
+                <button class="btn btn-primary btn-sm" type="submit">确认上报</button>
+                <% } %>
+            </form>
+        </div>
+    </div>
+</div>
+<% } %>
+<% if(ctx.settle && (ctx.settle.audit_status !== auditConst.status.uncheck)) { %>
+<!--审批流程/结果-->
+<div class="modal fade" id="sp-list" data-backdrop="static">
+    <div class="modal-dialog modal-lg" role="document">
+        <div class="modal-content">
+            <div class="modal-header">
+                <h5 class="modal-title">审批流程</h5>
+            </div>
+            <div class="modal-body">
+                <div class="row">
+                    <div class="col-4">
+                        <% if(ctx.settle.audit_status === auditConst.status.checkNo && ctx.session.sessionUser.accountId === ctx.settle.user_id) { %>
+                        <a class="sp-list-item" href="#sub-sp" data-toggle="modal" data-target="#sub-sp"
+                           id="hideSp">修改审批流程</a>
+                        <% } else if(ctx.settle.audit_status !== auditConst.status.checked && ctx.session.sessionUser.is_admin) { %>
+                        <a class="sp-list-item" href="#sub-sp2" data-toggle="modal" data-target="#sub-sp2"
+                           id="hideSp">修改审批流程</a>
+                        <% } %>
+                        <div class="card modal-height-500 mt-3" style="overflow: auto">
+                            <ul class="list-group list-group-flush auditors-list" id="auditors-list">
+                                <% ctx.settle.userGroups.forEach((item, idx) => { %>
+                                <% if (idx === 0) { %>
+                                <li class="list-group-item d-flex justify-content-between align-items-center">
+                                    <span class="mr-1"><i class="fa fa fa-play-circle fa-rotate-90"></i></span>
+                                    <span class="text-muted">
+                                        <% for (const u of item) { %>
+                                        <small class="d-inline-block text-dark mx-1" title="<%- u.role %>" data-auditorId="<%- u.audit_id %>"><%- u.name %></small>
+                                        <% } %>
+                                    </span>
+                                    <span class="badge badge-light badge-pill ml-auto"><small>原报</small></span>
+                                </li>
+                                <% } else if(idx === ctx.settle.userGroups.length -1 && idx !== 0) { %>
+                                <li class="list-group-item d-flex justify-content-between align-items-center">
+                                    <span class="mr-1"><i class="fa fa fa-stop-circle"></i></span>
+                                    <span class="text-muted">
+                                        <% for (const u of item) { %>
+                                        <small class="d-inline-block text-dark mx-1" title="<%- u.role %>" data-auditorId="<%- u.audit_id %>"><%- u.name %></small>
+                                        <% } %>
+                                    </span>
+                                    <div class="d-flex ml-auto">
+                                        <% if (item[0].audit_type !== auditType.key.common) { %>
+                                        <span class="badge badge-pill badge-<%-  auditType.info[item[0].audit_type].class %> p-1"><small><%- auditType.info[item[0].audit_type].short %></small></span>
+                                        <% } %>
+                                        <span class="badge badge-light badge-pill"><small>终审</small></span>
+                                    </div>
+                                </li>
+                                <% } else {%>
+                                <li class="list-group-item d-flex justify-content-between align-items-center">
+                                    <span class="mr-1"><i class="fa fa-chevron-circle-down"></i></span>
+                                    <span class="text-muted">
+                                        <% for (const u of item) { %>
+                                        <small class="d-inline-block text-dark mx-1" title="<%- u.role %>" data-auditorId="<%- u.audit_id %>"><%- u.name %></small>
+                                        <% } %>
+                                    </span>
+                                    <div class="d-flex ml-auto">
+                                        <% if (item[0].audit_type !== auditType.key.common) { %>
+                                        <span class="badge badge-pill badge-<%- auditType.info[item[0].audit_type].class %> p-1"><small><%- auditType.info[item[0].audit_type].short %></small></span>
+                                        <% } %>
+                                        <span class="badge badge-light badge-pill"><small><%= ctx.helper.transFormToChinese(idx) %>审</small></span>
+                                    </div>
+                                </li>
+                                <% } %>
+                                <% }) %>
+                            </ul>
+                        </div>
+                    </div>
+                    <div class="col-8 modal-height-500" style="overflow: auto">
+                        <% ctx.settle.auditHistory.forEach((his, idx) => { %>
+                        <!-- 展开/收起历史流程 -->
+                        <% if(idx === ctx.settle.auditHistory.length - 1 && ctx.settle.auditHistory.length !== 1) { %>
+                        <div class="text-right">
+                            <a href="javascript: void(0);" id="fold-btn" data-target="show" >展开历史审批流程</a>
+                        </div>
+                        <% } %>
+                        <div class="<%- idx < ctx.settle.auditHistory.length - 1 ? 'fold-card' : '' %>">
+                            <div class="text-center text-muted"><%- idx+1 %>#</div>
+                            <ul class="timeline-list list-unstyled mt-2 <% if (idx === ctx.settle.auditHistory.length - 1 && ctx.settle.auditHistory.length !== 1) { %>last-auditor-list<% } %>">
+                                <% his.forEach((group, index) => { %>
+                                <li class="timeline-list-item pb-2 <% if (group.audit_status === auditConst.status.uncheck && idx === ctx.settle.auditHistory.length - 1 && ctx.settle.auditHistory.length !== 1) { %>is_uncheck<% } %>">
+                                    <% if (group.auditYear) { %>
+                                    <div class="timeline-item-date">
+                                        <%- group.auditYear %>
+                                        <span><%- group.auditDate %></span>
+                                        <span><%- group.auditTime %></span>
+                                    </div>
+                                    <% } %>
+                                    <% if (index < his.length - 1) { %>
+                                    <div class="timeline-item-tail"></div>
+                                    <% } %>
+                                    <% if (group.audit_order === 0) { %>
+                                    <div class="timeline-item-icon bg-success text-light"><i class="fa fa-caret-down"></i></div>
+                                    <% } else if (group.audit_status === auditConst.status.checked) { %>
+                                    <div class="timeline-item-icon bg-success text-light"><i class="fa fa-check"></i></div>
+                                    <% } else if (group.audit_status === auditConst.status.checkNo || group.audit_status === auditConst.status.checkNoPre || group.audit_status === auditConst.status.checkCancel) { %>
+                                    <div class="timeline-item-icon bg-warning text-light"><i class="fa fa-level-up"></i></div>
+                                    <% } else if (group.audit_status === auditConst.status.checking) { %>
+                                    <div class="timeline-item-icon bg-warning text-light"><i class="fa fa-ellipsis-h"></i></div>
+                                    <% } else { %>
+                                    <div class="timeline-item-icon bg-secondary text-light"></div>
+                                    <% } %>
+                                    <div class="timeline-item-content">
+                                        <div class="py-1">
+                                            <span class="text-black-50">
+                                                <%- group.auditName %>
+                                                <% if (group.audit_type !== auditType.key.common) { %><span class="text-<%- auditType.info[group.audit_type].class %> "><%- auditType.info[group.audit_type].long %></span><% } %>
+                                            </span>
+                                            <% if (group.audit_order === 0) { %>
+                                            <span class="pull-right text-success"><%- idx !== 0 ? '重新' : '' %>上报审批</span>
+                                            <% } else if (group.audit_status !== auditConst.status.uncheck) { %>
+                                            <span class="pull-right <%- auditConst.statusClass[group.audit_status] %>"><%- auditConst.statusString[group.audit_status] %></span>
+                                            <% } %>
+                                        </div>
+                                        <div class="card">
+                                            <div class="card-body px-3 py-0">
+                                                <% for (const [i, auditor] of group.auditors.entries()) { %>
+                                                <div class="card-text p-2 py-3 row <%- ( i > 0 ? 'border-top' : '') %>">
+                                                    <div class="col">
+                                                        <span class="h6"><%- auditor.name %></span>
+                                                        <span class="text-muted ml-1"><%- auditor.role %></span>
+                                                    </div>
+                                                    <div class="col">
+                                                        <% if (auditor.audit_status === auditConst.status.checked) { %>
+                                                        <span class="pull-right text-success"><i class="fa fa-check-circle"></i></span>
+                                                        <% } if (auditor.audit_status === auditConst.status.checkNo || auditor.audit_status === auditConst.status.checkNoPre || auditor.audit_status === auditConst.status.checkCancel) { %>
+                                                        <span class="pull-right text-warning"><i class="fa fa-share-square fa-rotate-270"></i></span>
+                                                        <% } %>
+                                                    </div>
+                                                    <% if (auditor.opinion) { %>
+                                                    <div class="col-12 py-1 bg-light"><i class="fa fa-commenting-o mr-1"></i><%- auditor.opinion%></div>
+                                                    <% } %>
+                                                </div>
+                                                <% } %>
+                                            </div>
+                                        </div>
+                                    </div>
+                                </li>
+                                <% }) %>
+                            </ul>
+                        </div>
+                        <% }) %>
+                    </div>
+                </div>
+            </div>
+            <form class="modal-footer" method="post" action="audit/start" name="settle-start">
+                <input type="hidden" name="_csrf_j" value="<%= ctx.csrf %>">
+                <button type="button" class="btn btn-secondary btn-sm" data-dismiss="modal">关闭</button>
+                <% if(ctx.settle.audit_status === auditConst.status.checkNo && ctx.session.sessionUser.accountId === ctx.settle.user_id) { %>
+                <button class="btn btn-primary btn-sm sp-list-item" type="submit">确认上报</button>
+                <% } %>
+            </form>
+        </div>
+    </div>
+</div>
+<% } %>
+<% if (ctx.settle && (ctx.settle.audit_status === auditConst.status.checking || ctx.settle.audit_status === auditConst.status.checkNoPre) && ctx.settle.curAuditorIds.indexOf(ctx.session.sessionUser.accountId) >= 0) { %>
+<!--审批通过-->
+<div class="modal fade sp-location-list" id="sp-done" data-backdrop="static">
+    <div class="modal-dialog modal-lg" role="document">
+        <form class="modal-content" action="audit/check" method="post" id="audit-check">
+            <div class="modal-header">
+                <h5 class="modal-title">审批通过</h5>
+            </div>
+            <div class="modal-body">
+                <div class="row">
+                    <div class="col-4">
+                        <% if(ctx.settle.audit_status !== auditConst.status.checked && ctx.session.sessionUser.is_admin) { %>
+                        <a class="sp-list-item" href="#sub-sp2" data-toggle="modal" data-target="#sub-sp2" id="hideSp">修改审批流程</a>
+                        <% } %>
+                        <div class="card modal-height-500 mt-3" style="overflow: auto">
+                            <ul class="list-group list-group-flush auditors-list">
+                                <% ctx.settle.userGroups.forEach((item, idx) => { %>
+                                <li class="list-group-item d-flex justify-content-between align-items-center">
+                                    <% if (idx === 0) { %>
+                                    <span class="mr-1"><i class="fa fa fa-play-circle fa-rotate-90"></i></span>
+                                    <% } else if (idx === ctx.settle.userGroups.length -1 && idx !== 0) { %>
+                                    <span class="mr-1"><i class="fa fa fa-stop-circle"></i></span>
+                                    <% } else { %>
+                                    <span class="mr-1"><i class="fa fa-chevron-circle-down"></i></span>
+                                    <% } %>
+                                    <span class="text-muted">
+                                        <% for (const u of item) { %>
+                                        <small class="d-inline-block text-dark mx-1" title="<%- u.role %>" data-auditorId="<%- u.audit_id %>"><%- u.name %></small>
+                                        <% } %>
+                                    </span>
+                                    <div class="d-flex ml-auto">
+                                        <% if (item[0].audit_type !== auditType.key.common) { %>
+                                        <span class="badge badge-pill badge-<%-  auditType.info[item[0].audit_type].class %> p-1"><small><%- auditType.info[item[0].audit_type].short %></small></span>
+                                        <% } %>
+                                        <span class="badge badge-light badge-pill"><small><%- item.auditName %></small></span>
+                                    </div>
+                                </li>
+                                <% }) %>
+                            </ul>
+                        </div>
+                    </div>
+                    <div class="col-8 modal-height-500" style="overflow: auto">
+                        <% ctx.settle.auditHistory.forEach((his, idx) => { %>
+                        <!-- 展开/收起历史流程 -->
+                        <% if(idx === ctx.settle.auditHistory.length - 1 && ctx.settle.auditHistory.length !== 1) { %>
+                        <div class="text-right"><a href="javascript: void(0);" id="fold-btn" data-target="show">展开历史审批流程</a></div>
+                        <% } %>
+                        <div class="<%- idx < ctx.settle.auditHistory.length - 1 ? 'fold-card' : '' %>">
+                            <div class="text-center text-muted"><%- idx+1 %>#</div>
+                            <ul class="timeline-list list-unstyled mt-2 <% if (idx === ctx.settle.auditHistory.length - 1 && ctx.settle.auditHistory.length !== 1) { %>last-auditor-list<% } %>">
+                                <% his.forEach((group, index) => { %>
+                                <li class="timeline-list-item pb-2 <% if (group.audit_status === auditConst.status.uncheck && idx === ctx.settle.auditHistory.length - 1 && ctx.settle.auditHistory.length !== 1) { %>is_uncheck<% } %>">
+                                    <% if (group.auditYear) { %>
+                                    <div class="timeline-item-date">
+                                        <%- group.auditYear %>
+                                        <span><%- group.auditDate %></span>
+                                        <span><%- group.auditTime %></span>
+                                    </div>
+                                    <% } %>
+                                    <% if (index < his.length - 1) { %>
+                                    <div class="timeline-item-tail"></div>
+                                    <% } %>
+                                    <% if (group.audit_order === 0) { %>
+                                    <div class="timeline-item-icon bg-success text-light"><i class="fa fa-caret-down"></i></div>
+                                    <% } else if (group.audit_status === auditConst.status.checked) { %>
+                                    <div class="timeline-item-icon bg-success text-light"><i class="fa fa-check"></i></div>
+                                    <% } else if (group.audit_status === auditConst.status.checkNo || group.audit_status === auditConst.status.checkNoPre || group.audit_status === auditConst.status.checkCancel) { %>
+                                    <div class="timeline-item-icon bg-warning text-light"><i class="fa fa-level-up"></i></div>
+                                    <% } else if (group.audit_status === auditConst.status.checking) { %>
+                                    <div class="timeline-item-icon bg-warning text-light"><i class="fa fa-ellipsis-h"></i></div>
+                                    <% } else { %>
+                                    <div class="timeline-item-icon bg-secondary text-light"></div>
+                                    <% } %>
+                                    <div class="timeline-item-content">
+                                        <div class="py-1">
+                                            <span class="text-black-50">
+                                                <%- group.auditName %>
+                                                <% if (group.audit_type !== auditType.key.common) { %><span class="text-<%- auditType.info[group.audit_type].class %> "><%- auditType.info[group.audit_type].long %></span><% } %>
+                                            </span>
+                                            <% if (group.audit_order === 0) { %>
+                                            <span class="pull-right text-success"><%- idx !== 0 ? '重新' : '' %>上报审批</span>
+                                            <% } else if (group.audit_status !== auditConst.status.uncheck) { %>
+                                            <span class="pull-right <%- auditConst.statusClass[group.audit_status] %>"><%- auditConst.statusString[group.audit_status] %></span>
+                                            <% } %>
+                                        </div>
+                                        <div class="card">
+                                            <div class="card-body px-3 py-0">
+                                                <% for (const [i, auditor] of group.auditors.entries()) { %>
+                                                <div class="card-text p-2 py-3 row <%- ( i > 0 ? 'border-top' : '') %>">
+                                                    <div class="col">
+                                                        <span class="h6"><%- auditor.name %></span>
+                                                        <span class="text-muted ml-1"><%- auditor.role %></span>
+                                                    </div>
+                                                    <div class="col">
+                                                        <% if (auditor.audit_status === auditConst.status.checked) { %>
+                                                        <span class="pull-right text-success"><i class="fa fa-check-circle"></i></span>
+                                                        <% } if (auditor.audit_status === auditConst.status.checkNo || auditor.audit_status === auditConst.status.checkNoPre || auditor.audit_status === auditConst.status.checkCancel) { %>
+                                                        <span class="pull-right text-warning"><i class="fa fa-share-square fa-rotate-270"></i></span>
+                                                        <% } else if (auditor.audit_status === auditConst.status.checking) { %>
+                                                        <span class="pull-right text-warning"><i class="fa fa-commenting"></i></span>
+                                                        <% } %>
+                                                    </div>
+                                                    <% if (auditor.audit_status !== auditConst.status.uncheck && auditor.opinion) { %>
+                                                    <div class="col-12 py-1 bg-light"><i class="fa fa-commenting-o mr-1"></i><%- auditor.opinion%></div>
+                                                    <% } %>
+                                                    <% if (auditor.audit_status === auditConst.status.checking && auditor.audit_id === ctx.session.sessionUser.accountId) { %>
+                                                    <div class="col-12 py-1 bg-light">
+                                                        <textarea class="form-control form-control-sm" name="opinion">同意</textarea>
+                                                    </div>
+                                                    <% } %>
+                                                </div>
+                                                <% } %>
+                                            </div>
+                                        </div>
+                                    </div>
+                                </li>
+                                <% }) %>
+                            </ul>
+                        </div>
+                        <% }) %>
+                    </div>
+                </div>
+            </div>
+            <div class="modal-footer">
+                <button type="button" class="btn btn-secondary btn-sm" data-dismiss="modal">关闭</button>
+                <input type="hidden" name="_csrf_j" value="<%= ctx.csrf %>" />
+                <input type="hidden" name="checkType" value="<%= auditConst.status.checked %>" />
+                <button type="submit" class="btn btn-success btn-sm">确认通过</button>
+            </div>
+        </form>
+    </div>
+</div>
+<!--审批退回-->
+<div class="modal fade sp-location-list" id="sp-back" data-backdrop="static">
+    <div class="modal-dialog modal-lg" role="document">
+        <form class="modal-content modal-lg" action="audit/check" method="post" id="audit-check-no">
+            <div class="modal-header">
+                <h5 class="modal-title">审批退回</h5>
+            </div>
+            <div class="modal-body">
+                <div class="row">
+                    <div class="col-4">
+                        <% if(ctx.settle.audit_status !== auditConst.status.checked && ctx.session.sessionUser.is_admin) { %>
+                        <a class="sp-list-item" href="#sub-sp2" data-toggle="modal" data-target="#sub-sp2" id="hideSp">修改审批流程</a>
+                        <% } %>
+                        <div class="card modal-height-500 mt-3" style="overflow: auto">
+                            <ul class="list-group list-group-flush auditors-list">
+                                <% ctx.settle.userGroups.forEach((item, idx) => { %>
+                                <li class="list-group-item d-flex justify-content-between align-items-center">
+                                    <% if (idx === 0) { %>
+                                    <span class="mr-1"><i class="fa fa fa-play-circle fa-rotate-90"></i></span>
+                                    <% } else if (idx === ctx.settle.userGroups.length -1 && idx !== 0) { %>
+                                    <span class="mr-1"><i class="fa fa fa-stop-circle"></i></span>
+                                    <% } else { %>
+                                    <span class="mr-1"><i class="fa fa-chevron-circle-down"></i></span>
+                                    <% } %>
+                                    <span class="text-muted">
+                                        <% for (const u of item) { %>
+                                        <small class="d-inline-block text-dark mx-1" title="<%- u.role %>" data-auditorId="<%- u.audit_id %>"><%- u.name %></small>
+                                        <% } %>
+                                    </span>
+                                    <div class="d-flex ml-auto">
+                                        <% if (item[0].audit_type !== auditType.key.common) { %>
+                                        <span class="badge badge-pill badge-<%-  auditType.info[item[0].audit_type].class %> p-1"><small><%- auditType.info[item[0].audit_type].short %></small></span>
+                                        <% } %>
+                                        <span class="badge badge-light badge-pill"><small><%- item.auditName %></small></span>
+                                    </div>
+                                </li>
+                                <% }) %>
+                            </ul>
+                        </div>
+                    </div>
+                    <div class="col-8 modal-height-500" style="overflow: auto">
+                        <% ctx.settle.auditHistory.forEach((his, idx) => { %>
+                        <!-- 展开/收起历史流程 -->
+                        <% if(idx === ctx.settle.auditHistory.length - 1 && ctx.settle.auditHistory.length !== 1) { %>
+                        <div class="text-right"><a href="javascript: void(0);" id="fold-btn" data-target="show" data-idx="<%- idx + 1 %>">展开历史审批流程</a></div>
+                        <% } %>
+                        <div class="<%- idx < ctx.settle.auditHistory.length - 1 ? 'fold-card' : '' %>">
+                            <div class="text-center text-muted"><%- idx+1 %>#</div>
+                            <ul class="timeline-list list-unstyled mt-2 <% if (idx === ctx.settle.auditHistory.length - 1 && ctx.settle.auditHistory.length !== 1) { %>last-auditor-list<% } %>">
+                                <% his.forEach((group, index) => { %>
+                                <li class="timeline-list-item pb-2 <% if (group.audit_status === auditConst.status.uncheck && idx === ctx.settle.auditHistory.length - 1 && ctx.settle.auditHistory.length !== 1) { %>is_uncheck<% } %>">
+                                    <% if (group.auditYear) { %>
+                                    <div class="timeline-item-date">
+                                        <%- group.auditYear %>
+                                        <span><%- group.auditDate %></span>
+                                        <span><%- group.auditTime %></span>
+                                    </div>
+                                    <% } %>
+                                    <% if (index < his.length - 1) { %>
+                                    <div class="timeline-item-tail"></div>
+                                    <% } %>
+                                    <% if (group.audit_order === 0) { %>
+                                    <div class="timeline-item-icon bg-success text-light"><i class="fa fa-caret-down"></i></div>
+                                    <% } else if (group.audit_status === auditConst.status.checked) { %>
+                                    <div class="timeline-item-icon bg-success text-light"><i class="fa fa-check"></i></div>
+                                    <% } else if (group.audit_status === auditConst.status.checkNo || group.audit_status === auditConst.status.checkNoPre || group.audit_status === auditConst.status.checkCancel) { %>
+                                    <div class="timeline-item-icon bg-warning text-light"><i class="fa fa-level-up"></i></div>
+                                    <% } else if (group.audit_status === auditConst.status.checking) { %>
+                                    <div class="timeline-item-icon bg-warning text-light"><i class="fa fa-ellipsis-h"></i></div>
+                                    <% } else { %>
+                                    <div class="timeline-item-icon bg-secondary text-light"></div>
+                                    <% } %>
+                                    <div class="timeline-item-content">
+                                        <div class="py-1">
+                                            <span class="text-black-50">
+                                                <%- group.auditName %>
+                                                <% if (group.audit_type !== auditType.key.common) { %><span class="text-<%- auditType.info[group.audit_type].class %> "><%- auditType.info[group.audit_type].long %></span><% } %>
+                                            </span>
+                                            <% if (group.audit_order === 0) { %>
+                                            <span class="pull-right text-success"><%- idx !== 0 ? '重新' : '' %>上报审批</span>
+                                            <% } else if (group.audit_status !== auditConst.status.uncheck) { %>
+                                            <span class="pull-right <%- auditConst.statusClass[group.audit_status] %>"><%- auditConst.statusString[group.audit_status] %></span>
+                                            <% } %>
+                                        </div>
+                                        <div class="card">
+                                            <div class="card-body px-3 py-0">
+                                                <% for (const [i, auditor] of group.auditors.entries()) { %>
+                                                <div class="card-text p-2 py-3 row <%- ( i > 0 ? 'border-top' : '') %>">
+                                                    <div class="col">
+                                                        <span class="h6"><%- auditor.name %></span>
+                                                        <span class="text-muted ml-1"><%- auditor.role %></span>
+                                                    </div>
+                                                    <div class="col">
+                                                        <% if (auditor.audit_status === auditConst.status.checked) { %>
+                                                        <span class="pull-right text-success"><i class="fa fa-check-circle"></i></span>
+                                                        <% } if (auditor.audit_status === auditConst.status.checkNo || auditor.audit_status === auditConst.status.checkNoPre || auditor.audit_status === auditConst.status.checkCancel) { %>
+                                                        <span class="pull-right text-warning"><i class="fa fa-share-square fa-rotate-270"></i></span>
+                                                        <% } else if (auditor.audit_status === auditConst.status.checking) { %>
+                                                        <span class="pull-right text-warning"><i class="fa fa-commenting"></i></span>
+                                                        <% } %>
+                                                    </div>
+                                                    <% if (auditor.audit_status !== auditConst.status.uncheck && auditor.opinion) { %>
+                                                    <div class="col-12 py-1 bg-light"><i class="fa fa-commenting-o mr-1"></i><%- auditor.opinion%></div>
+                                                    <% } %>
+                                                    <% if (auditor.audit_status === auditConst.status.checking && auditor.audit_id === ctx.session.sessionUser.accountId) { %>
+                                                    <div class="col-12 py-1 bg-light">
+                                                        <textarea class="form-control form-control-sm" name="opinion">不同意</textarea>
+                                                        <div id="reject-process" class="alert alert-warning mt-1 mb-0 p-2">
+                                                            <div class="form-check form-check-inline">
+                                                                <input class="form-check-input" type="radio" name="checkType" id="inlineRadio1" value="<%- auditConst.status.checkNo %>">
+                                                                <label class="form-check-label" for="inlineRadio1">退回原报 <%- ctx.settle.user.name %></label>
+                                                            </div>
+                                                            <% if (auditor.audit_order > 1) { %>
+                                                            <div class="form-check form-check-inline">
+                                                                <input class="form-check-input" type="radio" name="checkType" id="inlineRadio2" value="<%- auditConst.status.checkNoPre %>">
+                                                                <label class="form-check-label" for="inlineRadio2">退回上一审批人
+                                                                    <% const pre = his.find(x => { return x.audit_order === auditor.audit_order - 1}); %>
+                                                                    <%- ( pre ? pre.name : '') %>
+                                                                </label>
+                                                            </div>
+                                                            <% } %>
+                                                        </div>
+                                                    </div>
+                                                    <% } %>
+                                                </div>
+                                                <% } %>
+                                            </div>
+                                        </div>
+                                    </div>
+                                </li>
+                                <% }) %>
+                            </ul>
+                        </div>
+                        <% }) %>
+                    </div>
+                </div>
+            </div>
+            <div class="modal-footer">
+                <button type="button" class="btn btn-secondary btn-sm" data-dismiss="modal">关闭</button>
+                <input type="hidden" name="_csrf_j" value="<%= ctx.csrf %>" />
+                <button type="submit" class="btn btn-warning btn-sm">确认退回</button>
+            </div>
+        </form>
+    </div>
+</div>
+<% } %>
+<% if (ctx.settle && ctx.settle.user_id === ctx.session.sessionUser.accountId && ctx.settle.settle_order === ctx.settle.highOrder && (ctx.settle.audit_status === auditConst.status.checkNo || ctx.settle.audit_status === auditConst.status.uncheck)) { %>
+<div class="modal fade" id="del-qi" data-backdrop="static">
+    <div class="modal-dialog" role="document">
+        <form class="modal-content" action='/tender/<%= ctx.tender.id %>/settle/delete' method="post">
+            <div class="modal-header">
+                <h5 class="modal-title">删除期</h5>
+            </div>
+            <div class="modal-body">
+                <h5>确认删除「第<%= ctx.settle.order %>期」?</h5>
+                <h5>删除后,数据无法恢复,请谨慎操作。</h5>
+            </div>
+            <div class="modal-footer">
+                <input type="hidden" name="stage_id" value="<%= ctx.settle.id %>">
+                <input type="hidden" name="_csrf_j" value="<%= ctx.csrf %>" />
+                <button type="button" class="btn btn-secondary btn-sm" data-dismiss="modal">取消</button>
+                <button type="submit" class="btn btn-danger btn-sm">确定删除</button>
+            </div>
+        </form>
+    </div>
+</div>
+<% } %>
+<% if (ctx.settle && ctx.settle.cancancel) { %>
+<div class="modal fade" id="sp-down-cancel" 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>确定撤回?</h5>
+            </div>
+            <div class="modal-footer">
+                <button type="button" class="btn btn-secondary btn-sm" data-dismiss="modal">取消</button>
+                <button type="submit" class="btn btn-danger btn-sm" id="cancel-shenpi-btn">确定撤回</button>
+            </div>
+        </div>
+    </div>
+</div>
+<% } %>
+<% if (ctx.settle && ctx.settle.audit_status !== auditConst.status.checked && ctx.session.sessionUser.is_admin) { %>
+<!--上报审批-->
+<div class="modal fade" id="sub-sp2" data-backdrop="static">
+    <div class="modal-dialog" style="max-width: 650px" role="document">
+        <div class="modal-content">
+            <div class="modal-header">
+                <h5 class="modal-title">修改审批流程</h5>
+            </div>
+            <div class="modal-body">
+                <div class="card mt-1">
+                    <div class="modal-height-500" style="overflow: auto">
+                        <style>
+                            #admin-edit-shenpi thead th {
+                                border-bottom: 0;
+                            }
+                            #admin-edit-shenpi td, #admin-edit-shenpi th {
+                                padding: 0.75rem;
+                            }
+                            #admin-edit-shenpi th {
+                                background: none;
+                                color: #212529;
+                                border-top: 0;
+                            }
+                        </style>
+                        <table class="table table-hover" id="admin-edit-shenpi">
+                            <thead>
+                            <tr class="card-header text-center">
+                                <th width="100px">审批流程</th>
+                                <th>审批人员</th>
+                                <th width="80" style="text-align: center">审批状态</th>
+                                <th width="200" style="text-align: center">操作</th>
+                            </tr>
+                            </thead>
+                            <tbody id="admin-edit-shenpi-list">
+                            <% for (const [i, group] of ctx.settle.userGroups.entries()) { %>
+                            <% if (i === 0) continue; %>
+                            <% for (const [j, auditor] of group.entries()) { %>
+                            <tr>
+                                <td class="text-left d-flex">
+                                    <% if (j === 0) { %>
+                                    <%- i + '审' %>
+                                    <% if (auditor.audit_type !== auditType.key.common) { %>
+                                    <span class="ml-2 badge badge-pill badge-<%-  auditType.info[auditor.audit_type].class %> p-1"><small><%- auditType.info[auditor.audit_type].short %></small></span>
+                                    <% } %>
+                                    <% } %>
+                                </td>
+                                <td></span> <%- auditor.name %> <small class="text-muted"><%- auditor.role %></small></td>
+                                <td style="text-align: center"><span class="<%- auditConst.auditStringClass[auditor.audit_status] %>"><%- auditor.audit_status !== auditConst.status.uncheck ? auditConst.auditString[auditor.audit_status] : '待审批'  %></span></td>
+                                <td style="text-align: center">
+                                    <% if (auditor.audit_status === auditConst.status.checking && j === group.length - 1) { %>
+                                    <span class="dropdown mr-2">
+                                    <a href="javascript: void(0)" class="add-audit" id="<%- auditor.audit_id %>_add_dropdownMenuButton" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">增加</a>
+                                    <div class="dropdown-menu dropdown-menu-right" id="<%- auditor.audit_id %>_add_dropdownMenu" aria-labelledby="<%- auditor.audit_id %>_add_dropdownMenuButton" style="width:220px">
+                                        <div class="mb-2 p-2"><input class="form-control form-control-sm gr-search"
+                                                                     placeholder="姓名/手机 检索" autocomplete="off" data-code="<%- auditor.audit_id %>_add"></div>
+                                        <dl class="list-unstyled book-list" data-aid="<%- auditor.audit_id %>" data-operate="add">
+                                            <% accountGroup.forEach((group, idx) => { %>
+                                            <dt><a href="javascript: void(0);" class="acc-btn" data-groupid="<%- idx %>"
+                                                   data-type="hide"><i class="fa fa-plus-square"></i></a> <%- group.groupName %></dt>
+                                                <div class="dd-content" data-toggleid="<%- idx %>">
+                                                    <% group.groupList.forEach(item => { %>
+                                                    <% if (item.id !== ctx.settle.user_id) { %>
+                                                    <dd class="border-bottom p-2 mb-0 " data-id="<%- item.id %>">
+                                                                <p class="mb-0 d-flex"><span class="text-primary"><%- item.name %></span><span
+                                                                            class="ml-auto"><%- item.mobile %></span></p>
+                                                                <span class="text-muted"><%- item.role %></span>
+                                                            </dd>
+                                                    <% } %>
+                                                    <% });%>
+                                                </div>
+                                            <% }) %>
+                                        </dl>
+                                    </div>
+                                    </span>
+                                    <% } %>
+                                    <% if (auditor.audit_status === auditConst.status.uncheck) { %>
+                                    <% if (j === group.length - 1) { %>
+                                    <span class="dropdown mr-2">
+                                    <a href="javascript: void(0)" class="add-audit" id="<%- auditor.audit_id %>_add_dropdownMenuButton" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">增加</a>
+                                        <div class="dropdown-menu dropdown-menu-right" id="<%- auditor.audit_id %>_add_dropdownMenu" aria-labelledby="<%- auditor.audit_id %>_add_dropdownMenuButton" style="width:220px">
+                                            <div class="mb-2 p-2"><input class="form-control form-control-sm gr-search"
+                                                                         placeholder="姓名/手机 检索" autocomplete="off" data-code="<%- auditor.audit_id %>_add"></div>
+                                            <dl class="list-unstyled book-list" data-aid="<%- auditor.audit_id %>" data-operate="add">
+                                                <% accountGroup.forEach((group, idx) => { %>
+                                                <dt><a href="javascript: void(0);" class="acc-btn" data-groupid="<%- idx %>"
+                                                       data-type="hide"><i class="fa fa-plus-square"></i></a> <%- group.groupName %></dt>
+                                                    <div class="dd-content" data-toggleid="<%- idx %>">
+                                                        <% group.groupList.forEach(item => { %>
+                                                        <% if (item.id !== ctx.settle.user_id) { %>
+                                                        <dd class="border-bottom p-2 mb-0 " data-id="<%- item.id %>">
+                                                                    <p class="mb-0 d-flex"><span class="text-primary"><%- item.name %></span><span
+                                                                                class="ml-auto"><%- item.mobile %></span></p>
+                                                                    <span class="text-muted"><%- item.role %></span>
+                                                                </dd>
+                                                        <% } %>
+                                                        <% });%>
+                                                    </div>
+                                                <% }) %>
+                                            </dl>
+                                        </div>
+                                    </span>
+                                    <% if (auditor.audit_type !== auditType.key.common) { %>
+                                    <span class="dropdown mr-2">
+                                    <a href="javascript: void(0)" class="add-audit" id="<%- auditor.audit_id %>_add-sibling_dropdownMenuButton" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">平级</a>
+                                        <div class="dropdown-menu dropdown-menu-right" id="<%- auditor.audit_id %>_add-sibling_dropdownMenu" aria-labelledby="<%- auditor.audit_id %>_add-sibling_dropdownMenuButton" style="width:220px">
+                                            <div class="mb-2 p-2"><input class="form-control form-control-sm gr-search"
+                                                                         placeholder="姓名/手机 检索" autocomplete="off" data-code="<%- auditor.audit_id %>_add-sibling"></div>
+                                            <dl class="list-unstyled book-list" data-aid="<%- auditor.audit_id %>" data-operate="add-sibling">
+                                                <% accountGroup.forEach((group, idx) => { %>
+                                                <dt><a href="javascript: void(0);" class="acc-btn" data-groupid="<%- idx %>"
+                                                       data-type="hide"><i class="fa fa-plus-square"></i></a> <%- group.groupName %></dt>
+                                                    <div class="dd-content" data-toggleid="<%- idx %>">
+                                                        <% group.groupList.forEach(item => { %>
+                                                        <% if (item.id !== ctx.settle.user_id) { %>
+                                                        <dd class="border-bottom p-2 mb-0 " data-id="<%- item.id %>">
+                                                                    <p class="mb-0 d-flex"><span class="text-primary"><%- item.name %></span><span
+                                                                                class="ml-auto"><%- item.mobile %></span></p>
+                                                                    <span class="text-muted"><%- item.role %></span>
+                                                                </dd>
+                                                        <% } %>
+                                                        <% });%>
+                                                    </div>
+                                                <% }) %>
+                                            </dl>
+                                        </div>
+                                    </span>
+                                    <% } %>
+                                    <% } %>
+                                    <span class="dropdown mr-2">
+                                        <a href="javascript: void(0)" class="change-audit" id="<%- auditor.audit_id %>_change_dropdownMenuButton" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">更换</a>
+                                        <div class="dropdown-menu dropdown-menu-right" id="<%- auditor.audit_id %>_change_dropdownMenu" aria-labelledby="<%- auditor.audit_id %>_change_dropdownMenuButton" style="width:220px">
+                                            <div class="mb-2 p-2"><input class="form-control form-control-sm gr-search"
+                                                                         placeholder="姓名/手机 检索" autocomplete="off" data-code="<%- auditor.audit_id %>_change"></div>
+                                            <dl class="list-unstyled book-list" data-aid="<%- auditor.audit_id %>" data-operate="change">
+                                                <% accountGroup.forEach((group, idx) => { %>
+                                                <dt><a href="javascript: void(0);" class="acc-btn" data-groupid="<%- idx %>"
+                                                       data-type="hide"><i class="fa fa-plus-square"></i></a> <%- group.groupName %></dt>
+                                                    <div class="dd-content" data-toggleid="<%- idx %>">
+                                                        <% group.groupList.forEach(item => { %>
+                                                        <% if (item.id !== ctx.settle.user_id) { %>
+                                                        <dd class="border-bottom p-2 mb-0 " data-id="<%- item.id %>">
+                                                                    <p class="mb-0 d-flex"><span class="text-primary"><%- item.name %></span><span
+                                                                                class="ml-auto"><%- item.mobile %></span></p>
+                                                                    <span class="text-muted"><%- item.role %></span>
+                                                                </dd>
+                                                        <% } %>
+                                                        <% });%>
+                                                    </div>
+                                                <% }) %>
+                                            </dl>
+                                        </div>
+                                    </span>
+                                    <span class="dropdown">
+                                    <a href="javascript: void(0)" class="text-danger" title="移除" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">移除</a>
+                                    <div class="dropdown-menu">
+                                        <span class="dropdown-item" href="javascript:void(0);">确认移除审批人?</span>
+                                        <div class="dropdown-divider"></div>
+                                        <div class="px-2 py-1 text-center">
+                                            <button class="remove-audit btn btn-sm btn-danger" data-id="<%- auditor.audit_id %>">移除</button>
+                                            <button class="btn btn-sm btn-secondary">取消</button>
+                                        </div>
+                                    </div>
+                                    </span>
+                                    <% } %>
+                                </td>
+                            </tr>
+                            <% } %>
+                            <% } %>
+                            </tbody>
+                        </table>
+                    </div>
+                </div>
+            </div>
+            <form class="modal-footer">
+                <div class="mr-auto text-warning">
+                    <span class="mr-3">增加:后级审核人</span>
+                    <span class="">平级:会签/或签增加平级审核人</span>
+                </div>
+                <button type="button" class="btn btn-secondary btn-sm" data-dismiss="modal">关闭</button>
+            </form>
+        </div>
+    </div>
+</div>
+<% } %>
+<script type="text/javascript">
+    const csrf = '<%= ctx.csrf %>';
+    const authMobile = '<%= authMobile %>';
+    const auditType = JSON.parse('<%- JSON.stringify(auditType) %>');
+    const shenpi_status = <%- ctx.tender.info.shenpi.settle %>;
+    const shenpiConst =  JSON.parse('<%- JSON.stringify(shenpiConst) %>');
+    const accountGroup = JSON.parse(unescape('<%- escape(JSON.stringify(accountGroup)) %>'));
+    const accountList = JSON.parse(unescape('<%- escape(JSON.stringify(accountList)) %>'));
+    const cur_tenderid = parseInt('<%- ctx.tender.id %>');
+    const settle = parseInt('<%= ctx.settle.user_id %>');
+    const auditorList = JSON.parse(unescape('<%- escape(JSON.stringify(ctx.settle.userGroups || [])) %>'));
+</script>
+<script>
+    $('[name=settle-start]').submit(function (e) {
+        if (checkAuditorFrom()) {
+            $(this).parent().parent().parent().modal('hide');
+            $('#hide-all').hide();
+        } else {
+            return false;
+        }
+    });
+    $('#audit-check').submit(function (e) {
+        const checkType = parseInt($('[name=checkType]').val());
+        const data = {
+            opinion: $(`${'#sp-done'}`).find('[name=opinion]').val().replace(/\r\n/g, '<br/>').replace(/\n/g, '<br/>').replace(/\s/g, ' '),
+            checkType,
+        };
+        $('#sp-done').modal('hide');
+        checkType && dataChecker.checkAndPost(this.action, data);
+        $('#hide-all').hide();
+        return false;
+    });
+    $('#audit-check-no').submit(function (e) {
+        const checkType = parseInt($('[name=checkType]:checked').val());
+        if ($('#warning-text').length) $('#warning-text').remove();
+        if (!checkType && !$('#warning-text').length) {
+            $('#reject-process').prepend('<p id="warning-text" style="color: red; margin: 0;">请选择退回流程</p>');
+            return false;
+        }
+        $('#hide-all').hide();
+        // const data = {
+        //     opinion: $(`${'#sp-back'}`).find('[name=opinion]').val().replace(/\r\n/g, '<br/>').replace(/\n/g, '<br/>').replace(/\s/g, ' '),
+        //     checkType,
+        // };
+    });
+
+    $('.sp-location-list').on('shown.bs.modal', function () {
+        const scrollBox = $(this).find('div[class="col-8 modal-height-500"]');
+        const bdiv = (scrollBox.offset() && scrollBox.offset().top) || 0;
+        scrollBox.scrollTop(0);
+        const hdiv = divSearch($(this).find('textarea')) ? $(this).find('textarea') : null;
+        const hdheight = hdiv ? hdiv.parents('.timeline-item-content').offset().top : null;
+        if (hdiv && scrollBox.length && scrollBox[0].scrollHeight > 200 && hdheight - bdiv > 200) {
+            scrollBox.scrollTop(hdheight - bdiv);
+        }
+    });
+    function divSearch(div) {
+        if (div.length > 0) {
+            return true;
+        }
+        return false;
+    }
+
+    // 展开历史审核记录
+    $('.modal-body #fold-btn').click(function () {
+        const type = $(this).data('target')
+        const auditCard = $(this).parent().parent()
+        if (type === 'show') {
+            $(this).data('target', 'hide')
+            auditCard.find('.fold-card').slideDown('swing', () => {
+                auditCard.find('#fold-btn').text('收起历史审核记录')
+            })
+        } else {
+            $(this).data('target', 'show')
+            auditCard.find('.fold-card').slideUp('swing', () => {
+                auditCard.find('#fold-btn').text('展开历史审核记录')
+            })
+        }
+    });
+
+    // 重新审批按钮
+    $("#re-shenpi-btn").click(function () {
+        const data = {
+        };
+        <% if (ctx.session.sessionUser.loginStatus === 0) { %>
+        const code = $("#sp-down-back input[name='code']").val();
+        if ($(this).hasClass('disabled')) {
+            return false;
+        }
+        if (code.length < 6) {
+            // alert('请填写正确的验证码');
+            toast('请填写正确的验证码', 'error');
+            return false;
+        }
+        data.code = code;
+        <% } %>
+        $.ajax({
+            url: '<%- preUrl %>/audit/check/again',
+            type: 'get',
+            data: data,
+            dataTye: 'json',
+            success: function (response) {
+                if (response.err === 0) {
+                    window.location.href = response.url;
+                } else {
+                    toast(response.msg, 'error');
+                }
+            }
+        });
+    });
+
+    <% if (ctx.settle && ctx.settle.cancancel) { %>
+    $("#cancel-shenpi-btn").click(function () {
+        const data = {
+        };
+        $.ajax({
+            url: '<%- preUrl %>/audit/check/cancel',
+            type: 'get',
+            data: data,
+            dataTye: 'json',
+            success: function (response) {
+                if (response.err === 0) {
+                    window.location.href = response.url;
+                } else {
+                    toast(response.msg, 'error');
+                }
+            }
+        });
+    });
+    <% } %>
+</script>

+ 2 - 1
app/view/settle/modal.ejs

@@ -1,2 +1,3 @@
 <% include ../shares/new_tag_modal.ejs %>
-<% include ../shares/upload_att.ejs %>
+<% include ../shares/upload_att.ejs %>
+<% include ./audit_modal.ejs %>

+ 1 - 0
app/view/settle/select_modal.ejs

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

+ 1 - 0
app/view/tender/shenpi.ejs

@@ -30,6 +30,7 @@
                                             <% } %>
                                         </div>
                                     </div>
+                                    <% if (!shenpi.sp_status_list[sp.status]) console.log(sp) %>
                                     <div class="alert alert-warning"><%- shenpi.sp_status_list[sp.status].name %>:<%- shenpi.sp_status_list[sp.status].msg %></div>
                                     <div class="lc-show">
                                     <% if (sp.status === shenpi.sp_status.gdspl) { %>

+ 2 - 0
config/web.js

@@ -1361,6 +1361,7 @@ const JsFiles = {
                     '/public/js/shares/sjs_setting.js',
                     '/public/js/zh_calc.js',
                     '/public/js/path_tree.js',
+                    '/public/js/shares/settle_audit.js',
                     '/public/js/settle_select.js',
 
                 ],
@@ -1385,6 +1386,7 @@ const JsFiles = {
                     '/public/js/shares/tools_att.js',
                     '/public/js/zh_calc.js',
                     '/public/js/path_tree.js',
+                    '/public/js/shares/settle_audit.js',
                     '/public/js/settle_ledger.js',
 
                 ],