MaiXinRong 2 месяцев назад
Родитель
Сommit
91850a128a
49 измененных файлов с 6472 добавлено и 64 удалено
  1. 1 0
      app/base/base_controller.js
  2. 4 0
      app/const/audit.js
  3. 512 0
      app/controller/cost_controller.js
  4. 15 0
      app/controller/sub_proj_controller.js
  5. 59 0
      app/middleware/cost_stage_check.js
  6. 163 0
      app/public/js/cost_stage.js
  7. 1193 0
      app/public/js/cost_stage_ledger.js
  8. 68 0
      app/public/js/cost_tender.js
  9. 2 1
      app/public/js/ledger.js
  10. 1 1
      app/public/js/revise.js
  11. 1 1
      app/public/js/settle_ledger.js
  12. 16 13
      app/public/js/shares/cs_tools.js
  13. 6 6
      app/public/js/shares/new_tag.js
  14. 30 7
      app/public/js/shares/tools_att.js
  15. 0 17
      app/public/js/spreadjs_rela/spreadjs_zh.js
  16. 1 1
      app/public/js/sr_detail.js
  17. 1 1
      app/public/js/stage.js
  18. 19 1
      app/router.js
  19. 305 0
      app/service/cost_stage.js
  20. 1190 0
      app/service/cost_stage_audit.js
  21. 232 0
      app/service/cost_stage_detail.js
  22. 94 0
      app/service/cost_stage_file.js
  23. 520 0
      app/service/cost_stage_ledger.js
  24. 95 0
      app/service/cost_stage_tag.js
  25. 9 1
      app/service/tender_permission.js
  26. 85 0
      app/view/cost/analysis_list.ejs
  27. 79 0
      app/view/cost/analysis_list_modal.ejs
  28. 2 0
      app/view/cost/analysis_menu_list.ejs
  29. 41 0
      app/view/cost/audit_btn.ejs
  30. 898 0
      app/view/cost/audit_modal.ejs
  31. 78 0
      app/view/cost/book_list.ejs
  32. 79 0
      app/view/cost/book_list_modal.ejs
  33. 2 0
      app/view/cost/book_menu_list.ejs
  34. 90 0
      app/view/cost/ledger.ejs
  35. 92 0
      app/view/cost/ledger_list.ejs
  36. 68 0
      app/view/cost/ledger_list_modal.ejs
  37. 3 0
      app/view/cost/ledger_menu_list.ejs
  38. 4 0
      app/view/cost/ledger_modal.ejs
  39. 14 0
      app/view/cost/list_menu.ejs
  40. 6 0
      app/view/cost/list_menu_list.ejs
  41. 16 0
      app/view/cost/list_mini_menu.ejs
  42. 17 0
      app/view/cost/stage_memu.ejs
  43. 18 0
      app/view/cost/stage_mini_menu.ejs
  44. 27 0
      app/view/cost/tender.ejs
  45. 54 0
      app/view/cost/tender_modal.ejs
  46. 13 13
      app/view/phase_pay/index.ejs
  47. 8 0
      config/menu.js
  48. 58 1
      config/web.js
  49. 183 0
      sql/update.sql

+ 1 - 0
app/base/base_controller.js

@@ -52,6 +52,7 @@ class BaseController extends Controller {
             menuList.budget.children.find(item => item.msg === 'schedule').display = ctx.subProject.page_show.xxjd || false;
             menuList.budget.children.find(item => item.msg === 'schedule').display = ctx.subProject.page_show.xxjd || false;
             menuList.financial.display = ctx.subProject.page_show.openFinancial || false;
             menuList.financial.display = ctx.subProject.page_show.openFinancial || false;
             menuList.payment.display = ctx.subProject.page_show.openPayment || false;
             menuList.payment.display = ctx.subProject.page_show.openPayment || false;
+            menuList.cost.display = ctx.subProject.page_show.cost || false;
             for (const index in menuList) {
             for (const index in menuList) {
                 const im = menuList[index];
                 const im = menuList[index];
                 if (!im.url) {
                 if (!im.url) {

+ 4 - 0
app/const/audit.js

@@ -1535,6 +1535,9 @@ const pushType = {
     inspection: 13,
     inspection: 13,
     safeInspection: 14,
     safeInspection: 14,
     safeStage: 15,
     safeStage: 15,
+    costStageLedger: 16,
+    costStageBook: 17,
+    costStageAnalysis: 18,
 };
 };
 
 
 module.exports = {
 module.exports = {
@@ -1545,6 +1548,7 @@ module.exports = {
     stage,
     stage,
     phasePay,
     phasePay,
     safeStage: phasePay,
     safeStage: phasePay,
+    costStage: phasePay,
     settle,
     settle,
     revise,
     revise,
     material,
     material,

+ 512 - 0
app/controller/cost_controller.js

@@ -0,0 +1,512 @@
+'use strict';
+
+/**
+ *
+ *  成本管理
+ * @author Mai
+ * @date
+ * @version
+ */
+const audit = require('../const/audit');
+const shenpiConst = require('../const/shenpi');
+const moment = require('moment');
+const sendToWormhole = require('stream-wormhole');
+const fs = require('fs');
+const path = require('path');
+
+module.exports = app => {
+    class CostController extends app.BaseController {
+        constructor(ctx) {
+            super(ctx);
+            ctx.showProject = true;
+            // ctx.showTitle = true;
+        }
+
+        loadMenu(ctx) {
+            super.loadMenu(ctx);
+            // 虚拟menu,以保证标题显示正确
+            ctx.menu = {
+                name: '成本管理',
+                display: false,
+                caption: '成本管理',
+                controller: 'cost',
+            };
+        }
+
+        async tender(ctx) {
+            try {
+                if (!ctx.subProject.page_show.cost) throw '该功能已关闭';
+
+                const renderData = {
+                    jsFiles: this.app.jsFiles.common.concat(this.app.jsFiles.cost.tender),
+                };
+
+                const accountList = await ctx.service.projectAccount.getAllSubProjectAccount(ctx.subProject);
+                renderData.accountList = accountList;
+                const unitList = await ctx.service.constructionUnit.getAllDataByCondition({ where: { pid: ctx.session.sessionProject.id } });
+                const accountGroupList = unitList.map(item => {
+                    const groupList = accountList.filter(item1 => item1.company === item.name);
+                    return { groupName: item.name, groupList };
+                }).filter(x => { return x.groupList.length > 0; });
+                renderData.accountGroup = accountGroupList;
+                renderData.accountInfo = await ctx.service.projectAccount.getDataById(ctx.session.sessionUser.accountId);
+                renderData.tenderList = await ctx.service.tender.getSpecList(ctx.service.tenderPermission, 'cost', ctx.session.sessionUser.is_admin ? 'all' : '');
+                renderData.categoryData = await this.ctx.service.category.getAllCategory(this.ctx.subProject);
+                renderData.selfCategoryLevel = this.ctx.subProject.permission.self_category_level;
+                renderData.permissionConst = ctx.service.tenderPermission.partPermissionConst('cost');
+                renderData.permissionBlock = ctx.service.tenderPermission.partPermissionBlock('cost');
+
+                renderData.costLedgerTemplates = await ctx.app.mysql.select('zh_bills_template_list', { where: { sub_type: 1} });
+                renderData.costAnalysisTemplates = await ctx.app.mysql.select('zh_bills_template_list', { where: { sub_type: 2} });
+                renderData.costCalcTemplates = await ctx.service.calcTmpl.getAllTemplate(this.ctx.session.sessionProject.id, 'cost');
+                await this.layout('cost/tender.ejs', renderData, 'cost/tender_modal.ejs');
+            } catch (err) {
+                ctx.log(err);
+                ctx.postError(err, '无法查看成本管理数据');
+                ctx.redirect(`/sp/${ctx.subProject.id}/dashboard`);
+            }
+        }
+
+        async member(ctx) {
+            try {
+                const data = JSON.parse(ctx.request.body.data);
+                const result = await ctx.service.tenderPermission.getPartsPermission(data.tid, data.parts);
+                ctx.body = { err: 0, msg: '', data: result };
+            } catch (err) {
+                ctx.log(err);
+                ctx.ajaxErrorBody(err, '查询标段权限错误');
+            }
+        }
+        async memberSave(ctx) {
+            try {
+                const data = JSON.parse(ctx.request.body.data);
+                await ctx.service.tenderPermission.savePermission(data.tid, data.member, data.permissionBlock);
+                ctx.body = { err: 0, msg: '', data: '' };
+            } catch (err) {
+                ctx.log(err);
+                ctx.ajaxErrorBody(err, '保存标段权限错误');
+            }
+        }
+
+        async ledger(ctx) {
+            try {
+                const stage_type = this.ctx.service.costStage.stageType.ledger.key;
+                const stages = await this.ctx.service.costStage.getAllStages(ctx.tender.id, stage_type, 'DESC');
+                for (const s of stages) {
+                    if (s.audit_status !== audit.common.status.checked) await this.ctx.service.costStage.loadUser(s);
+                    s.can_del = (s.create_user_id === ctx.session.sessionUser.accountId || ctx.session.sessionUser.is_admin)
+                        && (s.audit_status === audit.common.status.uncheck || s.audit_status === audit.common.status.checkNo);
+                }
+                const renderData = {
+                    stage_type,
+                    auditType: audit.auditType,
+                    stages,
+                    auditConst: audit.common,
+                    jsFiles: this.app.jsFiles.common.concat(this.app.jsFiles.cost.cost_stage)
+                };
+                await this.layout('cost/ledger_list.ejs', renderData, 'cost/ledger_list_modal.ejs');
+            } catch(err) {
+                ctx.log(err);
+                ctx.redirect(ctx.request.header.referer);
+            }
+        }
+        async book(ctx) {
+            try {
+                const stage_type = this.ctx.service.costStage.stageType.book.key;
+                const stages = await this.ctx.service.costStage.getAllStages(ctx.tender.id, stage_type, 'DESC');
+                for (const s of stages) {
+                    if (s.audit_status !== audit.common.status.checked) await this.ctx.service.costStage.loadUser(s);
+                    s.can_del = (s.create_user_id === ctx.session.sessionUser.accountId || ctx.session.sessionUser.is_admin)
+                        && (s.audit_status === audit.common.status.uncheck || s.audit_status === audit.common.status.checkNo);
+                }
+                const renderData = {
+                    stage_type,
+                    auditType: audit.auditType,
+                    stages,
+                    auditConst: audit.common,
+                    jsFiles: this.app.jsFiles.common.concat(this.app.jsFiles.cost.cost_stage)
+                };
+                await this.layout('cost/book_list.ejs', renderData, 'cost/book_list_modal.ejs');
+            } catch(err) {
+                ctx.log(err);
+                ctx.redirect(ctx.request.header.referer);
+            }
+        }
+        async analysis(ctx) {
+            try {
+                const stage_type = this.ctx.service.costStage.stageType.analysis.key;
+                const stages = await this.ctx.service.costStage.getAllStages(ctx.tender.id, stage_type, 'DESC');
+                for (const s of stages) {
+                    if (s.audit_status !== audit.common.status.checked) await this.ctx.service.costStage.loadUser(s);
+                    s.can_del = (s.create_user_id === ctx.session.sessionUser.accountId || ctx.session.sessionUser.is_admin)
+                        && (s.audit_status === audit.common.status.uncheck || s.audit_status === audit.common.status.checkNo);
+                }
+                const renderData = {
+                    stage_type,
+                    auditType: audit.auditType,
+                    stages,
+                    auditConst: audit.common,
+                    jsFiles: this.app.jsFiles.common.concat(this.app.jsFiles.cost.cost_stage)
+                };
+                await this.layout('cost/ledger_list.ejs', renderData, 'cost/analysis_list_modal.ejs');
+            } catch(err) {
+                ctx.log(err);
+                ctx.redirect(ctx.request.header.referer);
+            }
+        }
+
+        async addStage(ctx) {
+            const stage_type = ctx.request.body.stage_type;
+            try {
+                if (!ctx.permission.cost[stage_type + '_add']) throw '您无权创建期';
+                const stage_date = ctx.request.body.date;
+                if (!stage_date) throw '请选择年月';
+
+                const stages = await ctx.service.costStage.getAllStages(ctx.tender.id, stage_type, 'DESC');
+                const unCompleteCount = stages.filter(s => { return s.status !== auditConst.common.status.checked; }).length;
+                if (unCompleteCount.length > 0) throw '最新一期未审批通过,请审批通过后再新增';
+
+                const newStage = await ctx.service.costStage.add(ctx.tender.id, stage_type, stage_date);
+                if (!newStage) throw '新增期失败';
+                ctx.redirect(`/sp/${ctx.subProject.id}/cost/tender/${ctx.tender.id}/${stage_type}/${newStage.stage_order}`);
+            } catch (err) {
+                ctx.log(err);
+                ctx.postError(err, '新增期失败');
+                ctx.redirect(`/sp/${ctx.subProject.id}/cost/tender/${ctx.tender.id}/${stage_type}`);
+            }
+        }
+        async delStage(ctx) {
+            try {
+                const stage_id = ctx.request.body.stage_id;
+                const stage = await ctx.service.costStage.getDataById(stage_id);
+                if (!stage) throw '删除的期不存在,请刷新页面';
+                if (!ctx.session.sessionUser.is_admin && stage.create_user_id !== ctx.session.sessionUser.accountId) throw '您无权删除本期';
+                // 获取最新的期数
+                const stages = await ctx.service.costStage.getAllStages(ctx.tender.id, stage.stage_type, 'DESC');
+                if (stage.id !== stages[0].id) throw '非最新一期,不可删除';
+
+                await ctx.service.costStage.delete(stage_id);
+                ctx.redirect(`/sp/${ctx.subProject.id}/cost/tender/${ctx.tender.id}/${stage.stage_type}`);
+            } catch (err) {
+                ctx.log(err);
+                ctx.postError(err, '删除期失败');
+                ctx.redirect(ctx.request.header.referer);
+            }
+        }
+        /**
+         * 期审批流程(POST)
+         * @param ctx
+         * @return {Promise<void>}
+         */
+        async loadAuditors(ctx) {
+            try {
+                const stageId = JSON.parse(ctx.request.body.data).id;
+                const stage = await ctx.service.costStage.get(stageId);
+                await ctx.service.costStage.loadUser(stage);
+                await ctx.service.costStage.loadAuditViewData(stage);
+                ctx.body = { err: 0, msg: '', data: stage };
+            } catch (error) {
+                ctx.log(error);
+                ctx.body = { err: 1, msg: error.toString(), data: null };
+            }
+        }
+
+        async _getStageAuditViewData(ctx) {
+            await this.ctx.service.costStage.loadAuditViewData(ctx.costStage);
+        }
+        async stage(ctx) {
+            const stageTypeInfo = ctx.service.costStage.stageType[ctx.costStage.stage_type];
+            try {
+                await this._getStageAuditViewData(ctx);
+                // 流程审批人相关数据
+                const accountList = await ctx.service.projectAccount.getAllSubProjectAccount(ctx.subProject);
+                const unitList = await ctx.service.constructionUnit.getAllDataByCondition({ where: { pid: ctx.session.sessionProject.id } });
+                const accountGroup = unitList.map(item => {
+                    const groupList = accountList.filter(item1 => item1.company === item.name);
+                    return { groupName: item.name, groupList };
+                }).filter(x => { return x.groupList.length > 0; });
+                // 是否已验证手机短信
+                const pa = await ctx.service.projectAccount.getDataById(ctx.session.sessionUser.accountId);
+                const renderData = {
+                    auditConst: audit.common,
+                    auditType: audit.auditType,
+                    accountList,
+                    accountGroup,
+                    shenpiConst,
+                    authMobile: pa.auth_mobile,
+                    jsFiles: this.app.jsFiles.common.concat(this.app.jsFiles.cost[`cost_stage_${stageTypeInfo.key}`]),
+                    shenpi_status: ctx.tender.info.shenpi[stageTypeInfo.shenpi_status] || 1,
+                };
+                await this.layout(`cost/${stageTypeInfo.key}.ejs`, renderData, `cost/${stageTypeInfo.key}_modal.ejs`);
+            } catch (err) {
+                ctx.log(err);
+                ctx.postError(err, '读取成本报审错误');
+                ctx.redirect(`/sp/${ctx.subProject.id}/cost/tender/${ctx.tender.id}/${stageTypeInfo.key}`);
+            }
+        }
+
+        async _ledgerLoad(ctx) {
+            try {
+                const data = JSON.parse(ctx.request.body.data);
+                const filter = data.filter.split(';');
+                const responseData = { err: 0, msg: '', data: {}, hpack: [] };
+                for (const f of filter) {
+                    switch (f) {
+                        case 'bills':
+                            responseData.data.bills = ctx.costStage.readOnly
+                                ? await ctx.service.costStageLedger.getReadData(ctx.costStage)
+                                : await ctx.service.costStageLedger.getEditData(ctx.costStage);
+                            break;
+                        case 'billsCompare':
+                            responseData.data[f] = await ctx.service.costStageLedger.getCompareData(ctx.costStage);
+                            break;
+                        case 'detail':
+                            responseData.data.detail =  ctx.costStage.readOnly
+                                ? await ctx.service.costStageDetail.getReadData(ctx.costStage)
+                                : await ctx.service.costStageDetail.getEditData(ctx.costStage);
+                            break;
+                        case 'detailCompare':
+                            // todo
+                            responseData.data.detailCompare = await ctx.service.costStageDetail.getCompareData(ctx.costStage);
+                            break;
+                        case 'auditFlow':
+                            responseData.data[f] = await ctx.service.costStageAudit.getViewFlow(ctx.costStage);
+                            break;
+                        case 'att':
+                            responseData.data[f] = await ctx.service.costStageFile.getData(ctx.costStage.id, 'DESC');
+                            break;
+                        case 'tags':
+                            responseData.data[f] = await ctx.service.costStageTag.getDatas(ctx.costStage.id);
+                            break;
+                        default:
+                            responseData.data[f] = [];
+                            break;
+                    }
+                }
+
+                ctx.body = responseData;
+            } catch (err) {
+                this.log(err);
+                ctx.body = { err: 1, msg: err.toString(), data: null };
+            }
+        }
+        async _bookLoad(ctx) {
+
+        }
+        async _analysisLoad(ctx) {
+
+        }
+        async stageLoad(ctx) {
+            const updateFun = `_${ctx.costStage.stage_type}Load`;
+            if (this[updateFun]) {
+                await this[updateFun](ctx);
+            } else {
+                ctx.ajaxErrorBody('未知期数据类型', '加载数据错误');
+            }
+        }
+
+        async _billsBase(stage, type, data) {
+            if (isNaN(data.id) || data.id <= 0) throw '数据错误';
+            if (type !== 'add') {
+                if (isNaN(data.count) || data.count <= 0) data.count = 1;
+            }
+            switch (type) {
+                case 'add':
+                    return await this.ctx.service.costStageLedger.addLedgerNode(stage, data.id, data.count);
+                case 'delete':
+                    return await this.ctx.service.costStageLedger.delete(stage.id, data.id, data.count);
+                case 'up-move':
+                    return await this.ctx.service.costStageLedger.upMoveNode(stage.id, data.id, data.count);
+                case 'down-move':
+                    return await this.ctx.service.costStageLedger.downMoveNode(stage.id, data.id, data.count);
+                case 'up-level':
+                    return await this.ctx.service.costStageLedger.upLevelNode(stage.id, data.id, data.count);
+                case 'down-level':
+                    return await this.ctx.service.costStageLedger.downLevelNode(stage.id, data.id, data.count);
+            }
+        }
+        async _ledgerUpdate(ctx) {
+            try {
+                const data = JSON.parse(ctx.request.body.data);
+                if (!data.target) throw '数据错误';
+                const responseData = { err: 0, msg: '', data: {} };
+
+                if (data.target === 'ledger') {
+                    if (!data.postType || !data.postData) throw '数据错误';
+                    switch (data.postType) {
+                        case 'add':
+                        case 'delete':
+                        case 'up-move':
+                        case 'down-move':
+                        case 'up-level':
+                        case 'down-level':
+                            responseData.data = await this._billsBase(ctx.costStage, data.postType, data.postData);
+                            break;
+                        case 'update':
+                            responseData.data = await this.ctx.service.costStageLedger.updateCalc(ctx.costStage, data.postData);
+                            break;
+                        default:
+                            throw '未知操作';
+                    }
+                } else if (data.target === 'detail') {
+                    responseData.data = await this.ctx.service.costStageDetail.updateDatas(data);
+                }
+                ctx.body = responseData;
+            } catch (err) {
+                this.log(err);
+                ctx.body = this.ajaxErrorBody(err, '数据错误');
+            }
+        }
+        async _bookUpdate(ctx) {
+            try {
+                const data = JSON.parse(ctx.request.body.data);
+                if (!data.postType || !data.postData) throw '数据错误';
+                const responseData = { err: 0, msg: '', data: {} };
+
+                switch (data.postType) {
+                    case 'update':
+                        responseData.data = await this.ctx.service.costStageBook.updateCalc(ctx.costStage, data.postData);
+                        break;
+                    default:
+                        throw '未知操作';
+                }
+                ctx.body = responseData;
+            } catch (err) {
+                this.log(err);
+                ctx.body = this.ajaxErrorBody(err, '数据错误');
+            }
+        }
+        async _analysisBillsBase(stage, type, data) {
+            if (isNaN(data.id) || data.id <= 0) throw '数据错误';
+            if (type !== 'add') {
+                if (isNaN(data.count) || data.count <= 0) data.count = 1;
+            }
+            switch (type) {
+                case 'add':
+                    return await this.ctx.service.costStageAnalysis.addSafeBillsNode(stage, data.id, data.count);
+                case 'delete':
+                    return await this.ctx.service.costStageAnalysis.delete(stage.id, data.id, data.count);
+                case 'up-move':
+                    return await this.ctx.service.costStageAnalysis.upMoveNode(stage.id, data.id, data.count);
+                case 'down-move':
+                    return await this.ctx.service.costStageAnalysis.downMoveNode(stage.id, data.id, data.count);
+                case 'up-level':
+                    return await this.ctx.service.costStageAnalysis.upLevelNode(stage.id, data.id, data.count);
+                case 'down-level':
+                    return await this.ctx.service.costStageAnalysis.downLevelNode(stage.id, data.id, data.count);
+            }
+        }
+        async _analysisUpdate(ctx) {
+            try {
+                const data = JSON.parse(ctx.request.body.data);
+                if (!data.postType || !data.postData) throw '数据错误';
+                const responseData = { err: 0, msg: '', data: {} };
+
+                switch (data.postType) {
+                    case 'add':
+                    case 'delete':
+                    case 'up-move':
+                    case 'down-move':
+                    case 'up-level':
+                    case 'down-level':
+                        responseData.data = await this._analysisBillsBase(ctx.costStage, data.postType, data.postData);
+                        break;
+                    case 'update':
+                        responseData.data = await this.ctx.service.costStageAnalysis.updateCalc(ctx.costStage, data.postData);
+                        break;
+                    default:
+                        throw '未知操作';
+                }
+                ctx.body = responseData;
+            } catch (err) {
+                this.log(err);
+                ctx.body = this.ajaxErrorBody(err, '数据错误');
+            }
+        }
+        async stageUpdate(ctx) {
+            const updateFun = `_${ctx.costStage.stage_type}Update`;
+            if (this[updateFun]) {
+                await this[updateFun](ctx);
+            } else {
+                ctx.ajaxErrorBody('未知期数据类型', '保存数据错误');
+            }
+        }
+
+        async uploadStageFile(ctx) {
+            let stream;
+            try {
+                const parts = ctx.multipart({ autoFields: true });
+
+                let index = 0;
+                const create_time = Date.parse(new Date()) / 1000;
+                let stream = await parts();
+                const user = await ctx.service.projectAccount.getDataById(ctx.session.sessionUser.accountId);
+                const rela_id = parts.field.rela_id;
+                const rela_sub_id = parts.field.rela_sub_id;
+
+                const uploadfiles = [];
+                while (stream !== undefined) {
+                    if (!stream.filename) throw '未发现上传文件!';
+
+                    const fileInfo = path.parse(stream.filename);
+                    const filepath = `app/public/upload/${ctx.costStage.tid}/costStage/${ctx.moment().format('YYYYMMDD')}/${create_time + '_' + index + fileInfo.ext}`;
+
+                    // 保存文件
+                    await ctx.app.fujianOss.put(ctx.app.config.fujianOssFolder + filepath, stream);
+                    await sendToWormhole(stream);
+
+                    // 插入到stage_pay对应的附件列表中
+                    uploadfiles.push({
+                        rela_id, rela_sub_id,
+                        filename: fileInfo.name,
+                        fileext: fileInfo.ext,
+                        filesize: Array.isArray(parts.field.size) ? parts.field.size[index] : parts.field.size,
+                        filepath,
+                    });
+                    ++index;
+                    if (Array.isArray(parts.field.size) && index < parts.field.size.length) {
+                        stream = await parts();
+                    } else {
+                        stream = undefined;
+                    }
+                }
+
+                const result = await ctx.service.costStageFile.addFiles(ctx.costStage, uploadfiles, user);
+                ctx.body = { err: 0, msg: '', data: result };
+            } catch (error) {
+                ctx.log(error);
+                // 失败需要消耗掉stream 以防卡死
+                if (stream) await sendToWormhole(stream);
+                ctx.body = this.ajaxErrorBody(error, '上传附件失败,请重试');
+            }
+        }
+        async deleteStageFile(ctx) {
+            try {
+                const data = JSON.parse(ctx.request.body.data);
+                if (!data && !data.id) throw '缺少参数';
+                const result = await ctx.service.costStageFile.delFiles(data.id);
+                ctx.body = { err: 0, msg: '', data: result };
+            } catch (error) {
+                ctx.log(error);
+                ctx.ajaxErrorBody(error, '删除附件失败');
+            }
+        }
+
+        async tag(ctx) {
+            try {
+                const isRelaUser = ctx.costStage.userIds.indexOf(this.ctx.session.sessionUser.accountId) >= 0;
+                const isValidTourist = ctx.permission.cost.view && ctx.tender.touristPermission.tag;
+                if (!isRelaUser && !isValidTourist) throw '您无权进行该操作';
+                const data = JSON.parse(ctx.request.body.data);
+                const result = await ctx.service.costStageTag.update(data);
+                ctx.body = { err: 0, msg: '', data: result };
+            } catch (err) {
+                console.log(err);
+                this.log(err);
+                ctx.body = this.ajaxErrorBody(err, '书签数据错误');
+            }
+        }
+    }
+
+    return CostController;
+};

+ 15 - 0
app/controller/sub_proj_controller.js

@@ -537,6 +537,21 @@ module.exports = app => {
             }
             }
         }
         }
 
 
+        async templateSet(ctx) {
+            try {
+                const data = JSON.parse(ctx.request.body.data);
+                const updateData = { id: ctx.subProject.id };
+                if (data.cost_ledger_template !== undefined) updateData.cost_ledger_template = data.cost_ledger_template;
+                if (data.cost_analysis_template !== undefined) updateData.cost_analysis_template = data.cost_analysis_template;
+                if (data.cost_calc_template !== undefined) updateData.cost_calc_template = data.cost_calc_template;
+                await ctx.service.subProject.defaultUpdate(updateData);
+                ctx.body = { err: 0, msg: '', data: null };
+            } catch(err) {
+                ctx.log(err);
+                ctx.ajaxErrorBody(err, '保存模板设置失败');
+            }
+        }
+
         async listInfo(ctx) {
         async listInfo(ctx) {
             try {
             try {
                 const renderData = {
                 const renderData = {

+ 59 - 0
app/middleware/cost_stage_check.js

@@ -0,0 +1,59 @@
+'use strict';
+
+/**
+ *
+ *
+ * @author Mai
+ * @date
+ * @version
+ */
+
+module.exports = options => {
+    /**
+     * 期校验 中间件
+     * 1. 读取期数据
+     * 2. 检验用户是否参与期(不校验具体权限)
+     *
+     * 写入ctx.costStage数据
+     * 其中:
+     * costStage.user: 创建人
+     * costStage.auditors: 审批人列表(退回原报时,加载上一流程)
+     * costStage.curAuditors: 当前审批人(未上报为空,审批通过 or 退回原报时,为空)
+     *
+     * costStage.readOnly: 登录人,是否可操作
+     * costStage.curTimes: 当前登录人,操作、查阅数据times
+     * costStage.curOrder: 当前登录人,操作、查阅数据order
+     *
+     * 该方法为通用方法,如需costStage其他数据,请在controller中查询
+     *
+     * @param {function} next - 中间件继续执行的方法
+     * @return {void}
+     */
+    return function* costStageCheck(next) {
+        try {
+            // 读取标段数据
+            const stageOrder = parseInt(this.params.sorder);
+            if (stageOrder <= 0) throw '您访问的期不存在';
+
+            const stageType = this.params.stype;
+            const costStage = yield this.service.costStage.getStageByOrder(this.tender.id, stageType, stageOrder);
+            if (!costStage) throw '期数据错误';
+
+            // 读取原报、审核人数据
+            yield this.service.costStage.doCheckStage(costStage);
+            costStage.latestOrder = yield this.service.costStage.count({ tid: this.tender.id });
+            costStage.isLatest = costStage.latestOrder === costStage.stage_order;
+            yield this.service.costStage.checkShenpi(costStage);
+            this.costStage = costStage;
+            yield next;
+        } catch (err) {
+            this.log(err);
+            if (this.helper.isAjax(this.request)) {
+                this.ajaxErrorBody(err, '读取期数据错误');
+            } else {
+                this.postError(err, '读取期数据错误');
+                this.redirect(this.request.headers.referer);
+            }
+        }
+    };
+};

+ 163 - 0
app/public/js/cost_stage.js

@@ -0,0 +1,163 @@
+'use strict';
+
+$(document).ready(() => {
+    $('#audit-list').on('click', 'a', 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('#end-target').text($(this).data('idx') + '#')
+                auditCard.find('#fold-btn').text('收起历史审核记录')
+            })
+        } else {
+            $(this).data('target', 'show')
+            auditCard.find('.fold-card').slideUp('swing', () => {
+                auditCard.find('#end-target').text('1#')
+                auditCard.find('#fold-btn').text('展开历史审核记录')
+            })
+        }
+    });
+
+    const getGroupAuditHtml = function (group) {
+        return group.map(u => { return `<small class="d-inline-block text-dark mx-1" title="${u.role}" data-auditorId="${u.aid}">${u.name}</small>`; }).join('');
+    };
+    const getAuditTypeHtml = function (type) {
+        if (type === auditType.key.common) return '';
+        return `<div class="li-subscript"><span class="badge badge-pill badge-${auditType.info[type].class} p-1 badge-bg-small"><small>${auditType.info[type].short}</small></span></div>`;
+    };
+    const getAuditTypeText = function (type) {
+        if (type === auditType.key.common) return '';
+        return `<span class="text-${auditType.info[type].class}">${auditType.info[type].long}</span>`;
+    };
+    const getAuditorsHtml = function (auditors) {
+        const auditorsHTML = [];
+        auditors.forEach((group, idx) => {
+            if (idx === 0) {
+                auditorsHTML.push(`<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">${getGroupAuditHtml(group)}</span>
+                <span class="badge badge-light badge-pill ml-auto"><small>原报</small></span>
+                </li>`);
+            } else if(idx === auditors.length -1 && idx !== 0) {
+                auditorsHTML.push(`<li class="list-group-item d-flex justify-content-between align-items-center">
+                    <span class="mr-1"><i class="fa fa fa-stop-circle fa-rotate-90"></i></span>
+                <span class="text-muted">${getGroupAuditHtml(group)}</span>
+                <div class="d-flex ml-auto">
+                ${getAuditTypeHtml(group[0].audit_type)}
+                <span class="badge badge-light badge-pill ml-auto"><small>终审</small></span>
+                </div>
+                </li>`);
+            } else {
+                auditorsHTML.push(`<li class="list-group-item d-flex justify-content-between align-items-center">
+                    <span class="mr-1"><i class="fa fa fa-chevron-circle-down"></i></span>
+                <span class="text-muted">${getGroupAuditHtml(group)}</span>
+                <div class="d-flex ml-auto">
+                ${getAuditTypeHtml(group[0].audit_type)}
+                <span class="badge badge-light badge-pill"><small>${transFormToChinese(idx)}审</small></span>
+                </div>
+                </li>`);
+            }
+        });
+        return auditorsHTML;
+    };
+    const getAuditHistroyHtml = function (auditHistory) {
+        const historyHTML = [];
+        auditHistory.forEach((his, idx) => {
+            if (idx === auditHistory.length - 1 && auditHistory.length !== 1) {
+                historyHTML.push(`<div class="text-right"><a href="javascript: void(0);" id="fold-btn" data-target="show">展开历史审批流程</a></div>`);
+            }
+            historyHTML.push(`<div class="${idx < auditHistory.length - 1 ? 'fold-card' : ''}">`);
+            historyHTML.push(`<div class="text-center text-muted">${idx+1}#</div>`);
+            historyHTML.push(`<ul class="timeline-list list-unstyled mt-2 ${ idx === auditHistory.length - 1 && auditHistory.length !== 1 ? 'last-auditor-list' : '' }">`);
+            his.forEach((group) => {
+                historyHTML.push(`<li class="timeline-list-item pb-2 ${ group.audit_status === auditConst.status.uncheck && idx === auditHistory.length - 1 && auditHistory.length !== 1 ? 'is_uncheck' : ''}">`);
+                if (group.auditYear) {
+                    historyHTML.push(`<div class="timeline-item-date">${group.auditYear}<span>${group.auditDate}</span><span>${group.auditTime}</span></div>`);
+                }
+                if (group.audit_order < his.length - 1) {
+                    historyHTML.push('<div class="timeline-item-tail"></div>');
+                }
+                if (group.audit_status === auditConst.status.checked) {
+                    historyHTML.push('<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.status === auditConst.status.checkCancel) {
+                    historyHTML.push('<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) {
+                    historyHTML.push('<div class="timeline-item-icon bg-warning text-light"><i class="fa fa-ellipsis-h"></i></div>');
+                } else {
+                    historyHTML.push('<div class="timeline-item-icon bg-secondary text-light"></div>');
+                }
+
+                historyHTML.push('<div class="timeline-item-content">');
+                if (group.audit_order > 0) {
+                    const statuStr = group.audit_status !== auditConst.status.uncheck ?
+                        `<span class="pull-right ${auditConst.info[group.audit_status].class}">${auditConst.info[group.audit_status].title}</span>` : '';
+                    historyHTML.push(`<div class="py-1">
+                        <span class="text-black-50">
+                        ${ !group.is_final ? group.audit_order + '' : '终' }审 ${getAuditTypeText(group.audit_type)}
+                        </span>
+                        ${statuStr}
+                    </div>`);
+                } else {
+                    historyHTML.push(` <div class="py-1">
+                        <span class="text-black-50">原报</span>
+                        <span class="pull-right text-success">${idx !== 0 ? '重新' : '' }上报审批</span>
+                    </div>`);
+                }
+                historyHTML.push('<div class="card"><div class="card-body px-3 py-0">');
+                for (const [i, auditor] of group.auditors.entries()) {
+                    historyHTML.push(`<div class="card-text p-2 py-3 row ${ ( i > 0 ? 'border-top' : '') }">`);
+                    let companyRolePart = '';
+                    if (group.audit_order !== 0) {
+                        const rolePart = auditor.role ? ' - ' + auditor.role : '';
+                        companyRolePart = `<span class="text-muted ml-1">${auditor.company}${rolePart}</span>`;
+                    }
+
+                    historyHTML.push(`<div class="col-10"><span class="h6">${auditor.name}</span>${companyRolePart}</div>`);
+                    historyHTML.push('<div class="col">');
+                    if (auditor.audit_status === auditConst.status.checked) {
+                        historyHTML.push('<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) {
+                        historyHTML.push('<span class="pull-right text-warning"><i class="fa fa-share-square fa-rotate-270"></i></span>');
+                    }
+                    historyHTML.push('</div>');
+                    if (group.audit_order > 0 && auditor.opinion) {
+                        historyHTML.push(`<div class="col-12 py-1 bg-light"><i class="fa fa-commenting-o mr-1"></i>${auditor.opinion}</div>`);
+                    }
+                    historyHTML.push('</div>');
+                }
+                historyHTML.push('</div></div>');
+                historyHTML.push('</div>');
+                historyHTML.push('</li>');
+            });
+            historyHTML.push('</div>');
+            historyHTML.push('</ul>');
+        });
+        return historyHTML.join('');
+    };
+    // 获取审批流程
+    $('a[data-target="#sp-list" ]').on('click', function () {
+        postData('stage/auditors', { order: $(this).attr('stage-order') }, function (result) {
+            $('#auditor-list').html(getAuditorsHtml(result.hisUserGroup));
+            $('#audit-list').html(getAuditHistroyHtml(result.auditHistory));
+        });
+    });
+
+
+    $.subMenu({
+        menu: '#sub-menu', miniMenu: '#sub-mini-menu', miniMenuList: '#mini-menu-list',
+        toMenu: '#to-menu', toMiniMenu: '#to-mini-menu',
+        key: 'menu.1.0.0',
+        miniHint: '#sub-mini-hint', hintKey: 'menu.hint.1.0.1',
+        callback: function (info) {
+            if (info.mini) {
+                $('.panel-title').addClass('fluid');
+                $('#sub-menu').removeClass('panel-sidebar');
+            } else {
+                $('.panel-title').removeClass('fluid');
+                $('#sub-menu').addClass('panel-sidebar');
+            }
+            autoFlashHeight();
+        }
+    });
+});

Разница между файлами не показана из-за своего большого размера
+ 1193 - 0
app/public/js/cost_stage_ledger.js


+ 68 - 0
app/public/js/cost_tender.js

@@ -0,0 +1,68 @@
+'use strict';
+
+const tenderListSpec = (function(){
+    function getTenderNodeHtml(node, arr, pid) {
+        const html = [];
+        html.push('<tr pid="' + pid + '">');
+        // 名称
+        html.push('<td style="min-width: 200px" class="in-' + node.level + '">');
+        if (node.cid) {
+            html.push('<span onselectstart="return false" style="{-moz-user-select:none}" class="fold-switch mr-1" title="收起" cid="'+ node.sort_id +'"><i class="fa fa-minus-square-o"></i></span> <i class="fa fa-folder-o"></i> ', node.name);
+        } else {
+            html.push('<span class="text-muted mr-2">');
+            html.push(arr.indexOf(node) === arr.length - 1 ? '└' : '├');
+            html.push('</span>');
+            //html.push('<a href="/tender/' + node.id + '">', node[c.field], '</a>');
+            html.push(`<a href="/sp/${spid}/cost/tender/${node.id}/ledger" name="name" style="min-width: 200px;word-break:break-all;" id="${node.id}">${node.name}</a>`);
+        }
+        html.push('</td>');
+
+        // 创建时间
+        html.push('<td style="width: 8%" class="text-center">');
+        html.push(node.create_time ? moment(node.create_time).format('YYYY-MM-DD HH:mm:ss') : '');
+        html.push('</td>');
+        // 设置
+        if (is_admin) {
+            html.push('<td style="width: 10%" class="text-center">');
+            if (!node.cid) {
+                html.push(`<a href="javascript:void(0);" data-toggle="modal" data-tid="${node.id}" class="btn btn-sm btn-outline-primary member-manage"> 成员管理 </a>`);
+            }
+            html.push('</td>');
+        }
+        html.push('</tr>');
+        return html.join('');
+    }
+    function getTenderTreeHeaderHtml() {
+        const html = [];
+        const left = $('#sub-menu').css('display') === 'none' ? 56 : 176;
+        html.push('<table class="table table-hover table-bordered">');
+        html.push('<thead style="position: sticky;left:'+ left +'px;top: 0;" class="text-center">', '<tr>');
+        html.push('<th style="min-width: 50%">',  '标段名称',  tenderListOrder.getOrderButton('name'), '</th>');
+        html.push('<th style="width: 15%">', '创建时间',  tenderListOrder.getOrderButton('create_time'), '</th>');
+        if (is_admin) {
+            html.push('<th style="width: 15%">', '操作', '</th>');
+        }
+        html.push('</tr>');
+        html.push('</thead>');
+        return html.join('');
+    }
+    return { getTenderNodeHtml, getTenderTreeHeaderHtml }
+})();
+
+$(document).ready(() => {
+    const memberPermission = MemberPermission();
+    $('body').on('click', '.member-manage', function() {
+        const tid = this.getAttribute('data-tid');
+        memberPermission.show({ data: { tid }, loadUrl: `/sp/${spid}/cost/member`, saveUrl: `/sp/${spid}/cost/memberSave`});
+    });
+    $('#set-template-ok').click(function() {
+        const data = {
+            cost_ledger_template: $('#cost_ledger_template').val(),
+            cost_analysis_template: $('#cost_analysis_template').val(),
+            cost_calc_template: $('#cost_calc_template').val(),
+        };
+        postData(`/sp/${spid}/cost/templateSet`, data, function(data) {
+            $('#set-template').modal('hide');
+        });
+    });
+});

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

@@ -470,7 +470,8 @@ $(document).ready(function() {
         relaPosSpread: posSpread,
         relaPosSpread: posSpread,
         updateUrl: window.location.pathname + '/tag',
         updateUrl: window.location.pathname + '/tag',
         afterModify: function (nodes) {
         afterModify: function (nodes) {
-            SpreadJsObj.repaintNodesRowHeader(ledgerSpread.getActiveSheet(), nodes);
+            ledgerSpread.getActiveSheet().repaint();
+            if (nodes[0].pos_id && posSpread) posSpread.getActiveSheet().repaint();
         },
         },
         afterLocated:  function (lid, pos_id) {
         afterLocated:  function (lid, pos_id) {
             posOperationObj.loadCurPosData();
             posOperationObj.loadCurPosData();

+ 1 - 1
app/public/js/revise.js

@@ -195,7 +195,7 @@ $(document).ready(() => {
         relaSpread: billsSpread,
         relaSpread: billsSpread,
         updateUrl: window.location.pathname + '/tag',
         updateUrl: window.location.pathname + '/tag',
         afterModify: function (nodes) {
         afterModify: function (nodes) {
-            SpreadJsObj.repaintNodesRowHeader(billsSpread.getActiveSheet(), nodes);
+            billsSheet.repaint();
         },
         },
         afterLocated:  function () {
         afterLocated:  function () {
             posSpreadObj.loadCurPosData();
             posSpreadObj.loadCurPosData();

+ 1 - 1
app/public/js/settle_ledger.js

@@ -201,7 +201,7 @@ $(document).ready(() => {
         key: 'lid',
         key: 'lid',
         treeId: 'tree_id',
         treeId: 'tree_id',
         afterModify: function (nodes) {
         afterModify: function (nodes) {
-            SpreadJsObj.repaintNodesRowHeader(slSheet, nodes);
+            slSheet.repaint();
         },
         },
         afterLocated:  function () {
         afterLocated:  function () {
             settlePosObj.loadCurPosData();
             settlePosObj.loadCurPosData();

+ 16 - 13
app/public/js/shares/cs_tools.js

@@ -1016,7 +1016,6 @@ const showSelectTab = function(select, spread, afterShow) {
         });
         });
         return {spread: resultSpread};
         return {spread: resultSpread};
     };
     };
-
     $.xmjSearch = function (setting) {
     $.xmjSearch = function (setting) {
         if (!setting.selector || !setting.searchSpread) return;
         if (!setting.selector || !setting.searchSpread) return;
         if (!setting.searchRangeStr) setting.searchRangeStr = '输入项目节编号、细目、计量单元查找';
         if (!setting.searchRangeStr) setting.searchRangeStr = '输入项目节编号、细目、计量单元查找';
@@ -1130,6 +1129,10 @@ const showSelectTab = function(select, spread, afterShow) {
         ];
         ];
         if (!setting.key) setting.key = 'id';
         if (!setting.key) setting.key = 'id';
         if (!setting.treeId) setting.treeId = 'ledger_id';
         if (!setting.treeId) setting.treeId = 'ledger_id';
+        if (!setting.user_id) setting.user_id = 'uid';
+        if (!setting.user_name) setting.user_name = 'u_name';
+        if (!setting.lid) setting.lid = 'lid';
+        if (!setting.pos_id) setting.pos_id = 'pos_id';
         const obj = $(setting.selector);
         const obj = $(setting.selector);
         const relaTree = setting.relaTree;
         const relaTree = setting.relaTree;
         const html = [], pageLength = 15;
         const html = [], pageLength = 15;
@@ -1218,12 +1221,12 @@ const showSelectTab = function(select, spread, afterShow) {
                 tagHtml.push((tag.node.code || '') + (tag.node.b_code || ''), ' / ', tag.node.name || '', posHint);
                 tagHtml.push((tag.node.code || '') + (tag.node.b_code || ''), ' / ', tag.node.name || '', posHint);
             }
             }
             if (tag.share) {
             if (tag.share) {
-                tagHtml.push(`<div class="pull-right"><i class="fa fa-users text-warning" data-toggle="tooltip" data-placement="bottom" title="" data-original-title="所有参与台账审批管理的用户都可以看到这条书签"></i> <span>${tag.u_name}</span></div>`);
+                tagHtml.push(`<div class="pull-right"><i class="fa fa-users text-warning" data-toggle="tooltip" data-placement="bottom" title="" data-original-title="所有参与台账审批管理的用户都可以看到这条书签"></i> <span>${tag[setting.user_name]}</span></div>`);
             }
             }
             tagHtml.push('<div class="pull-right edit-tag-btn">');
             tagHtml.push('<div class="pull-right edit-tag-btn">');
             const lid = tag.node ? tag.node[setting.treeId] : -1;
             const lid = tag.node ? tag.node[setting.treeId] : -1;
-            tagHtml.push(`<a class="mr-1" name="bills-tag-locate" href="javascript: void(0);" lid="${lid}" pos_id="${tag.pos_id}"><i class="fa fa-crosshairs"></i> 定位</a>`);
-            if (tag.uid === userID && !setting.readOnly) tagHtml.push(`<a href="javascript: void(0);" name="bills-tag-edit" tag-id="${tag.id}"><i class="fa fa-edit"></i> 编辑</a>`);
+            tagHtml.push(`<a class="mr-1" name="bills-tag-locate" href="javascript: void(0);" lid="${lid}" pos_id="${tag[setting.pos_id]}"><i class="fa fa-crosshairs"></i> 定位</a>`);
+            if (tag[setting.user_id] === userID && !setting.readOnly) tagHtml.push(`<a href="javascript: void(0);" name="bills-tag-edit" tag-id="${tag.id}"><i class="fa fa-edit"></i> 编辑</a>`);
             tagHtml.push('</div>');
             tagHtml.push('</div>');
             if (tag.node && relaTree) {
             if (tag.node && relaTree) {
                 const parents = relaTree.getAllParents(tag.node);
                 const parents = relaTree.getAllParents(tag.node);
@@ -1269,8 +1272,8 @@ const showSelectTab = function(select, spread, afterShow) {
         const refreshPosTagView = function(pos) {
         const refreshPosTagView = function(pos) {
             const posRange = pos instanceof Array ? pos : [pos];
             const posRange = pos instanceof Array ? pos : [pos];
             for (const p of posRange) {
             for (const p of posRange) {
-                const bi = billsIndexes[p.lid] || [];
-                const pi = bi.filter(x => { return x.pos_id === p.id; });
+                const bi = billsIndexes[p[setting.lid]] || [];
+                const pi = bi.filter(x => { return x[setting.pos_id] === p.id; });
 
 
                 for (const tag of pi) {
                 for (const tag of pi) {
                     refreshTagView(tag);
                     refreshTagView(tag);
@@ -1313,10 +1316,10 @@ const showSelectTab = function(select, spread, afterShow) {
         };
         };
 
 
         const _addToBillsIndex = function(data, isTop = false) {
         const _addToBillsIndex = function(data, isTop = false) {
-            let bi = billsIndexes[data.lid];
+            let bi = billsIndexes[data[setting.lid]];
             if (!bi) {
             if (!bi) {
                 bi = [];
                 bi = [];
-                billsIndexes[data.lid] = bi;
+                billsIndexes[data[setting.lid]] = bi;
             }
             }
             isTop ? bi.unshift(data) : bi.push(data);
             isTop ? bi.unshift(data) : bi.push(data);
         };
         };
@@ -1394,12 +1397,12 @@ const showSelectTab = function(select, spread, afterShow) {
         };
         };
         const getPosTagsColor = function (lid, pos_id) {
         const getPosTagsColor = function (lid, pos_id) {
             const billsTags = billsIndexes[lid] || [];
             const billsTags = billsIndexes[lid] || [];
-            const posTags = billsTags.filter(x => { return x.pos_id === pos_id; });
+            const posTags = billsTags.filter(x => { return x[setting.pos_id] === pos_id; });
             return posTags.length > 0 ? posTags.map(x => {return x.color}) : undefined;
             return posTags.length > 0 ? posTags.map(x => {return x.color}) : undefined;
         };
         };
         const getPosTagsInfo = function(lid, pos_id) {
         const getPosTagsInfo = function(lid, pos_id) {
             const billsTags = billsIndexes[lid] || [];
             const billsTags = billsIndexes[lid] || [];
-            const posTags = billsTags.filter(x => { return x.pos_id === pos_id; });
+            const posTags = billsTags.filter(x => { return x[setting.pos_id] === pos_id; });
             return posTags.length > 0 ? posTags.map(x => {
             return posTags.length > 0 ? posTags.map(x => {
                 const tagClass = classIndexes.find(tc => {return tc.color === x.color}) || {};
                 const tagClass = classIndexes.find(tc => {return tc.color === x.color}) || {};
                 return {color: x.color, comment: x.comment, tagClass: tagClass.tagClass};
                 return {color: x.color, comment: x.comment, tagClass: tagClass.tagClass};
@@ -1420,10 +1423,10 @@ const showSelectTab = function(select, spread, afterShow) {
         };
         };
         const afterDeletePos = function (pos) {
         const afterDeletePos = function (pos) {
             for (const p of pos) {
             for (const p of pos) {
-                const bi = billsIndexes[p.lid];
+                const bi = billsIndexes[p[setting.lid]];
                 if (!bi) continue;
                 if (!bi) continue;
 
 
-                const pi = bi.filter(x => { return x.pos_id === p.id; });
+                const pi = bi.filter(x => { return x[setting.pos_id] === p.id; });
                 for (const piTag of pi) {
                 for (const piTag of pi) {
                     const delTag = billsTags.find(x => {return x.id === piTag.id});
                     const delTag = billsTags.find(x => {return x.id === piTag.id});
                     billsTags.splice(billsTags.indexOf(delTag), 1);
                     billsTags.splice(billsTags.indexOf(delTag), 1);
@@ -1518,7 +1521,7 @@ const showSelectTab = function(select, spread, afterShow) {
         $('#bills-tag-search').bind('click', () => {searchTagsAndShow();});
         $('#bills-tag-search').bind('click', () => {searchTagsAndShow();});
         $('#bills-tag-keyword').bind('keydown', e => {if (e.keyCode === 13) searchTagsAndShow();});
         $('#bills-tag-keyword').bind('keydown', e => {if (e.keyCode === 13) searchTagsAndShow();});
 
 
-        return { loadDatas, updateDatasAndShow, show,
+        return { setting: setting, loadDatas, updateDatasAndShow, show,
             getBillsTagsColor, getBillsTagsInfo, getPosTagsColor, getPosTagsInfo,
             getBillsTagsColor, getBillsTagsInfo, getPosTagsColor, getPosTagsInfo,
             refreshBillsTagView, refreshPosTagView,
             refreshBillsTagView, refreshPosTagView,
             afterDeleteBills, afterDeletePos }
             afterDeleteBills, afterDeletePos }

+ 6 - 6
app/public/js/shares/new_tag.js

@@ -7,9 +7,9 @@ const newTag = function (setting) {
     const addTag = function (node, pos) {
     const addTag = function (node, pos) {
         relaNode = node;
         relaNode = node;
         relaPos = pos;
         relaPos = pos;
-        const code = (node.code || '') + (node.b_code || '');
+        const nodeInfo = (node.code || '') + (node.b_code || '') + (node.name ? ' / ' + node.name : '');
         const posInfo = pos ? ' - ' + pos.name : '';
         const posInfo = pos ? ' - ' + pos.name : '';
-        $('#addtag-info').html(code + ' / ' + node.name + posInfo);
+        $('#addtag-info').html(nodeInfo + posInfo);
         $('#addtag').modal('show');
         $('#addtag').modal('show');
         $('#addtag-content').val('');
         $('#addtag-content').val('');
     };
     };
@@ -21,20 +21,20 @@ const newTag = function (setting) {
         const data = {
         const data = {
             add: {
             add: {
                 color: $('.active[name=addtag-color]').attr('tag-color'),
                 color: $('.active[name=addtag-color]').attr('tag-color'),
-                lid: setting.key ? relaNode[setting.key] : relaNode.id,
-                pos_id: relaPos ? (setting.key ? relaPos[setting.key] : relaPos.id) : '',
                 share: $('#addtag-share')[0].checked,
                 share: $('#addtag-share')[0].checked,
                 comment: $('#addtag-content').val(),
                 comment: $('#addtag-content').val(),
             }
             }
         };
         };
+        data.add[billsTag.setting.lid] = setting.key ? relaNode[setting.key] : relaNode.id;
+        data.add[billsTag.setting.pos_id] = relaPos ? (setting.key ? relaPos[setting.key] : relaPos.id) : '';
         postData(window.location.pathname + '/tag', data, result => {
         postData(window.location.pathname + '/tag', data, result => {
             if (result.add) {
             if (result.add) {
                 result.add.node = relaNode;
                 result.add.node = relaNode;
                 result.add.pos = relaPos;
                 result.add.pos = relaPos;
             }
             }
             billsTag.updateDatasAndShow(result);
             billsTag.updateDatasAndShow(result);
-            SpreadJsObj.repaintNodesRowHeader(billsSheet, relaNode);
-            if (relaPos) SpreadJsObj.repaintNodesRowHeader(billsSheet, relaPos);
+            billsSheet.repaint();
+            posSheet.repaint();
             $('#addtag').modal('hide');
             $('#addtag').modal('hide');
         });
         });
     });
     });

+ 30 - 7
app/public/js/shares/tools_att.js

@@ -16,7 +16,7 @@
         const obj = $(setting.selector);
         const obj = $(setting.selector);
         const fileInfo = setting.fileInfo || { user_name: 'username', user_id: 'uid', create_time: 'in_time' };
         const fileInfo = setting.fileInfo || { user_name: 'username', user_id: 'uid', create_time: 'in_time' };
         const pageLength = 20;
         const pageLength = 20;
-        let curNode = null, curPage = 0;
+        let curNode = null, curSubNode = null, curPage = 0;
         let ctrlLength = 80;
         let ctrlLength = 80;
         if (setting.saveUrl) ctrlLength = ctrlLength + 15;
         if (setting.saveUrl) ctrlLength = ctrlLength + 15;
         if (setting.moveUrl) ctrlLength = ctrlLength + 15;
         if (setting.moveUrl) ctrlLength = ctrlLength + 15;
@@ -69,7 +69,7 @@
         autoFlashHeight();
         autoFlashHeight();
         $('#att-cur-button')[0].click();
         $('#att-cur-button')[0].click();
 
 
-        let allAtts = [], nodeIndexes = {};
+        let allAtts = [], nodeIndexes = {}, subNodeIndexes = {};
 
 
         const getNodeHint = function(node) {
         const getNodeHint = function(node) {
             return setting.getCurHint ? setting.getCurHint(node) : `${node.code || node.b_code || ''}/${node.name || ''}`;
             return setting.getCurHint ? setting.getCurHint(node) : `${node.code || node.b_code || ''}/${node.name || ''}`;
@@ -99,7 +99,7 @@
         };
         };
         const refreshCurAttHtml = function () {
         const refreshCurAttHtml = function () {
             const html = [];
             const html = [];
-            const atts = curNode ? (nodeIndexes[curNode[setting.key]]) || [] : [];
+            const atts = curSubNode ? (subNodeIndexes[curSubNode[setting.subKey]] || []) : (curNode ? (nodeIndexes[curNode[setting.key]]) || [] : []);
             for (const att of atts) {
             for (const att of atts) {
                 html.push(getAttHtml(att));
                 html.push(getAttHtml(att));
             }
             }
@@ -126,10 +126,16 @@
             $('#att-cur-page').text(curPage);
             $('#att-cur-page').text(curPage);
             refreshAllAttHtml();
             refreshAllAttHtml();
         };
         };
-        const getCurAttHtml = function (node) {
+        const getCurAttHtml = function (node, subNode, check) {
+            if (check && node === curNode && subNode === curSubNode) return;
             curNode = node;
             curNode = node;
+            curSubNode = subNode;
             if (curNode) {
             if (curNode) {
-                $('#att-cur-hint').text(getNodeHint(curNode));
+                if (subNode) {
+                    $('#att-cur-hint').text(getNodeHint(curNode)+ '(' + getNodeHint(subNode) + ')');
+                } else {
+                    $('#att-cur-hint').text(getNodeHint(curNode));
+                }
             } else {
             } else {
                 $('#att-cur-hint').text('');
                 $('#att-cur-hint').text('');
             }
             }
@@ -250,7 +256,7 @@
                 this.showMoveTree();
                 this.showMoveTree();
                 $('#move-att2').modal('show');
                 $('#move-att2').modal('show');
             },
             },
-            moveoNodeIndex: function(data) {
+            moveNodeIndex: function(data) {
                 const oxi = nodeIndexes[this.file[setting.masterKey]];
                 const oxi = nodeIndexes[this.file[setting.masterKey]];
                 if (oxi) {
                 if (oxi) {
                     const oxii = findFileIndex(oxi, this.file.id);
                     const oxii = findFileIndex(oxi, this.file.id);
@@ -342,6 +348,7 @@
             const files = $('#upload-file')[0].files;
             const files = $('#upload-file')[0].files;
             const formData = new FormData();
             const formData = new FormData();
             formData.append(setting.masterKey, curNode[setting.key]);
             formData.append(setting.masterKey, curNode[setting.key]);
+            if (curSubNode && setting.subMasterKey) formData.append(setting.subMasterKey, curSubNode[setting.subKey]);
             for (const file of files) {
             for (const file of files) {
                 if (file === undefined) {
                 if (file === undefined) {
                     toastr.error('未选择上传文件!');
                     toastr.error('未选择上传文件!');
@@ -364,6 +371,7 @@
                 // 插入到attData中
                 // 插入到attData中
                 data.forEach(d => {
                 data.forEach(d => {
                     d.node = curNode;
                     d.node = curNode;
+                    d.sub_node = curSubNode;
                     allAtts.push(d);
                     allAtts.push(d);
                     _addToNodeIndex(d, true);
                     _addToNodeIndex(d, true);
                 });
                 });
@@ -400,11 +408,13 @@
             }
             }
             const data = {};
             const data = {};
             data[setting.masterKey] = curNode[setting.key];
             data[setting.masterKey] = curNode[setting.key];
+            if (curSubNode && setting.subMasterKey) data[setting.subMasterKey] = curNode[setting.subKey];
             AliOss.uploadBigFile(file, setting.uploadBigUrl, data,
             AliOss.uploadBigFile(file, setting.uploadBigUrl, data,
                 { progressObj: $('#upload-big-file-progress'), resumeObj: $('#add-big-file-resume'), stopObj: $('#add-big-file-stop') },
                 { progressObj: $('#upload-big-file-progress'), resumeObj: $('#add-big-file-resume'), stopObj: $('#add-big-file-stop') },
                 function(result) {
                 function(result) {
                     result.forEach(d => {
                     result.forEach(d => {
                         d.node = curNode;
                         d.node = curNode;
+                        d.sub_node = curSubNode;
                         allAtts.push(d);
                         allAtts.push(d);
                         _addToNodeIndex(d, true);
                         _addToNodeIndex(d, true);
                     });
                     });
@@ -429,6 +439,10 @@
                 allAtts.splice(att_index, 1);
                 allAtts.splice(att_index, 1);
                 const xi = nodeIndexes[att.node[setting.key]];
                 const xi = nodeIndexes[att.node[setting.key]];
                 xi.splice(findFileIndex(xi, fid), 1);
                 xi.splice(findFileIndex(xi, fid), 1);
+                if (setting.subMasterKey && att.sub_node) {
+                    const sxi = subNodeIndexes[att.sub_node[setting.subKey]];
+                    sxi.splice(findFileIndex(sxi, fid), 1);
+                }
                 // 重新生成List
                 // 重新生成List
                 if (allAtts.length === 1) {
                 if (allAtts.length === 1) {
                     getAllAttHtml();
                     getAllAttHtml();
@@ -505,7 +519,7 @@
             const updateData = { id: moveObj.file.id };
             const updateData = { id: moveObj.file.id };
             updateData[setting.masterKey] = select[setting.key];
             updateData[setting.masterKey] = select[setting.key];
             postData(setting.moveUrl, updateData, function (result) {
             postData(setting.moveUrl, updateData, function (result) {
-                moveObj.moveoNodeIndex(result);
+                moveObj.moveNodeIndex(result);
                 refreshCurAttHtml();
                 refreshCurAttHtml();
                 $('#move-att2').modal('hide');
                 $('#move-att2').modal('hide');
             });
             });
@@ -542,6 +556,15 @@
             }
             }
             let xi = nodeIndexes[id];
             let xi = nodeIndexes[id];
             isTop ? xi.unshift(att) : xi.push(att);
             isTop ? xi.unshift(att) : xi.push(att);
+
+            if (setting.subMasterKey && att.sub_node) {
+                const sid = att[setting.subMasterKey];
+                if (!subNodeIndexes[sid]) {
+                    subNodeIndexes[sid] = [];
+                }
+                let sxi = subNodeIndexes[sid];
+                isTop ? sxi.unshift(att) : sxi.push(att);
+            }
         };
         };
         const loadDatas = function (datas) {
         const loadDatas = function (datas) {
             for (const d of datas) {
             for (const d of datas) {

+ 0 - 17
app/public/js/spreadjs_rela/spreadjs_zh.js

@@ -1130,23 +1130,6 @@ const SpreadJsObj = {
         }
         }
         this.endMassOperation(sheet);
         this.endMassOperation(sheet);
     },
     },
-    repaintNodesRowHeader: function (sheet, nodes) {
-        nodes = nodes instanceof Array ? nodes : [nodes];
-        const sortData = sheet.zh_dataType === 'tree' ? sheet.zh_tree.nodes : sheet.zh_data;
-        const rowIndex = [];
-        for (const node of nodes) {
-            rowIndex.push(sortData.indexOf(node));
-        }
-        this.repaintRowsRowHeader(sheet, rowIndex);
-    },
-    repaintRowsRowHeader: function (sheet, rows) {
-        for (const r of rows) {
-            const cellRect = sheet.getCellRect(r, 0);
-            cellRect.width = cellRect.x;
-            cellRect.x = 0;
-            sheet.repaint(cellRect);
-        }
-    },
     /**
     /**
      * 根据data加载sheet数据,合并了一般数据和树结构数据的加载
      * 根据data加载sheet数据,合并了一般数据和树结构数据的加载
      * @param {GC.Spread.Sheets.Worksheet} sheet
      * @param {GC.Spread.Sheets.Worksheet} sheet

+ 1 - 1
app/public/js/sr_detail.js

@@ -340,7 +340,7 @@ $(document).ready(() => {
         updateUrl: window.location.pathname + '/tag',
         updateUrl: window.location.pathname + '/tag',
         readOnly: true,
         readOnly: true,
         afterModify: function (nodes) {
         afterModify: function (nodes) {
-            SpreadJsObj.repaintNodesRowHeader(slSpread.getActiveSheet(), nodes);
+            slSpread.getActiveSheet().repaint();
         },
         },
         afterLocated:  function () {
         afterLocated:  function () {
             stagePosSpreadObj.loadCurPosData();
             stagePosSpreadObj.loadCurPosData();

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

@@ -921,7 +921,7 @@ $(document).ready(() => {
         relaSpread: slSpread,
         relaSpread: slSpread,
         updateUrl: window.location.pathname + '/tag',
         updateUrl: window.location.pathname + '/tag',
         afterModify: function (nodes) {
         afterModify: function (nodes) {
-            SpreadJsObj.repaintNodesRowHeader(slSpread.getActiveSheet(), nodes);
+            slSheet.repaint();
         },
         },
         afterLocated:  function () {
         afterLocated:  function () {
             stagePosSpreadObj.loadCurPosData();
             stagePosSpreadObj.loadCurPosData();

+ 19 - 1
app/router.js

@@ -64,6 +64,7 @@ module.exports = app => {
     // 安全巡检中间件
     // 安全巡检中间件
     const safeInspectionCheck = app.middlewares.safeInspectionCheck();
     const safeInspectionCheck = app.middlewares.safeInspectionCheck();
     const safeStageCheck = app.middlewares.safeStageCheck();
     const safeStageCheck = app.middlewares.safeStageCheck();
+    const costStageCheck = app.middlewares.costStageCheck();
     // 登入登出相关
     // 登入登出相关
     app.get('/login', 'loginController.index');
     app.get('/login', 'loginController.index');
     app.get('/login/:code', 'loginController.index');
     app.get('/login/:code', 'loginController.index');
@@ -530,6 +531,24 @@ module.exports = app => {
     app.post('/sp/:id/quality/tender/:tid/inspection/:qiid/information/file/delete', sessionAuth, subProjectCheck, tenderCheck, tenderPermissionCheck, qualityInspectionCheck, 'qualityController.deleteInspectionFile');
     app.post('/sp/:id/quality/tender/:tid/inspection/:qiid/information/file/delete', sessionAuth, subProjectCheck, tenderCheck, tenderPermissionCheck, qualityInspectionCheck, 'qualityController.deleteInspectionFile');
     app.get('/sp/:id/quality/tender/:tid/inspection/:qiid/information/file/:fid/download', sessionAuth, subProjectCheck, tenderCheck, tenderPermissionCheck, qualityInspectionCheck, 'qualityController.downloadInspectionFile');
     app.get('/sp/:id/quality/tender/:tid/inspection/:qiid/information/file/:fid/download', sessionAuth, subProjectCheck, tenderCheck, tenderPermissionCheck, qualityInspectionCheck, 'qualityController.downloadInspectionFile');
 
 
+    // 成本管理
+    app.get('/sp/:id/cost', sessionAuth, subProjectCheck, 'costController.tender');
+    app.post('/sp/:id/cost/member', sessionAuth, subProjectCheck, projectManagerCheck, 'costController.member');
+    app.post('/sp/:id/cost/memberSave', sessionAuth, subProjectCheck, projectManagerCheck, 'costController.memberSave');
+    app.post('/sp/:id/cost/templateSet', sessionAuth, subProjectCheck, projectManagerCheck, 'subProjController.templateSet');
+    app.get('/sp/:id/cost/tender/:tid/ledger', sessionAuth, subProjectCheck, tenderCheck, tenderPermissionCheck, 'costController.ledger');
+    app.get('/sp/:id/cost/tender/:tid/book', sessionAuth, subProjectCheck, tenderCheck, tenderPermissionCheck, 'costController.book');
+    app.get('/sp/:id/cost/tender/:tid/analysis', sessionAuth, subProjectCheck, tenderCheck, tenderPermissionCheck, 'costController.analysis');
+    app.post('/sp/:id/cost/tender/:tid/addStage', sessionAuth, subProjectCheck, tenderCheck, tenderPermissionCheck, 'costController.addStage');
+    app.post('/sp/:id/cost/tender/:tid/delStage', sessionAuth, subProjectCheck, tenderCheck, tenderPermissionCheck, 'costController.delStage');
+    app.post('/sp/:id/cost/tender/:tid/auditors', sessionAuth, subProjectCheck, tenderCheck, tenderPermissionCheck, 'costController.loadAuditors');
+    app.get('/sp/:id/cost/tender/:tid/:stype/:sorder', sessionAuth, subProjectCheck, tenderCheck, tenderPermissionCheck, costStageCheck, 'costController.stage');
+    app.post('/sp/:id/cost/tender/:tid/:stype/:sorder/load', sessionAuth, subProjectCheck, tenderCheck, tenderPermissionCheck, costStageCheck, 'costController.stageLoad');
+    app.post('/sp/:id/cost/tender/:tid/:stype/:sorder/update', sessionAuth, subProjectCheck, tenderCheck, tenderPermissionCheck, costStageCheck, 'costController.stageUpdate');
+    app.post('/sp/:id/cost/tender/:tid/:stype/:sorder/file/upload', sessionAuth, subProjectCheck, tenderCheck, tenderPermissionCheck, costStageCheck, 'costController.uploadStageFile');
+    app.post('/sp/:id/cost/tender/:tid/:stype/:sorder/file/delete', sessionAuth, subProjectCheck, tenderCheck, tenderPermissionCheck, costStageCheck, 'costController.deleteStageFile');
+    app.post('/sp/:id/cost/tender/:tid/:stype/:sorder/tag', sessionAuth, subProjectCheck, tenderCheck, tenderPermissionCheck, costStageCheck, 'costController.tag');
+
     // 安全管理
     // 安全管理
     // 安全计量
     // 安全计量
     app.get('/sp/:id/safe', sessionAuth, subProjectCheck, 'safeController.tender');
     app.get('/sp/:id/safe', sessionAuth, subProjectCheck, 'safeController.tender');
@@ -603,7 +622,6 @@ module.exports = app => {
     app.post('/tender/:id/expr/save', sessionAuth, tenderCheck, subProjectCheck, 'tenderController.saveExpr');
     app.post('/tender/:id/expr/save', sessionAuth, tenderCheck, subProjectCheck, 'tenderController.saveExpr');
     app.post('/tender/:id/expr/load', sessionAuth, tenderCheck, subProjectCheck, 'tenderController.loadExpr');
     app.post('/tender/:id/expr/load', sessionAuth, tenderCheck, subProjectCheck, 'tenderController.loadExpr');
 
 
-
     // 预付款
     // 预付款
     app.get('/tender/:id/advance/:type', sessionAuth, tenderCheck, subProjectCheck, 'advanceController.index');
     app.get('/tender/:id/advance/:type', sessionAuth, tenderCheck, subProjectCheck, 'advanceController.index');
     // app.get('/tender/:id/advance/material', sessionAuth, tenderCheck, subProjectCheck, 'advanceController.materialList');
     // app.get('/tender/:id/advance/material', sessionAuth, tenderCheck, subProjectCheck, 'advanceController.materialList');

+ 305 - 0
app/service/cost_stage.js

@@ -0,0 +1,305 @@
+'use strict';
+
+/**
+ *
+ *
+ * @author Mai
+ * @date
+ * @version
+ */
+
+const audit = require('../const/audit').common;
+const auditType = require('../const/audit').auditType;
+const shenpiConst = require('../const/shenpi');
+
+module.exports = app => {
+    class CostStage extends app.BaseService {
+
+        /**
+         * 构造函数
+         *
+         * @param {Object} ctx - egg全局变量
+         * @return {void}
+         */
+        constructor(ctx) {
+            super(ctx);
+            this.tableName = 'cost_stage';
+            this.stageType = {
+                ledger: { key: 'ledger', decimal: {tp: 6}, shenpi_status: 'cost_ledger', push_type: 'costStageLedger', dataService: 'costStageLedger' },
+                book: { key: 'book', decimal: {tp: 6}, shenpi_status: 'cost_book', push_type: 'costStageBook', dataService: 'costStageBook' },
+                analysis: { key: 'analysis', decimal: {tp: 6}, shenpi_status: 'cost_analysis', push_type: 'costStageAnalysis', dataService: 'costStageAnalysis' },
+            };
+        }
+
+        _analysisstage(stage) {
+            if (!stage) return;
+            const stages = stage instanceof Array ? stage : [stage];
+            if (stage.length === 0) return;
+
+            const typeInfo = this.stageType[stages[0].stage_type];
+            stages.forEach(s => {
+                s.decimal = s.decimal ? JSON.parse(s.decimal) : typeInfo.decimal;
+                s.stage_tp = s.stage_tp ? JSON.parse(s.stage_tp) : {};
+                s.stage_pre_tp = s.stage_pre_tp ? JSON.parse(s.stage_pre_tp) : {};
+                s.stage_end_tp = {};
+                for (const prop in s.stage_tp) {
+                    s.stage_end_tp[prop] = this.ctx.helper.add(s.stage_tp[prop], s.stage_pre_tp[prop]);
+                }
+                s.typeInfo = this.stageType[s.stage_type];
+            });
+        }
+        /**
+         * 获取全部期
+         * @param tid
+         * @returns {Promise<*>}
+         */
+        async getAllStages(tid, stage_type, sort = 'ASC') {
+            const sql = `SELECT cls.*, pa.name AS user_name FROM ${this.tableName} cls LEFT JOIN ${this.ctx.service.projectAccount.tableName} pa ON cls.create_user_id = pa.id` +
+                        `  WHERE cls.tid = ? and cls.stage_type = ? ORDER BY cls.stage_order ${sort}`;
+            const result = await this.db.query(sql, [tid, stage_type]);
+            this._analysisstage(result);
+            return result;
+        }
+        async getStage(id) {
+            const result = await this.getDataById(id);
+            this._analysisstage(result);
+            return result;
+        }
+        async getStageByOrder(tid, stage_type, stage_order) {
+            const result = await this.getDataByCondition({ tid, stage_type, stage_order });
+            this._analysisstage(result);
+            return result;
+        }
+
+        async getMaxOrder(tid, stage_type) {
+            const sql = 'SELECT Max(`stage_order`) As max_order FROM ' + this.tableName + ' Where `tid` = ? and stage_type = ?';
+            const sqlParam = [tid, stage_type];
+            const result = await this.db.queryOne(sql, sqlParam);
+            return result.max_order || 0;
+        }
+
+        async add(tid, stage_type, stage_date) {
+            const typeInfo = this.stageType[stage_type];
+            if (!tid) throw '数据错误';
+            const user_id = this.ctx.session.sessionUser.accountId;
+
+            const maxOrder = await this.getMaxOrder(tid, stage_type);
+            const data = {
+                id: this.uuid.v4(), tid: tid, create_user_id: user_id, update_user_id: user_id,
+                stage_order: maxOrder + 1, stage_date, stage_type,
+                audit_times: 1, audit_status: audit.status.uncheck,
+                decimal: JSON.stringify({ tp: 6 }),
+            };
+
+            const preStage = maxOrder > 0 ? await this.getStageByOrder(tid, stage_type, maxOrder) : null;
+            if (preStage) {
+                data.stage_pre_tp = JSON.stringify(preStage.stage_end_tp);
+            }
+            const transaction = await this.db.beginTransaction();
+            try {
+                const result = await transaction.insert(this.tableName, data);
+                if (result.affectedRows !== 1) throw '新增安全计量期失败';
+
+                await this.ctx.service[typeInfo.dataService].initStageData(transaction, data, preStage);
+                await this.ctx.service.costStageAudit.copyPreAuditors(transaction, preStage, data);
+                await transaction.commit();
+                return data;
+            } catch(err) {
+                await transaction.rollback();
+                throw err;
+            }
+        }
+        async delete(id) {
+            const stage = await this.getDataById(id);
+            const typeInfo = this.stageType[stage.stage_type];
+            const conn = await this.db.beginTransaction();
+            try {
+                await conn.delete(this.tableName, { id });
+                await conn.delete(this.ctx.service[typeInfo.dataService].tableName, { stage_id: id });
+                const files = await this.ctx.service.costStageFile.getFiles({ where: { stage_id: id} });
+                for (const f of files) {
+                    this.ctx.app.fujianOss.delete(f.filepath);
+                }
+                await conn.delete(this.ctx.service.costStageFile.tableName, { stage_id: id });
+                await conn.delete(this.ctx.service.costStageAudit.tableName, { stage_id: id });
+                // 记录删除日志
+                // await this.ctx.service.projectLog.addProjectLog(conn, projectLogConst.type.stage, projectLogConst.status.delete, `第${info.stage_order}期`);
+                await conn.commit();
+            } catch (err) {
+                await conn.rollback();
+                throw err;
+            }
+        }
+        async save(stage, data) {
+            await this.defaultUpdate({ id: stage.id, stage_date: data.stage_date, update_user_id: this.ctx.session.sessionUser.accountId });
+        }
+
+        async loadUser(stage) {
+            stage.user = await this.ctx.service.projectAccount.getAccountInfoById(stage.create_user_id);
+            stage.auditors = await this.ctx.service.costStageAudit.getAuditors(stage.id, stage.curTimes || stage.audit_times);
+            stage.auditorIds = this._.map(stage.auditors, 'audit_id');
+            stage.curAuditors = stage.auditors.filter(x => { return x.audit_status === audit.status.checking; });
+            stage.curAuditorIds = stage.curAuditors.map(x => { return x.audit_id; });
+            stage.flowAuditors = stage.curAuditors.length === 0 ? [] : stage.auditors.filter(x => { return x.active_order === stage.curAuditors[0].active_order; });
+            stage.flowAuditorIds = stage.flowAuditors.map(x => { return x.audit_id; });
+            stage.nextAuditors = stage.curAuditors.length > 0 ? stage.auditors.filter(x => { return x.active_order === stage.curAuditors[0].active_order + 1; }) : [];
+            stage.nextAuditorIds = this._.map(stage.nextAuditors, 'audit_id');
+            stage.auditorGroups = this.ctx.helper.groupAuditors(stage.auditors, 'active_order');
+            stage.userGroups = this.ctx.helper.groupAuditorsUniq(stage.auditorGroups);
+            stage.finalAuditorIds = stage.userGroups.length > 1 ? stage.userGroups[stage.userGroups.length - 1].map(x => { return x.audit_id; }) : [];
+            stage.userIds = stage.audit_status === audit.status.uncheck // 当前流程下全部参与人id
+                ? [stage.create_user_id]
+                : stage.auditorIds;
+            if (stage.audit_status === audit.status.checkNo) {
+                stage.checkNoAuditors = await this.ctx.service.costStageAudit.getAuditorsByStatus(stage.id, audit.status.checkNo, stage.audit_times-1);
+            }
+        }
+        async loadAuditViewData(stage) {
+            if (!stage.user) stage.user = await this.ctx.service.projectAccount.getAccountInfoById(stage.user_id);
+            const auditTimes = stage.audit_status === audit.status.checkNo ? stage.audit_times - 1 : stage.audit_times;
+            stage.auditHistory = await this.ctx.service.costStageAudit.getAuditorHistory(stage.id, auditTimes);
+            // 获取审批流程中左边列表
+            if (stage.audit_status === audit.status.checkNo && stage.create_user_id !== this.ctx.session.sessionUser.accountId) {
+                const auditors = await this.ctx.service.costStageAudit.getAuditors(stage.id, stage.audit_times - 1); // 全部参与的审批人
+                const auditorGroups = this.ctx.helper.groupAuditors(auditors);
+                stage.hisUserGroup = this.ctx.helper.groupAuditorsUniq(auditorGroups);
+            } else {
+                stage.hisUserGroup = stage.userGroups;
+            }
+        }
+        /**
+         * cancancel = 0 不可撤回
+         * cancancel = 1 原报撤回
+         * cancancel = 2 审批人撤回 审批通过
+         * cancancel = 3 审批人撤回 审批退回上一人
+         * cancancel = 4 审批人撤回 退回原报
+         * cancancel = 5 会签未全部审批通过时,审批人撤回 审批通过
+         *
+         * @param stage
+         * @returns {Promise<void>}
+         */
+        async doCheckCanCancel(stage) {
+            // 默认不可撤回
+            stage.cancancel = 0;
+            // 获取当前审批人的上一个审批人,判断是否是当前登录人,并赋予撤回功能,(当审批人存在有审批过时,上一人不允许再撤回)
+            const status = audit.status;
+            if (stage.audit_status === status.checked || stage.audit_status === status.uncheck) return;
+
+            const accountId = this.ctx.session.sessionUser.accountId;
+            if (stage.audit_status !== status.checkNo) {
+                // 找出当前操作人上一个审批人,包括审批完成的和退回上一个审批人的,同时当前操作人为第一人时,就是则为原报
+                if (stage.flowAuditors.find(x => { return x.audit_status !== status.checking}) && stage.flowAuditorIds.indexOf(accountId) < 0) return; // 当前流程存在审批人审批通过时,不可撤回
+                if (stage.curAuditorIds.indexOf(accountId) < 0 && stage.flowAuditorIds.indexOf(accountId) >= 0) {
+                    stage.cancancel = 5; // 会签未全部审批通过时,审批人撤回审批通过
+                    return;
+                }
+
+                const preAuditors = stage.curAuditors[0] && stage.curAuditors[0].active_order !== 1 ? stage.auditors.filter(x => { return x.active_order === stage.curAuditors[0].active_order - 1; }) : [];
+                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.audit_id })); // 重审不可撤回
+                if ((this._.isEqual(stage.flowAuditorIds, preAuditorIds) && preAuditorCheckCancel) || preAuditorHasOld) {
+                    return; // 不可以多次撤回
+                }
+
+                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) {
+                        stage.cancancel = 2;// 审批人撤回审批通过
+                    } else if (preAuditCheckNoPre) {
+                        stage.cancancel = 3;// 审批人撤回审批退回上一人
+                    }
+                    stage.preAuditors = preAuditors;
+                } else if (preAuditors.length === 0 && accountId === stage.create_user_id) {
+                    stage.cancancel = 1;// 原报撤回
+                }
+            } else {
+                const lastAuditors = await this.ctx.service.costStageAudit.getAuditors(stage.id, stage.audit_times - 1);
+                const onAuditor = this._.findLast(lastAuditors, { audit_status: status.checkNo });
+                if (onAuditor.audit_id === accountId) {
+                    stage.cancancel = 4;// 审批人撤回退回原报
+                    stage.preAuditors = lastAuditors.filter(x => { return x.active_order === onAuditor.active_order });
+                }
+            }
+        }
+        async doCheckStage(stage) {
+            const accountId = this.ctx.session.sessionUser.accountId;
+            // 审批退回时,原报读取本轮流程,其他人读取上一轮流程
+            if (stage.audit_status === audit.status.checkNo) {
+                stage.curTimes = stage.create_user_id === accountId ? stage.audit_times : stage.audit_times - 1;
+            } else {
+                stage.curTimes = stage.audit_times;
+            }
+            // 加载参与人
+            await this.loadUser(stage);
+
+            if (stage.audit_status === audit.status.uncheck) {
+                stage.readOnly = accountId !== stage.create_user_id;
+                stage.curSort = 0;
+            } else if (stage.audit_status === audit.status.checkNo) {
+                stage.readOnly = accountId !== stage.create_user_id;
+                if (!stage.readOnly) {
+                    stage.curSort = 0;
+                } else {
+                    const checkNoAudit = await this.service.costStageAudit.getDataByCondition({
+                        stage_id: stage.id, audit_times: stage.audit_times - 1, audit_status: audit.status.checkNo,
+                    });
+                    stage.curSort = checkNoAudit.active_order;
+                }
+            } else if (stage.audit_status === audit.status.checked) {
+                stage.readOnly = true;
+                stage.curSort = stage.audit_max_sort;
+            } else {
+                // 会签,会签人部分审批通过时,只读,但是curSort需按原来的取值
+                stage.curSort = stage.flowAuditorIds.indexOf(accountId) >= 0 ? stage.curAuditors[0].active_order : stage.curAuditors[0].active_order - 1;
+                stage.readOnly = stage.curAuditorIds.indexOf(accountId) < 0;
+                stage.canCheck = stage.readOnly && stage.curAuditorIds.indexOf(accountId) > 0;
+            }
+            await this.doCheckCanCancel(stage);
+        }
+        async checkShenpi(stage) {
+            const status = audit.status;
+            const info = this.ctx.tender.info;
+            const typeInfo = this.stageType[stage.stage_type];
+            const shenpi_status = info.shenpi[typeInfo.shenpi_status];
+            if ((stage.audit_status === status.uncheck || stage.audit_status === status.checkNo) && shenpi_status !== shenpiConst.sp_status.sqspr) {
+                // 进一步比较审批流是否与审批流程设置的相同,不同则替换为固定审批流或固定的终审
+                const auditList = await this.ctx.service.costStageAudit.getAllDataByCondition({ where: { stage_id: stage.id, audit_times: stage.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: stage.tid, sp_type: shenpiConst.sp_type[typeInfo.shenpi_status], 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.costStageAudit.updateNewAuditList(stage, shenpiList);
+                        await this.loadUser(stage);
+                    }
+                } else if (shenpi_status === shenpiConst.sp_status.gdzs) {
+                    const shenpiInfo = await this.ctx.service.shenpiAudit.getDataByCondition({ tid: stage.tid, sp_type: shenpiConst.sp_type[typeInfo.shenpi_status], sp_status: shenpi_status });
+                    // 判断最后一个id是否与固定终审id相同,不同则删除原审批流中如果存在的id和添加终审
+                    const lastAuditors = auditList.filter(x => { x.active_order === auditList.active_order; });
+                    if (shenpiInfo && (lastAuditors.length === 0 || (lastAuditors.length > 1 || shenpiInfo.audit_id !== lastAuditors[0].audit_id))) {
+                        await this.ctx.service.costStageAudit.updateLastAudit(stage, auditList, shenpiInfo.audit_id);
+                        await this.loadUser(stage);
+                    } else if (!shenpiInfo) {
+                        // 不存在终审人的状态下这里恢复为授权审批人
+                        this.ctx.tender.info.shenpi[typeInfo.shenpi_status] = shenpiConst.sp_status.sqspr;
+                    }
+                }
+            }
+        }
+    }
+
+    return CostStage;
+};

Разница между файлами не показана из-за своего большого размера
+ 1190 - 0
app/service/cost_stage_audit.js


+ 232 - 0
app/service/cost_stage_detail.js

@@ -0,0 +1,232 @@
+'use strict';
+
+/**
+ *
+ *
+ * @author Mai
+ * @date
+ * @version
+ */
+
+const costFields = {
+    textFields: ['code', 'name', 'party_b', 'postil', 'memo'],
+    preFields: ['pre_pay_tp', 'pre_cut_tp', 'pre_yf_tp', 'pre_sf_tp'],
+    curFields: ['pay_tp', 'cut_tp', 'yf_tp', 'sf_tp'],
+    readFields: ['read_pay_tp', 'read_cut_tp', 'read_yf_tp', 'read_sf_tp'],
+    baseFields: ['id', 'tender_id', 'stage_id', 'ledger_id', 'cost_id', 'd_order', 'is_deal', 'is_used'],
+};
+costFields.calcFields = [...costFields.curFields];
+costFields.editQueryFields = [...costFields.baseFields, ...costFields.textFields, ...costFields.preFields, ...costFields.curFields];
+costFields.readQueryFields = [...costFields.baseFields, ...costFields.textFields, ...costFields.preFields, ...costFields.readFields];
+costFields.compareQueryFields = [...costFields.baseFields, ...costFields.textFields, ...costFields.preFields, ...costFields.curFields, 'calc_his'];
+
+module.exports = app => {
+    class CostStageDetail extends app.BaseService {
+
+        constructor(ctx) {
+            super(ctx);
+            this.tableName = 'cost_stage_detail';
+        }
+
+        async getEditData(stage) {
+            const helper = this.ctx.helper;
+            const result = await this.getAllDataByCondition({
+                where: { stage_id: stage.id },
+                columns: costFields.editQueryFields
+            });
+            result.forEach(x => {
+                for(const prop of costFields.curFields) {
+                    x['end_' + prop] = helper.add(x['pre_' + prop], x[prop]);
+                }
+            });
+            return result;
+        }
+        async getReadData(stage) {
+            const helper = this.ctx.helper;
+            const result = await this.getAllDataByCondition({
+                where: { stage_id: stage.id },
+                columns: costFields.readQueryFields
+            });
+            result.forEach(x => {
+                for(const prop of costFields.curFields) {
+                    x[prop] = x['read_' + prop];
+                    x['end_' + prop] = helper.add(x['pre_' + prop], x[prop]);
+                }
+            });
+            return result;
+        }
+        async getCompareData(stage) {
+            const result = await this.getAllDataByCondition({
+                where: { stage_id: stage.id },
+                columns: costFields.compareQueryFields
+            });
+            result.forEach(x => {
+                x.calc_his = x.calc_his ? JSON.parse(x.calc_his) : [];
+            });
+            return result;
+        }
+
+        async _getLedgerUpdateData(data, ledger_id) {
+            const detailDatas = await this.getAllDataByCondition({ columns: ['id', 'ledger_id', ...costFields.curFields], where: { ledger_id, stage_id: this.ctx.costStage.id } });
+
+            const updateData = { id: ledger_id, update_user_id: this.ctx.session.sessionUser.accountId };
+            for (const d of data) {
+                for (const prop of costFields.curFields) {
+                    updateData[prop] = this.ctx.helper.add(updateData[prop], d[prop] || 0);
+                }
+            }
+            for (const d of detailDatas) {
+                if (data.findIndex(x => { return x.id === d.id; }) >= 0) continue;
+                for (const prop of costFields.curFields) {
+                    updateData[prop] = this.ctx.helper.add(updateData[prop], d[prop] || 0);
+                }
+            }
+            return updateData;
+        }
+        async _addDatas(data) {
+            const user_id = this.ctx.session.sessionUser.accountId;
+
+            const datas = data instanceof Array ? data : [data];
+            const insertData = [];
+            for (const d of datas) {
+                if (!d.ledger_id || !d.cost_id || !d.d_order) throw '新增明细数据,提交的数据错误';
+                const nd = {
+                    id: this.uuid.v4(), tender_id: this.ctx.costStage.tid, stage_id: this.ctx.costStage.id,
+                    ledger_id: d.ledger_id, cost_id: d.cost_id, d_order: d.d_order,
+                    add_user_id: user_id, update_user_id: user_id,
+                };
+                for (const prop of costFields.textFields) {
+                    if (d[prop] !== undefined) nd[prop] = d[prop] || '';
+                }
+                if (d.pay_tp !== undefined || d.cut_tp !== undefined) {
+                    nd.pay_tp = d.pay_tp !== undefined ? this.ctx.helper.round(d.pay_tp || 0, this.ctx.costStage.decimal.tp) : 0;
+                    nd.cut_tp = d.cut_tp !== undefined ? this.ctx.helper.round(d.cut_tp || 0, this.ctx.costStage.decimal.tp) : 0;
+                    nd.yf_tp = this.ctx.helper.sub(nd.pay_tp, nd.cut_tp);
+                    nd.sf_tp = nd.yf_tp;
+                }
+                insertData.push(nd);
+            }
+            const billsUpdate = await this._getLedgerUpdateData(insertData, insertData[0].ledger_id);
+
+            const conn = await this.db.beginTransaction();
+            try {
+                await conn.insert(this.tableName, insertData);
+                await conn.update(this.ctx.service.costStageLedger.tableName, billsUpdate);
+                await conn.commit();
+            } catch(err) {
+                await conn.rollback();
+                throw err;
+            }
+            const addData = await this.getAllDataByCondition({
+                where: { id: this.ctx.helper._.map(insertData, 'id') }
+            });
+            const ledgerData = await this.ctx.service.costStageLedger.getDataById(addData[0].ledger_id);
+            return [addData, ledgerData]
+        }
+        async _delDatas (data) {
+            if (!data || data.length === 0) throw '提交数据错误';
+            const orgDatas = await this.getAllDataByCondition({ where: { id: data } });
+
+            if (!orgDatas || orgDatas.length === 0) throw '删除的明细数据不存在';
+
+            const bills = await this.ctx.service.costStageLedger.getDataById(orgDatas[0].ledger_id);
+            let detail = await this.getAllDataByCondition({ where: { stage_id: this.ctx.costStage.id, ledger_id: bills.id } });
+
+            detail = detail.filter(ba => {
+                return data.indexOf(ba.id) < 0;
+            });
+            detail.sort((x, y) => { return x.d_order - y.d_order; });
+
+            const updateData = [];
+            detail.forEach((x, i) => {
+                if (x.d_order !== i + 1) updateData.push({ id: x.id, d_order: i + 1});
+            });
+            const billsUpdate = await this._getLedgerUpdateData(orgDatas.map(x => { return { id: x.id }}), orgDatas[0].ledger_id);
+
+            const conn = await this.db.beginTransaction();
+            try {
+                await conn.delete(this.tableName, { id: data });
+                if (updateData.length > 0) await conn.updateRows(this.tableName, updateData);
+                await conn.update(this.ctx.service.costStageLedger.tableName, billsUpdate);
+                await conn.commit();
+                return [data, updateData, billsUpdate];
+            } catch (err) {
+                await conn.rollback();
+                throw err;
+            }
+        }
+        async _updateDatas (data) {
+            if (!data || data.length === 0) throw '提交数据错误';
+            const user_id = this.ctx.session.sessionUser.accountId;
+
+            const datas = data instanceof Array ? data : [data];
+            const orgDatas = await this.getAllDataByCondition({
+                where: { id: this.ctx.helper._.map(datas, 'id') }
+            });
+            if (!orgDatas || orgDatas.length === 0) throw '修改的明细不存在';
+
+            const uDatas = [];
+            for (const d of datas) {
+                const od = orgDatas.find(x => { return x.id === d.id; });
+                if (!od) continue;
+
+                const nd = { id: od.id, update_user_id: user_id };
+                for (const prop of costFields.textFields) {
+                    if (d[prop] !== undefined) nd[prop] = d[prop];
+                }
+                if (d.pay_tp !== undefined || d.cut_tp !== undefined) {
+                    nd.pay_tp = d.pay_tp !== undefined ? this.ctx.helper.round(d.pay_tp || 0, this.ctx.costStage.decimal.tp) : od.pay_tp;
+                    nd.cut_tp = d.cut_tp !== undefined ? this.ctx.helper.round(d.cut_tp || 0, this.ctx.costStage.decimal.tp) : od.cut_tp;
+                    nd.yf_tp = this.ctx.helper.sub(nd.pay_tp, nd.cut_tp);
+                    if (od.yf_tp === od.sf_tp) nd.sf_tp = nd.yf_tp;
+                }
+                if (d.yf_tp !== undefined) nd.yf_tp = this.ctx.helper.round(d.yf_tp || 0, decimal.tp);
+                uDatas.push(nd);
+            }
+            if (uDatas.length > 0) {
+                const billsUpdate = await this._getLedgerUpdateData(uDatas, orgDatas[0].ledger_id);
+                const conn = await this.db.beginTransaction();
+                try {
+                    await conn.updateRows(this.tableName, uDatas);
+                    await conn.update(this.ctx.service.costStageLedger.tableName, billsUpdate);
+                    await conn.commit();
+                    return [uDatas, billsUpdate];
+                } catch(err) {
+                    await conn.rollback();
+                    throw err;
+                }
+            } else {
+                return [];
+            }
+        }
+        async updateDatas(data) {
+            const result = { detail: { add: [], del: [], update: [] }, ledger: {} };
+            try {
+                if (data.add) {
+                    [result.detail.add, result.ledger] = await this._addDatas(data.add);
+                }
+                if (data.update) {
+                    [result.detail.update, result.ledger] = await this._updateDatas(data.update);
+                }
+                if (data.del) {
+                    [result.detail.del, result.detail.update, result.ledger] = await this._delDatas(data.del);
+                }
+                return result;
+            } catch (err) {
+                if (err.stack) {
+                    throw err;
+                } else {
+                    result.err = err.toString();
+                    return result;
+                }
+            }
+        }
+
+        async deletePartData(transaction, tender_id, ledger_id) {
+            await transaction.delete(this.tableName, { tid, ledger_id });
+        }
+    }
+
+    return CostStageDetail;
+};
+

+ 94 - 0
app/service/cost_stage_file.js

@@ -0,0 +1,94 @@
+'use strict';
+
+/**
+ *
+ *  附件
+ * @author Mai
+ * @date 2026/4/21
+ * @version
+ */
+
+
+module.exports = app => {
+    class CostStageFile extends app.BaseService {
+        /**
+         * 构造函数
+         *
+         * @param {Object} ctx - egg全局变量
+         * @return {void}
+         */
+        constructor(ctx) {
+            super(ctx);
+            this.tableName = 'cost_stage_file';
+        }
+
+        _analysisData(files) {
+            const helper = this.ctx.helper;
+            const userId = this.ctx.session.sessionUser.accountId;
+            const ossPath = this.ctx.app.config.fujianOssPath;
+            files.forEach(x => {
+                x.viewpath = helper.canPreview(x.fileext) ? ossPath + x.filepath : '';
+                x.filepath = ossPath + x.filepath;
+                x.fileext_str = helper.fileExtStr(x.fileext);
+                x.canEdit = x.user_id === userId;
+            });
+        }
+
+        async getData(stage_id) {
+            const data = await this.getAllDataByCondition({
+                where: { stage_id, is_deleted: 0 },
+                orders: [['create_time', 'desc']],
+            });
+            this._analysisData(data);
+            return data;
+        }
+
+        async getFiles(condition) {
+            condition.orders = [['create_time', 'desc']];
+            const result = await this.getAllDataByCondition(condition);
+            this._analysisData(result);
+            return result;
+        }
+
+        async addFiles(costStage, fileInfo, user) {
+            const conn = await this.db.beginTransaction();
+            const result = {};
+            try {
+                const insertData = fileInfo.map(x => {
+                    return {
+                        id: this.uuid.v4(), tid: costStage.tid, stage_id: costStage.id, stage_type: costStage.stage_type,
+                        rela_id: x.rela_id, rela_sub_id: x.rela_sub_id,
+                        user_id: user.id, user_name: user.name, user_company: user.company, user_role: user.role,
+                        filename: x.filename, fileext: x.fileext, filesize: x.filesize, filepath: x.filepath,
+                    };
+                });
+                await conn.insert(this.tableName, insertData);
+                await conn.commit();
+                result.files = { id: insertData.map(x => { return x.id; })};
+            } catch (err) {
+                await conn.rollback();
+                throw err;
+            }
+            return await this.getFiles({ where: result.files });
+        }
+
+        async delFiles(files) {
+            const fileDatas = await this.getAllDataByCondition({ where: { id: files } });
+            const result = {};
+
+            const conn = await this.db.beginTransaction();
+            try {
+                const updateData = fileDatas.map(x => { return { id: x.id, is_deleted: 1 }; });
+                if (updateData.length > 0) await conn.updateRows(this.tableName, updateData);
+                await conn.commit();
+                result.del = files;
+            } catch (err) {
+                await conn.rollback();
+                throw err;
+            }
+            return result;
+        }
+    }
+
+    return CostStageFile;
+};

+ 520 - 0
app/service/cost_stage_ledger.js

@@ -0,0 +1,520 @@
+'use strict';
+
+/**
+ *
+ * 支付审批-安全生产
+ * @author Mai
+ * @date
+ * @version
+ */
+const billsUtils = require('../lib/bills_utils');
+const costFields = {
+    textFields: ['code', 'name', 'unit', 'postil', 'memo'],
+    preFields: ['pre_pay_tp', 'pre_cut_tp', 'pre_yf_tp', 'pre_sf_tp'],
+    curFields: ['pay_tp', 'cut_tp', 'yf_tp', 'sf_tp'],
+    readFields: ['read_pay_tp', 'read_cut_tp', 'read_yf_tp', 'read_sf_tp'],
+    treeFields: ['tree_id', 'tree_pid', 'tree_level', 'tree_order', 'tree_full_path', 'tree_is_leaf'],
+    baseFields: ['id', 'cost_id', 'tender_id', 'stage_id', 'is_deal', 'is_used'],
+};
+costFields.calcFields = [...costFields.curFields];
+costFields.editQueryFields = [...costFields.baseFields, ...costFields.treeFields, ...costFields.textFields, ...costFields.preFields, ...costFields.curFields];
+costFields.readQueryFields = [...costFields.baseFields, ...costFields.treeFields, ...costFields.textFields, ...costFields.preFields, ...costFields.readFields];
+costFields.compareQueryFields = [...costFields.baseFields, ...costFields.treeFields, ...costFields.textFields, ...costFields.preFields, ...costFields.curFields, 'calc_his'];
+
+const auditConst = require('../const/audit').costStage;
+
+module.exports = app => {
+
+    class CostStageLedger extends app.BaseTreeService {
+
+        /**
+         * 构造函数
+         *
+         * @param {Object} ctx - egg全局变量
+         * @return {void}
+         */
+        constructor(ctx) {
+            super(ctx, {
+                mid: 'stage_id',
+                kid: 'tree_id',
+                pid: 'tree_pid',
+                order: 'tree_order',
+                level: 'tree_level',
+                isLeaf: 'tree_is_leaf',
+                fullPath: 'tree_full_path',
+                keyPre: 'cost_ledger_maxTreeId:',
+                uuid: true,
+            });
+            // this.depart = ctx.app.config.table_depart.light;
+            this.tableName = 'cost_stage_ledger';
+            this.decimal = { tp: 6 };
+        }
+
+        // 继承方法
+        clearParentingData(data) {
+            for (const f of costFields.calcFields) {
+                data[f] = 0;
+            }
+        }
+
+        _getDefaultData(data, stage) {
+            data.id = this.uuid.v4();
+            data.cost_id = data.id;
+            data.tender_id = stage.tid;
+            data.stage_id = stage.id;
+            data.add_user_id = this.ctx.session.sessionUser.accountId;
+            data.update_user_id = data.add_user_id;
+        }
+
+        async getEditData(stage) {
+            const helper = this.ctx.helper;
+            const result = await this.getAllDataByCondition({
+                where: { stage_id: stage.id },
+                columns: costFields.editQueryFields
+            });
+            result.forEach(x => {
+                for(const prop of costFields.curFields) {
+                    x['end_' + prop] = helper.add(x['pre_' + prop], x[prop]);
+                }
+            });
+            return result;
+        }
+        async getReadData(stage) {
+            const helper = this.ctx.helper;
+            const result = await this.getAllDataByCondition({
+                where: { stage_id: stage.id },
+                columns: costFields.readQueryFields
+            });
+            result.forEach(x => {
+                for(const prop of costFields.curFields) {
+                    x[prop] = x['read_' + prop];
+                    x['end_' + prop] = helper.add(x['pre_' + prop], x[prop]);
+                }
+            });
+            return result;
+        }
+        async getCompareData(stage) {
+            const result = await this.getAllDataByCondition({
+                where: { stage_id: stage.id },
+                columns: costFields.compareQueryFields
+            });
+            result.forEach(x => {
+                x.calc_his = x.calc_his ? JSON.parse(x.calc_his) : [];
+            });
+            return result;
+        }
+
+        async init(stage, transaction) {
+            if (!this.ctx.subProject.cost_ledger_template) throw '未设置台账模板,请联系管理员设置';
+            if (!stage || !transaction) throw '安全生产费数据错误';
+
+            const templateData = await this.ctx.service.tenderNodeTemplate.getData(this.ctx.subProject.cost_ledger_template);
+            if (templateData.length === 0) throw '台账模板无数据,连请联系管理员修改';
+
+            const insertData = [];
+            for (const b of templateData) {
+                const bills = {
+                    tree_id: b.template_id, tree_pid: b.pid, tree_order: b.order, tree_level: b.level, tree_full_path: b.full_path, tree_is_leaf: b.is_leaf,
+                    code: b.code || '', name: b.name || '', unit: b.unit || '',
+                };
+                this._getDefaultData(bills, stage);
+                insertData.push(bills);
+            }
+
+            const operate = await transaction.insert(this.tableName, insertData);
+            return operate.affectedRows === insertData.length;
+        }
+        async initByPre(stage, preStage, transaction) {
+            if (!stage || !preStage || !transaction) throw '安全生产费数据错误';
+
+            const preBills = await this.getAllDataByCondition({
+                where: { stage_id: preStage.id },
+                columns: costFields.editQueryFields
+            });
+            const insertData = [];
+            for (const bills of preBills) {
+                bills.id = this.uuid.v4();
+                bills.stage_id = stage.id;
+                delete bills.postil;
+                for(const prop of costFields.curFields) {
+                    bills['pre_' + prop] = this.ctx.helper.add(bills['pre_' + prop], bills[prop]);
+                    bills[prop] = 0;
+                }
+                insertData.push(bills);
+            }
+            const operate = await transaction.insert(this.tableName, insertData);
+            return operate.affectedRows === insertData.length;
+        }
+        async initStageData(transaction, stage, preStage) {
+            if (preStage) {
+                this.initByPre(stage, preStage, transaction);
+            } else {
+                this.init(stage, transaction);
+            }
+        }
+
+        /**
+         * 新增数据(供内部或其他service类调用, controller不可直接使用)
+         * @param {Array|Object} data - 新增数据
+         * @param {Number} stageId - 期id
+         * @param {Object} transaction - 新增事务
+         * @return {Promise<boolean>} - {Promise<是否正确新增成功>}
+         */
+        async innerAdd(data, stageId, transaction) {
+            const datas = data instanceof Array ? data : [data];
+            if (!stageId) {
+                throw '期id错误';
+            }
+            if (datas.length <= 0) {
+                throw '插入数据为空';
+            }
+            if (!transaction) {
+                throw '内部错误';
+            }
+            // 整理数据
+            const insertData = [];
+            for (const tmp of datas) {
+                tmp[this.setting.id] = tmp.template_id;
+                tmp[this.setting.pid] = tmp.pid;
+                tmp[this.setting.mid] = stageId;
+                delete tmp.template_id;
+                delete tmp.pid;
+                tmp.id = this.uuid.v4();
+                insertData.push(tmp);
+            }
+            const operate = await transaction.insert(this.tableName, insertData);
+            return operate.affectedRows === datas.length;
+        }
+        /**
+         * 新增数据
+         *
+         * @param {Object} data - 新增的数据(可批量)
+         * @param {Number} stageId - 安全计量期id
+         * @return {Boolean} - 返回新增的结果
+         */
+        async add(data, stageId) {
+            this.transaction = await this.db.beginTransaction();
+            let result = false;
+            try {
+                result = await this.innerAdd(data, stageId, this.transaction);
+                if (!result) {
+                    throw '新增数据错误';
+                }
+                await this.transaction.commit();
+            } catch (error) {
+                await this.transaction.rollback();
+                result = false;
+            }
+
+            return result;
+        }
+
+        /**
+         * 根据节点Id获取数据
+         *
+         * @param {Number} stageId - 安全计量期id
+         * @param {Number} nodeId - 项目节/工程量清单节点id
+         * @return {Object} - 返回查询到的节点数据
+         */
+        async getDataByNodeId(stageId, nodeId) {
+            if ((nodeId <= 0) || (stageId <= 0)) {
+                return undefined;
+            }
+            const where = {};
+            where[this.setting.mid] = stageId;
+            where[this.setting.id] = nodeId;
+            const data = await this.db.getDataByCondition(where);
+
+            return data;
+        }
+        /**
+         * 根据节点Id获取数据
+         * @param {Number} stageId - 期Id
+         * @param {Array} nodesIds - 节点Id
+         * @return {Array}
+         */
+        async getDataByNodeIds(stageId, nodesIds) {
+            if (stageId <= 0) {
+                return [];
+            }
+
+            const where = {};
+            where[this.setting.mid] = stageId;
+            where[this.setting.id] = nodesIds;
+            const data = await this.db.getAllDataByCondition({ where });
+
+            return this._.sortBy(data, function(d) {
+                return nodesIds.indexOf(d[this.setting.id]);
+            });
+        }
+        /**
+         * 根据主键id获取数据
+         * @param {Array|Number} id - 主键id
+         * @return {Promise<*>}
+         */
+        async getDataByIds(id) {
+            if (!id) return [];
+            const ids = id instanceof Array ? id : [id];
+            if (ids.length === 0) return [];
+
+            const data = await this.db.getAllDataByCondition({ where: { id: ids } });
+            return data;
+        }
+
+        /**
+         * 根据 父节点id 获取子节点
+         * @param stageId
+         * @param nodeId
+         * @return {Promise<*>}
+         */
+        async getChildrenByParentId(stageId, nodeId) {
+            if (stageId <= 0 || !nodeId) {
+                return undefined;
+            }
+            const nodeIds = nodeId instanceof Array ? nodeId : [nodeId];
+            if (nodeIds.length === 0) {
+                return [];
+            }
+
+            const where = {};
+            where[this.setting.mid] = stageId;
+            where[this.setting.pid] = nodeIds;
+            const data = await this.getAllDataByCondition({ where, orders: [[this.setting.order, 'ASC']] });
+
+            return data;
+        }
+
+        async pasteBlockData(stage, targetId, pasteData, defaultData) {
+            const setting = this.setting;
+            if ((stageId <= 0) || (sid <= 0)) return [];
+
+            if (!pasteData || pasteData.length <= 0) throw '复制数据错误';
+            for (const pd of pasteData) {
+                if (!pd || pd.length <= 0) throw '复制数据错误';
+                pd.sort(function (x, y) {
+                    return x[setting.level] - y[setting.level]
+                });
+                if (pd[0][this.setting.pid] !== pasteData[0][0][this.setting.pid]) throw '复制数据错误:仅可操作同层节点';
+            }
+            this.newBills = false;
+            const targetData = await this.getDataByKid(stageId, targetId);
+            if (!targetData) throw '粘贴数据错误';
+
+            const newParentPath = targetData.full_path.replace(targetData.tree_id, '');
+            const tpDecimal = this.ctx.costStage && this.ctx.costStage.decimal ? this.ctx.costStage.decimal : this.decimal;
+
+            const pasteBillsData = [], leafBillsId = [];
+            let maxId = await this._getMaxLid(this.ctx.tender.id);
+            for (const [i, pd] of pasteData.entries()) {
+                for (const d of pd) {
+                    d.children = pd.filter(function (x) {
+                        return x[setting.pid] === d[setting.id];
+                    });
+                }
+                const pbd = [];
+                for (const [j, d] of pd.entries()) {
+                    const newBills = {};
+                    this._getDefaultData(newBills, this.ctx.costStage);
+                    for (const prop of costFields.textFields) {
+                        newBills[prop] = d[prop] || '';
+                    }
+                    newBills[setting.mid] = stageId;
+                    newBills[setting.id] = maxId + j + 1;
+                    newBills[setting.pid] = j === 0 ? targetData[setting.pid] : d[setting.pid];
+                    newBills[setting.level] = d[setting.level] + targetData[setting.level] - pd[0][setting.level];
+                    newBills[setting.order] = j === 0 ? targetData[setting.order] + i + 1 : d[setting.order];
+                    newBills[setting.isLeaf] = d[setting.isLeaf];
+
+                    for (const c of d.children) {
+                        c[setting.pid] = newBills[setting.id];
+                    }
+                    for (const prop of costFields.curFields) {
+                        newBills[prop] = this.ctx.helper.round(d[prop] || 0, tpDecimal.tp);
+                        newBills['end_' + prop] = newBills[prop];
+                    }
+                    pbd.push(newBills);
+                }
+                for (const d of pbd) {
+                    const parent = pbd.find(function (x) {
+                        return x[setting.id] === d[setting.pid];
+                    });
+                    d[setting.fullPath] = parent
+                        ? parent[setting.fullPath] + '-' + d[setting.id]
+                        : newParentPath + d[setting.id];
+                    if (defaultData) this.ctx.helper._.assignIn(pbd, defaultData);
+                    pasteBillsData.push(d);
+                }
+                maxId = maxId + pbd.length;
+            }
+
+            this.transaction = await this.db.beginTransaction();
+            try {
+                // 选中节点的所有后兄弟节点,order+粘贴节点个数
+                await this._updateChildrenOrder(tid, targetData[setting.pid], targetData[setting.order] + 1, pasteData.length);
+                // 数据库创建新增节点数据
+                if (pasteBillsData.length > 0) {
+                    const newData = await this.transaction.insert(this.tableName, pasteBillsData);
+                }
+                this._cacheMaxLid(tid, maxId);
+                await this.transaction.commit();
+            } catch (err) {
+                await this.transaction.rollback();
+                throw err;
+            }
+
+            // 查询应返回的结果
+            const updateData = await this.getNextsData(targetData[setting.mid], targetData[setting.pid], targetData[setting.order] + pasteData.length);
+            return { create: pasteBillsData, update: updateData };
+        }
+
+        async addLedgerNode(stage, targetId, count) {
+            if (!stage) return null;
+
+            const select = targetId ? await this.getDataByKid(stage.id, targetId) : null;
+            if (targetId && !select) throw '新增节点数据错误';
+
+            this.transaction = await this.db.beginTransaction();
+            try {
+                if (select) await this._updateChildrenOrder(stage.id, select[this.setting.pid], select[this.setting.order] + 1, count);
+                const newDatas = [];
+                const maxId = await this._getMaxLid(stage.id);
+                for (let i = 1; i < count + 1; i++) {
+                    const newData = {};
+                    newData[this.setting.kid] = maxId + i;
+                    newData[this.setting.pid] = select ? select[this.setting.pid] : this.rootId;
+                    newData[this.setting.mid] = stage.id;
+                    newData[this.setting.level] = select ? select[this.setting.level] : 1;
+                    newData[this.setting.order] = select ? select[this.setting.order] + i : i;
+                    newData[this.setting.fullPath] = newData[this.setting.level] > 1
+                        ? select[this.setting.fullPath].replace('-' + select[this.setting.kid], '-' + newData[this.setting.kid])
+                        : newData[this.setting.kid] + '';
+                    newData[this.setting.isLeaf] = true;
+                    this._getDefaultData(newData, stage);
+                    newDatas.push(newData);
+                }
+                const insertResult = await this.transaction.insert(this.tableName, newDatas);
+                this._cacheMaxLid(stage.id, maxId + count);
+
+                if (insertResult.affectedRows !== count) throw '新增节点数据错误';
+                await this.transaction.commit();
+                this.transaction = null;
+            } catch (err) {
+                await this.transaction.rollback();
+                this.transaction = null;
+                throw err;
+            }
+
+            if (select) {
+                const createData = await this.getChildBetween(stage.id, select[this.setting.pid], select[this.setting.order], select[this.setting.order] + count + 1);
+                const updateData = await this.getNextsData(stage.id, select[this.setting.pid], select[this.setting.order] + count);
+                return {create: createData, update: updateData};
+            } else {
+                const createData = await this.getChildBetween(stage.id, -1, 0, count + 1);
+                return {create: createData};
+            }
+        }
+
+        async updateCalc(stage, data) {
+            const helper = this.ctx.helper;
+            // 简单验证数据
+            if (!stage) throw '成本报审不存在';
+            const decimal = stage.decimal || this.decimal;
+            if (!data) throw '提交数据错误';
+            const datas = data instanceof Array ? data : [data];
+            const ids = datas.map(x => { return x.id; });
+            const orgData = await this.getAllDataByCondition({ where: { id: ids }});
+            const updateData = [];
+            for (const row of datas) {
+                const oData = orgData.find(x => { return x.id === row.id });
+                if (!oData || oData.stage_id !== stage.id || oData.tree_id !== row.tree_id) throw '提交数据错误';
+
+                let nData = { id: oData.id, tree_id: oData.tree_id, update_user_id: this.ctx.session.sessionUser.accountId };
+
+                // calc
+                if (row.pay_tp !== undefined || row.cut_tp !== undefined) {
+                    nData.pay_tp = row.pay_tp !== undefined ? helper.round(row.pay_tp || 0, decimal.tp) : oData.pay_tp || 0;
+                    nData.cut_tp = row.cut_tp !== undefined ? helper.round(row.cut_tp || 0, decimal.tp) : oData.cut_tp || 0;
+                    nData.yf_tp = helper.sub(nData.pay_tp, nData.cut_tp);
+                }
+                if (row.sf_tp !== undefined) nData.sf_tp = helper.round(row.sf_tp || 0, decimal.tp);
+                for (const field of costFields.textFields) {
+                    if (row[field] !== undefined) nData[field] = row[field] || '';
+                }
+                updateData.push(nData);
+            }
+
+            await this.db.updateRows(this.tableName, updateData);
+            return { update: updateData };
+        }
+
+        async auditCache(transaction, stageId, auditInfo) {
+            const leaf = await this.getAllDataByCondition({ where: { stage_id: stageId, tree_is_leaf: true} });
+            const updateData = [];
+            for (const l of leaf) {
+                const diff = costFields.curFields.find(x => {
+                    return l[x] !== l['read_' + x];
+                });
+                if (diff) {
+                    const data = { id: l.id };
+                    // cache read
+                    for (const f of costFields.curFields) {
+                        data['read_' + f] = l[f];
+                    }
+                    // cache his
+                    const his = l.calc_his ? JSON.parse(l.calc_his) : [];
+                    const fi = his.find(x => { return x.audit_times === auditInfo.audit_times && x.active_order === auditInfo.active_order; });
+                    if (fi >= 0) his.splice(fi, 1);
+                    const newHis = { ...auditInfo };
+                    for (const f of costFields.curFields) {
+                        newHis[f] = data[f];
+                    }
+                    his.push(newHis);
+                    data.calc_his = JSON.stringify(his);
+                    updateData.push(data);
+                }
+            }
+            if (updateData.length > 0) await transaction.updateRows(this.tableName, updateData);
+        }
+
+        async setDecimal(decimal) {
+            if (!this.ctx.costStage) throw '读取数据错误';
+            if (this.ctx.costStage.stage_order !== this.ctx.costStage.latestOrder) throw '往期不可修改小数位数';
+            if (this.ctx.costStage.audit_status !== auditConst.status.uncheck && this.ctx.costStage.audit_status !== auditConst.status.checkNo) throw '仅原报可修改小数位数';
+            const orgDecimal = this.ctx.costStage.decimal ? this.ctx.costStage.decimal : this.decimal;
+
+            const calcTp = decimal.tp < orgDecimal.tp;
+            this.ctx.costStage.decimal = { up: decimal.up, tp: decimal.tp, qty: decimal.qty };
+
+            const updateData = [];
+            if (calcTp) {
+                const calcData = await this.getAllDataByCondition({ where: {stage_id: this.ctx.costStage.id, tree_is_leaf: 1 } });
+                for (const cd of calcData) {
+                    const nd = { id: cd.id, tree_id: cd.tree_id };
+                    for (const prop of costFields.curFields) {
+                        nd[prop] = this.ctx.helper.round(nd[prop]);
+                    }
+                    updateData.push(nd);
+                }
+            }
+            const conn = await this.db.beginTransaction();
+            try {
+                await conn.update(this.ctx.service.costStage.tableName, { id: this.ctx.costStage.id, decimal: JSON.stringify(this.ctx.costStage.decimal)});
+                if (updateData.length > 0) await conn.updateRows(this.tableName, updateData);
+                await conn.commit();
+                return { calc: calcTp, update: updateData };
+            } catch(err) {
+                await conn.rollback();
+                return { calc: false, update: [] };
+            }
+
+        }
+
+        async getSum(stage) {
+            const sumFields = costFields.curFields.map(f => { return `SUM(${f}) AS ${f}`});
+            const result = await this.db.queryOne(`SELECT ${sumFields.join(', ')} FROM ${this.tableName} where stage_id = ?`, [stage.id]);
+            return result;
+        }
+    }
+
+    return CostStageLedger;
+};

+ 95 - 0
app/service/cost_stage_tag.js

@@ -0,0 +1,95 @@
+'use strict';
+
+/**
+ *
+ *
+ * @author Mai
+ * @date
+ * @version
+ */
+
+const validField = ['rela_id', 'rela_sub_id', 'share', 'color', 'comment'];
+
+module.exports = app => {
+
+    class LedgerTag extends app.BaseService {
+        /**
+         * 构造函数
+         *
+         * @param {Object} ctx - egg全局变量
+         * @return {void}
+         */
+        constructor(ctx) {
+            super(ctx);
+            this.tableName = 'cost_stage_tag';
+        }
+
+        /**
+         * 获取台账、期所有标段
+         * @param {Number} tid - 标段id
+         * @param {Number} sid - 期id(-1时查询台账分解全部标签)
+         * @returns {Promise<void>}
+         */
+        async getDatas(stageId) {
+            const sql = 'SELECT la.id, la.create_user_id, la.rela_id, la.rela_sub_id, la.share, la.color, la.comment, pa.name as user_name FROM ' + this.tableName + ' la ' +
+                '  LEFT JOIN ' + this.ctx.service.projectAccount.tableName + ' pa ON la.create_user_id = pa.id' +
+                '  WHERE la.stage_id = ? and (la.create_user_id = ? or la.share) ORDER BY la.create_time DESC';
+            return await this.db.query(sql, [stageId, this.ctx.session.sessionUser.accountId]);
+        }
+
+        /**
+         * 过滤无效字段,容错
+         * @param data
+         * @private
+         */
+        _filterInvalidField(data) {
+            for (const prop in data) {
+                if (validField.indexOf(prop) === -1) {
+                    delete data[prop];
+                }
+            }
+        }
+
+        async _addTag(data) {
+            this._filterInvalidField(data);
+            data.id = this.uuid.v4();
+            data.create_user_id = this.ctx.session.sessionUser.accountId;
+            data.update_user_id = data.create_user_id;
+            data.tender_id = this.ctx.costStage.tid;
+            data.stage_id = this.ctx.costStage.id;
+            data.stage_type = this.ctx.costStage.stage_type;
+            await this.db.insert(this.tableName, data);
+            data.user_name = this.ctx.session.sessionUser.name;
+            return data;
+        }
+
+        async _delTag(id) {
+            const tag = await this.getDataById(id);
+            if (tag.create_user_id !== this.ctx.session.sessionUser.accountId) throw '您无权删除该数据';
+
+            await this.deleteById(id);
+            return id;
+        }
+
+        async _updateTag(data) {
+            const tag = await this.getDataById(data.id);
+            if (tag.create_user_id !== this.ctx.session.sessionUser.accountId) throw '您无权修改该数据';
+
+            this._filterInvalidField(data);
+            data.id = tag.id;
+            data.update_user_id = this.ctx.session.sessionUser.accountId;
+            const result = await this.db.update(this.tableName, data);
+            if (result.affectedRows === 1) return data;
+        }
+
+        async update(data) {
+            const result = {};
+            if (data.add) result.add = await this._addTag(data.add);
+            if (data.del) result.del = await this._delTag(data.del);
+            if (data.update) result.update = await this._updateTag(data.update);
+            return result;
+        }
+    }
+
+    return LedgerTag;
+};

+ 9 - 1
app/service/tender_permission.js

@@ -45,7 +45,14 @@ module.exports = app => {
                 schedule: {
                 schedule: {
                     view: { title: '查看', value: 1, isDefault: 1 },
                     view: { title: '查看', value: 1, isDefault: 1 },
                     edit: { title: '修改', value: 2 },
                     edit: { title: '修改', value: 2 },
-                }
+                },
+                cost: {
+                    view: { title: '查看', value: 1, isDefault: 1 },
+                    ledger_add: { title: '成本台账上报', value: 2 },
+                    book_add: { title: '财务账面上报', value: 3 },
+                    analysis_add: { title: '成本分析上报', value: 4 },
+                    visitor: { title: '游客', value: 5 },
+                },
             };
             };
             this.PermissionBlock = [
             this.PermissionBlock = [
                 { key: 'quality', name: '工程资料', field: 'quality' },
                 { key: 'quality', name: '工程资料', field: 'quality' },
@@ -53,6 +60,7 @@ module.exports = app => {
                 { key: 'safe_inspection', name: '安全巡检', field: 'safe_inspection' },
                 { key: 'safe_inspection', name: '安全巡检', field: 'safe_inspection' },
                 { key: 'safe_payment', name: '安全计量', field: 'safe_payment' },
                 { key: 'safe_payment', name: '安全计量', field: 'safe_payment' },
                 { key: 'schedule', name: '标段进度', field: 'schedule' },
                 { key: 'schedule', name: '标段进度', field: 'schedule' },
+                { key: 'cost', name: '成本管理', field: 'schedule' },
             ];
             ];
             for (const p of this.PermissionBlock) {
             for (const p of this.PermissionBlock) {
                 if (p.children) {
                 if (p.children) {

+ 85 - 0
app/view/cost/analysis_list.ejs

@@ -0,0 +1,85 @@
+<% include ./list_sub_menu.ejs %>
+<div class="panel-content">
+    <div class="panel-title">
+        <div class="title-main d-flex">
+            <% include ./list_sub_mini_menu.ejs %>
+            <h2>
+                收支列表
+            </h2>
+            <% if (ctx.permission.cost.analysis_add && (stagelist.length === 0 || stagelist[0].audit_status === auditConst.status.checked)) { %>
+            <div class="ml-auto">
+                <a href="#add-qi" data-toggle="modal" data-target="#add-qi" class="btn btn-primary btn-sm">新建收支</a>
+            </div>
+            <% } %>
+        </div>
+    </div>
+    <div class="content-wrap">
+        <div class="c-body">
+            <div class="sjs-height-0">
+                <table class="table table-bordered table-hover">
+                    <thead>
+                    <tr class="text-center">
+                        <th width="80px">期数</th>
+                        <th width="70px">报审月份</th>
+                        <th width="70px">创建人</th>
+                        <th>项目收入</th>
+                        <th>项目支出</th>
+                        <th>利润</th>
+                        <th>利润率</th>
+                        <th width="120px">审批进度</th>
+                        <th width="100px">操作</th>
+                    </tr>
+                    </thead>
+                    <tbody>
+                    <% for (const s of stagelist) { %>
+                    <tr>
+                        <td>
+                            <a href="/sp/<%- ctx.subProject.id %>/cost/tender/<%- ctx.tender.id %>/analysis/<%- s.stage_order %>" target="_blank">第 <%- s.stage_order %> 期</a>
+                        </td>
+                        <td class="text-center"><%- s.stage_date %></td>
+                        <td class="text-right"><%- s.pay_tp %></td>
+                        <td class="text-right"><%- s.cut_tp %></td>
+                        <td class="text-right"><%- s.yf_tp %></td>
+                        <td class="text-right"><%- s.sf_tp %></td>
+                        <td class="text-right"><%- s.end_pay_tp %></td>
+                        <td class="text-right"><%- s.end_cut_tp %></td>
+                        <td class="text-right"><%- s.end_yf_tp %></td>
+                        <td class="text-right"><%- s.end_sf_tp %></td>
+                        <td class="<%- auditConst.info[s.audit_status].class %>">
+                            <% if (s.audit_status === auditConst.status.checked && s.final_auditor_str) { %>
+                            <a href="#sp-list" data-toggle="modal" data-target="#sp-list" stage-order="<%- s.stage_order %>"><%- s.final_auditor_str %></a>
+                            <% } else { %>
+                            <% if (s.curAuditors.length > 0) { %>
+                            <% if (s.curAuditors[0].audit_type === auditType.key.common) { %>
+                            <a href="#sp-list" data-toggle="modal" data-target="#sp-list" stage-order="<%- s.stage_order %>"><%- s.curAuditors[0].name %><%if (s.curAuditors[0].role !== '' && s.curAuditors[0].role !== null) { %>-<%- s.curAuditors[0].role %><% } %></a>
+                            <% } else { %>
+                            <a href="#sp-list" data-toggle="modal" data-target="#sp-list" stage-order="<%- s.stage_order %>"><%- ctx.helper.transFormToChinese(s.curAuditors[0].audit_order) + '审' %></a>
+                            <% } %>
+                            <% } %>
+                            <% } %>
+                            <%- auditConst.info[s.audit_status].title %>
+                        </td>
+                        <td class="text-center">
+                            <% if (s.audit_status === auditConst.status.uncheck && s.create_user_id === ctx.session.sessionUser.accountId) { %>
+                            <a href="/sp/<%- ctx.subProject.id %>/cost/tender/<%- ctx.tender.id %>/analysis/<%- s.stage_order %>" target="_blank" class="btn <%- auditConst.info[s.audit_status].btnClass %> btn-sm"><%- auditConst.info[s.audit_status].btnTitle %></a>
+                            <% } else if (s.status === auditConst.status.checkNo && s.user_id === ctx.session.sessionUser.accountId) { %>
+                            <a href="/sp/<%- ctx.subProject.id %>/cost/tender/<%- ctx.tender.id %>/analysis/<%- s.stage_order %>" target="_blank" class="btn <%- auditConst.info[s.audit_status].btnClass %> btn-sm"><%- auditConst.info[s.audit_status].btnTitle %></a>
+                            <% } else if ((s.status === auditConst.status.checking || s.status === auditConst.status.checkNoPre) && s.curAuditors && s.curAuditors.findIndex(x => { return x.aid === ctx.session.sessionUser.accountId; }) >= 0) { %>
+                            <a href="/sp/<%- ctx.subProject.id %>/cost/tender/<%- ctx.tender.id %>/analysis/<%- s.stage_order %>" target="_blank" class="btn <%- auditConst.info[s.audit_status].btnClass %> btn-sm"><%- auditConst.info[s.audit_status].btnTitle %></a>
+                            <% } else { %>
+                            <span class="<%- auditConst.info[s.audit_status].class %>"><%- auditConst.info[s.audit_status].title %></span>
+                            <% } %>
+                        </td>
+                    </tr>
+                    <% } %>
+                    </tbody>
+                </table>
+            </div>
+        </div>
+    </div>
+</div>
+<script>
+    const stageList = JSON.parse('<%- JSON.stringify(stageList) %>');
+    const auditType = JSON.parse('<%- JSON.stringify(auditType) %>');
+    const auditConst = JSON.parse('<%- JSON.stringify(auditConst) %>');
+</script>

+ 79 - 0
app/view/cost/analysis_list_modal.ejs

@@ -0,0 +1,79 @@
+<div class="modal" id="add-qi" data-backdrop="static" aria-modal="true" role="dialog">
+    <div class="modal-dialog" role="document">
+        <form class="modal-content" action="pay/add" method="POST" onsubmit="return checkAddValid();">
+            <div class="modal-header">
+                <h5 class="modal-title">添加新一期</h5>
+            </div>
+            <div class="modal-body">
+                <div class="form-group form-group-sm">
+                    <label>支付期</label>
+                    <input class="form-control form-control-sm" value="第 <%- (phasePays.length + 1) %> 期" type="text" readonly="">
+                    <input type="hidden" value="<%- (phasePays.length + 1) %>" name="phase_order">
+                </div>
+                <div class="form-group form-group-sm">
+                    <label>支付年月</label>
+                    <input class="datepicker-here form-control form-control-sm" autocomplete="off" name="date" placeholder="点击选择年月" data-view="months" data-min-view="months" data-date-format="yyyy-MM" data-language="zh" type="text">
+                </div>
+                <div class="form-group form-group-sm">
+                    <label>计量期</label>
+                    <select class="form-control form-control-sm" name="stage">
+                        <% for (const s of validStages) { %>
+                            <option value="<%- s.order %>">第 <%- s.order %> 期</option>
+                        <% } %>
+                    </select>
+                </div>
+                <div class="form-group form-group-sm">
+                    <label>支付期备注</label>
+                    <textarea class="form-control form-control-sm" rows="3" name="memo"></textarea>
+                </div>
+            </div>
+            <div class="modal-footer">
+                <input type="hidden" name="_csrf_j" value="<%= ctx.csrf %>" />
+                <button type="button" class="btn btn-sm btn-secondary" data-dismiss="modal">关闭</button>
+                <button type="submit" class="btn btn-sm btn-primary">确定</button>
+            </div>
+        </form>
+    </div>
+</div>
+<!--审批流程/结果-->
+<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 modal-height-500" style="overflow: auto">
+                        <div class="card mt-3">
+                            <ul class="list-group list-group-flush" id="auditor-list">
+                            </ul>
+                        </div>
+                    </div>
+                    <div class="col-8 modal-height-500" style="overflow: auto" id="audit-list">
+                    </div>
+                </div>
+            </div>
+            <div class="modal-footer">
+                <button type="button" class="btn btn-secondary btn-sm" data-dismiss="modal">关闭</button>
+            </div>
+        </div>
+    </div>
+</div>
+<script>
+    $('.datepicker-here').datepicker({
+        autoClose: true,
+    });
+    const checkAddValid = function() {
+        if ($('[name=date]', '#add-qi').val() == '') {
+            toastr.error('请选择计量年月');
+            return false;
+        }
+    }
+    const checkEditValid = function() {
+        if ($('[name=date]', '#edit-qi').val() == '') {
+            toastr.error('请选择计量年月');
+            return false;
+        }
+    }
+</script>

+ 2 - 0
app/view/cost/analysis_menu_list.ejs

@@ -0,0 +1,2 @@
+<nav-menu title="返回" url="/sp/<%= ctx.subProject.id %>/cost/tender/<%- ctx.tender.id %>/analysis" tclass="text-primary" ml="1" icon="fa-chevron-left"></nav-menu>
+<nav-menu title="成本分析" url="/sp/<%= ctx.subProject.id %>/cost/tender/<%- ctx.tender.id %>/analysis/<%- ctx.costStage.stage_order %>" ml="3" active="<%= (ctx.url.indexOf('ledger') >= 0 ? 1 : -1) %>"></nav-menu>

+ 41 - 0
app/view/cost/audit_btn.ejs

@@ -0,0 +1,41 @@
+<div class="contarl-box">
+    <% if (ctx.costStage.audit_status === auditConst.status.uncheck) { %>
+        <% if (ctx.session.sessionUser.accountId === ctx.costStage.create_user_id) { %>
+            <a id="sub-sp-btn" href="javascript: void(0);" data-toggle="modal" data-target="#sub-sp" class="btn btn-primary btn-sm btn-block">上报审批</a>
+        <% } else { %>
+            <a id="sub-sp-btn" href="javascript: void(0);" data-toggle="modal" data-target="#sub-sp" class="btn btn-outline-secondary btn-sm btn-block">上报中</a>
+        <% } %>
+    <% } %>
+
+    <% if (ctx.costStage.audit_status === auditConst.status.checking) { %>
+        <% if (ctx.costStage.curAuditorIds.indexOf(ctx.session.sessionUser.accountId) >= 0) { %>
+            <a id="sp-done-btn" href="javascript: void(0);" data-toggle="modal" data-target="#sp-done" class="btn btn-success btn-sm btn-block">审批通过</a>
+            <a href="#sp-back" data-toggle="modal" data-target="#sp-back" class="btn btn-warning btn-sm btn-block">审批退回</a>
+        <% } else { %>
+            <a href="#sp-list" data-toggle="modal" data-target="#sp-list" class="btn btn-outline-secondary btn-sm btn-block">审批中</a>
+        <% } %>
+    <% } %>
+
+    <% if (ctx.costStage.audit_status === auditConst.status.checked) { %>
+        <a href="#sp-list" data-type="hide" data-toggle="modal" data-target="#sp-list" class="btn btn-outline-secondary btn-sm btn-block sp-list-btn">审批完成</a>
+    <% } %>
+
+    <% if (ctx.costStage.audit_status === auditConst.status.checkNo) { %>
+        <a href="#sp-list"  data-type="hide" data-toggle="modal" data-target="#sp-list" class="btn btn-outline-warning btn-sm btn-block text-muted sp-list-btn">审批退回</a>
+        <% if (ctx.session.sessionUser.accountId === ctx.costStage.create_user_id) { %>
+            <a href="#sp-list" data-type="show" data-toggle="modal" data-target="#sp-list"  class="btn btn-primary btn-sm btn-block sp-list-btn">重新上报</a>
+        <% } %>
+    <% } %>
+
+    <% if (ctx.costStage.finalAuditorIds.indexOf(ctx.session.sessionUser.accountId) >= 0 && ctx.costStage.audit_status === auditConst.status.checked && ctx.costStage.isLatest) { %>
+        <a href="javascript: void(0);" data-toggle="modal" data-target="#sp-down-back" class="btn btn-warning btn-sm btn-block">重新审批</a>
+    <% } %>
+
+    <% if (ctx.costStage.cancancel) { %>
+        <a href="javascript: void(0);" data-toggle="modal" data-target="#sp-down-cancel" class="btn btn-danger btn-sm btn-block">撤回</a>
+    <% } %>
+
+    <% if (ctx.costStage.create_user_id === ctx.session.sessionUser.accountId && ctx.costStage.isLatest && (ctx.costStage.audit_status === auditConst.status.checkNo || ctx.costStage.audit_status === auditConst.status.uncheck)) { %>
+        <a href="#del-qi" data-toggle="modal" data-target="#del-qi" class="btn btn-outline-danger btn-sm btn-block mt-5">删除本期</a>
+    <% } %>
+</div>

+ 898 - 0
app/view/cost/audit_modal.ejs

@@ -0,0 +1,898 @@
+<% if (ctx.costStage && (ctx.costStage.audit_status === auditConst.status.uncheck || ctx.costStage.audit_status === auditConst.status.checkNo) && ctx.session.sessionUser.accountId === ctx.costStage.create_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 (shenpi_status !== shenpiConst.sp_status.gdspl && ctx.session.sessionUser.accountId === ctx.costStage.create_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.costStage.auditorGroups.length; i < iLen; i++) { %>
+                            <% if (ctx.costStage.auditorGroups[i][0].audit_order === 0) continue; %>
+                            <li class="list-group-item d-flex" auditorId="<%- ctx.costStage.auditorGroups[i][0].audit_id %>">
+                                <div class="col-auto"><%- ctx.costStage.auditorGroups[i][0].audit_order %></div>
+                                <div class="col">
+                                    <% for (const auditor of ctx.costStage.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.costStage.auditorGroups[i][0].audit_type !== auditType.key.common) { %>
+                                    <span class="badge badge-pill badge-<%- auditType.info[ctx.costStage.auditorGroups[i][0].audit_type].class %> badge-bg-small"><small><%- auditType.info[ctx.costStage.auditorGroups[i][0].audit_type].long%></small></span>
+                                    <% } %>
+                                    <% if ((shenpi_status === shenpiConst.sp_status.sqspr ||
+                                                    (shenpi_status === shenpiConst.sp_status.gdzs && i+1 !== iLen)) && ctx.session.sessionUser.accountId === ctx.costStage.create_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="audit/start" name="stage-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.costStage.create_user_id) { %>
+                <button class="btn btn-primary btn-sm" type="submit">确认上报</button>
+                <% } %>
+            </form>
+        </div>
+    </div>
+</div>
+<% } %>
+<% if(ctx.costStage && (ctx.costStage.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.costStage.audit_status === auditConst.status.checkNo && ctx.session.sessionUser.accountId === ctx.costStage.create_user_id) { %>
+                        <a class="sp-list-item" href="#sub-sp" data-toggle="modal" data-target="#sub-sp"
+                           id="hideSp">修改审批流程</a>
+                        <% } else if(ctx.costStage.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" style="display: none;">修改审批流程</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.costStage.userGroups.forEach((item, idx) => { %>
+                                <% if (idx === 0) { %>
+                                <li class="list-group-item d-flex justify-content-between align-items-center" data-auditorId="<%- item[0].audit_id%>">
+                                    <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.company %>" 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.costStage.userGroups.length -1 && idx !== 0) { %>
+                                <li class="list-group-item d-flex justify-content-between align-items-center" data-auditorId="<%- item[0].audit_id%>">
+                                    <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.company %>" 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" data-auditorId="<%- item[0].audit_id%>">
+                                    <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.company %>" 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.costStage.auditHistory.forEach((his, idx) => { %>
+                        <!-- 展开/收起历史流程 -->
+                        <% if(idx === ctx.costStage.auditHistory.length - 1 && ctx.costStage.auditHistory.length !== 1) { %>
+                        <div class="text-right">
+                            <a href="javascript: void(0);" id="fold-btn" data-target="show" >展开历史审批流程</a>
+                        </div>
+                        <% } %>
+                        <div class="<%- idx < ctx.costStage.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.costStage.auditHistory.length - 1 && ctx.costStage.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.costStage.auditHistory.length - 1 && ctx.costStage.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.info[group.audit_status].class %>"><%- auditConst.info[group.audit_status].title %></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-10">
+                                                        <span class="h6"><%- auditor.name %></span>
+                                                        <% if (group.audit_order === 0) { %>
+                                                            <% if (auditor.role && auditor.role.trim()) { %>
+                                                                <span class="text-muted ml-1"><%- auditor.role %></span>
+                                                            <% } %>
+                                                        <% } else { %>
+                                                            <span class="text-muted ml-1">
+                                                                <%- auditor.company %>
+                                                                <% if (auditor.role) { %>
+                                                                    - <%- 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="stage-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.costStage.audit_status === auditConst.status.checkNo && ctx.session.sessionUser.accountId === ctx.costStage.create_user_id) { %>
+                <button class="btn btn-primary btn-sm sp-list-item" type="submit">确认上报</button>
+                <% } %>
+            </form>
+        </div>
+    </div>
+</div>
+<% } %>
+<% if (ctx.costStage && (ctx.costStage.audit_status === auditConst.status.checking || ctx.costStage.audit_status === auditConst.status.checkNoPre) && ctx.costStage.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.costStage.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.costStage.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.costStage.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.company %>" 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.costStage.auditHistory.forEach((his, idx) => { %>
+                        <!-- 展开/收起历史流程 -->
+                        <% if(idx === ctx.costStage.auditHistory.length - 1 && ctx.costStage.auditHistory.length !== 1) { %>
+                        <div class="text-right"><a href="javascript: void(0);" id="fold-btn" data-target="show">展开历史审批流程</a></div>
+                        <% } %>
+                        <div class="<%- idx < ctx.costStage.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.costStage.auditHistory.length - 1 && ctx.costStage.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.costStage.auditHistory.length - 1 && ctx.costStage.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.info[group.audit_status].class %>"><%- auditConst.info[group.audit_status].title %></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-10">
+                                                        <span class="h6"><%- auditor.name %></span>
+                                                        <% if (group.audit_order === 0) { %>
+                                                            <% if (auditor.role && auditor.role.trim()) { %>
+                                                                <span class="text-muted ml-1"><%- auditor.role %></span>
+                                                            <% } %>
+                                                        <% } else { %>
+                                                            <span class="text-muted ml-1">
+                                                                <%- auditor.company %>
+                                                                <% if (auditor.role && auditor.role.trim()) { %>
+                                                                    - <%- 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.costStage.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.costStage.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.costStage.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.company %>" 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.costStage.auditHistory.forEach((his, idx) => { %>
+                        <!-- 展开/收起历史流程 -->
+                        <% if(idx === ctx.costStage.auditHistory.length - 1 && ctx.costStage.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.costStage.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.costStage.auditHistory.length - 1 && ctx.costStage.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.costStage.auditHistory.length - 1 && ctx.costStage.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.info[group.audit_status].class %>"><%- auditConst.info[group.audit_status].title %></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-10">
+                                                        <span class="h6"><%- auditor.name %></span>
+                                                        <% if (group.audit_order === 0) { %>
+                                                            <% if (auditor.role && auditor.role.trim()) { %>
+                                                                <span class="text-muted ml-1"><%- auditor.role %></span>
+                                                            <% } %>
+                                                        <% } else { %>
+                                                            <span class="text-muted ml-1">
+                                                                <%- auditor.company %>
+                                                                <% if (auditor.role && auditor.role.trim()) { %>
+                                                                    - <%- 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.costStage.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.costStage && ctx.costStage.finalAuditorIds.indexOf(ctx.session.sessionUser.accountId) >= 0 && ctx.costStage.audit_status === auditConst.status.checked && ctx.costStage.isLatest) { %>
+<% if (!authMobile && ctx.session.sessionUser.loginStatus === 0) { %>
+<!--终审重新审批-->
+<div class="modal fade" id="sp-down-back" 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>
+                <h5>您目前还没设置认证手机,请先设置。</h5>
+            </div>
+            <div class="modal-footer">
+                <input type="hidden" name="_csrf_j" value="<%= ctx.csrf %>" />
+                <button type="button" class="btn btn-sm btn-secondary" data-dismiss="modal">取消</button>
+                <a href="/profile/sms" class="btn btn-sm btn-primary">去设置</a>
+            </div>
+        </div>
+    </div>
+</div>
+<% } else { %>
+<div class="modal fade" id="sp-down-back" data-backdrop="static">
+    <div class="modal-dialog" role="document">
+        <form class="modal-content" method="post" action="audit/checkAgain" name="stage-checkAgain">
+            <div class="modal-header">
+                <h5 class="modal-title">重新审批</h5>
+            </div>
+            <div class="modal-body">
+                <h5>确认由「终审-<%= ctx.session.sessionUser.name %>」重新审批「第<%= ctx.costStage.stage_order %>期」?
+                </h5>
+                <% if (ctx.session.sessionUser.loginStatus === 0) { %>
+                <div class="form-group">
+                    <label>重审需要验证码确认,验证码将发送至尾号<%- authMobile.slice(-4) %>的手机</label>
+                    <div class="input-group input-group-sm mb-3">
+                        <input class="form-control" type="text" readonly="readonly" name="code"
+                               placeholder="输入短信中的6位验证码" />
+                        <div class="input-group-append">
+                            <button class="btn btn-outline-secondary" type="button" id="get-code">获取验证码</button>
+                        </div>
+                    </div>
+                </div>
+                <% } %>
+            </div>
+            <div class="modal-footer">
+                <input type="hidden" name="_csrf_j" value="<%= ctx.csrf %>" />
+                <button type="button" class="btn btn-secondary btn-sm" data-dismiss="modal">关闭</button>
+                <button <% if (ctx.session.sessionUser.loginStatus === 0) { %>disabled<% } %> id="re-shenpi-btn" class="btn btn-warning btn-sm" type="submit">确定重审</button>
+            </div>
+        </form>
+    </div>
+</div>
+<% } %>
+<% } %>
+<% if (ctx.costStage && ctx.costStage.create_user_id === ctx.session.sessionUser.accountId && ctx.costStage.isLatest && (ctx.costStage.audit_status === auditConst.status.checkNo || ctx.costStage.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='/sp/<%- ctx.subProject.id %>/cost/tender/<%= ctx.tender.id %>/delStage' method="post">
+            <div class="modal-header">
+                <h5 class="modal-title">删除期</h5>
+            </div>
+            <div class="modal-body">
+                <h5>确认删除「第<%= ctx.costStage.stage_order %>期」?</h5>
+                <h5>删除后,数据无法恢复,请谨慎操作。</h5>
+            </div>
+            <div class="modal-footer">
+                <input type="hidden" name="stage_id" value="<%= ctx.costStage.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.costStage && ctx.costStage.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>
+            <form class="modal-footer" action="audit/checkCancel" method="post">
+                <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" type="submit">确定撤回</button>
+            </form>
+        </div>
+    </div>
+</div>
+<% } %>
+<% if (ctx.costStage && ctx.costStage.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.costStage.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.info[auditor.audit_status].class %>"><%- auditor.audit_status !== auditConst.status.uncheck ? auditConst.info[auditor.audit_status].title : '待审批'  %></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.costStage.create_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.costStage.create_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.costStage.create_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.costStage.create_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>
+    const accountGroup = JSON.parse(unescape('<%- escape(JSON.stringify(accountGroup)) %>'));
+    const accountList = JSON.parse(unescape('<%- escape(JSON.stringify(accountList)) %>'));
+    const shenpi_status = <%- shenpi_status %>;
+    const shenpiConst =  JSON.parse('<%- JSON.stringify(shenpiConst) %>');
+    const createUserId = parseInt('<%= ctx.costStage.create_user_id %>');
+    const auditType = JSON.parse(unescape('<%- escape(JSON.stringify(auditType)) %>'));
+</script>
+<script>
+    $('[name=stage-start]').submit(function (e) {
+        if (checkAuditorFrom()) {
+            $(this).parent().parent().parent().modal('hide');
+            $('#hide-all').hide();
+        } else {
+            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();
+    });
+
+    $('.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('展开历史审核记录')
+            })
+        }
+    });
+
+    // 重新审批
+    $('[name=stage-checkAgain]').submit(function (e) {
+        <% 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) {
+            toast('请填写正确的验证码', 'error');
+            return false;
+        }
+        <% } %>
+        $('#hide-all').hide();
+    });
+</script>

+ 78 - 0
app/view/cost/book_list.ejs

@@ -0,0 +1,78 @@
+<% include ./list_sub_menu.ejs %>
+<div class="panel-content">
+    <div class="panel-title">
+        <div class="title-main d-flex">
+            <% include ./list_sub_mini_menu.ejs %>
+            <h2>
+                审批列表
+            </h2>
+            <% if (ctx.permission.cost.book_add && (stagelist.length === 0 || stagelist[0].audit_status === auditConst.status.checked)) { %>
+            <div class="ml-auto">
+                <a href="#add-qi" data-toggle="modal" data-target="#add-qi" class="btn btn-primary btn-sm">新建报审</a>
+            </div>
+            <% } %>
+        </div>
+    </div>
+    <div class="content-wrap">
+        <div class="c-body">
+            <div class="sjs-height-0">
+                <table class="table table-bordered table-hover">
+                    <thead>
+                    <tr class="text-center">
+                        <th width="80px">期数</th>
+                        <th width="70px">报审月份</th>
+                        <th width="70px">创建人</th>
+                        <th>本期入账金额(不含税)</th>
+                        <th>截止本期入账金额(不含税)</th>
+                        <th width="120px">审批进度</th>
+                        <th width="100px">操作</th>
+                    </tr>
+                    </thead>
+                    <tbody>
+                    <% for (const s of stagelist) { %>
+                    <tr>
+                        <td>
+                            <a href="/sp/<%- ctx.subProject.id %>/cost/tender/<%- ctx.tender.id %>/book/<%- s.stage_order %>" target="_blank">第 <%- s.stage_order %> 期</a>
+                        </td>
+                        <td class="text-center"><%- s.stage_date %></td>
+                        <td class="text-center"><%- s.user_name %></td>
+                        <td class="text-right"><%- s.tp %></td>
+                        <td class="text-right"><%- s.end_tp %></td>
+                        <td class="<%- auditConst.info[s.audit_status].class %>">
+                            <% if (s.audit_status === auditConst.status.checked && s.final_auditor_str) { %>
+                            <a href="#sp-list" data-toggle="modal" data-target="#sp-list" stage-order="<%- s.stage_order %>"><%- s.final_auditor_str %></a>
+                            <% } else { %>
+                            <% if (s.curAuditors.length > 0) { %>
+                            <% if (s.curAuditors[0].audit_type === auditType.key.common) { %>
+                            <a href="#sp-list" data-toggle="modal" data-target="#sp-list" stage-order="<%- s.stage_order %>"><%- s.curAuditors[0].name %><%if (s.curAuditors[0].role !== '' && s.curAuditors[0].role !== null) { %>-<%- s.curAuditors[0].role %><% } %></a>
+                            <% } else { %>
+                            <a href="#sp-list" data-toggle="modal" data-target="#sp-list" stage-order="<%- s.stage_order %>"><%- ctx.helper.transFormToChinese(s.curAuditors[0].audit_order) + '审' %></a>
+                            <% } %>
+                            <% } %>
+                            <% } %>
+                            <%- auditConst.info[s.audit_status].title %>
+                        </td>
+                        <td class="text-center">
+                            <% if (s.audit_status === auditConst.status.uncheck && s.create_user_id === ctx.session.sessionUser.accountId) { %>
+                            <a href="/sp/<%- ctx.subProject.id %>/cost/tender/<%- ctx.tender.id %>/book/<%- s.stage_order %>" target="_blank" class="btn <%- auditConst.info[s.audit_status].btnClass %> btn-sm"><%- auditConst.info[s.audit_status].btnTitle %></a>
+                            <% } else if (s.status === auditConst.status.checkNo && s.user_id === ctx.session.sessionUser.accountId) { %>
+                            <a href="/sp/<%- ctx.subProject.id %>/cost/tender/<%- ctx.tender.id %>/book/<%- s.stage_order %>" target="_blank" class="btn <%- auditConst.info[s.audit_status].btnClass %> btn-sm"><%- auditConst.info[s.audit_status].btnTitle %></a>
+                            <% } else if ((s.status === auditConst.status.checking || s.status === auditConst.status.checkNoPre) && s.curAuditors && s.curAuditors.findIndex(x => { return x.aid === ctx.session.sessionUser.accountId; }) >= 0) { %>
+                            <a href="/sp/<%- ctx.subProject.id %>/cost/tender/<%- ctx.tender.id %>/book/<%- s.stage_order %>" target="_blank" class="btn <%- auditConst.info[s.audit_status].btnClass %> btn-sm"><%- auditConst.info[s.audit_status].btnTitle %></a>
+                            <% } else { %>
+                            <span class="<%- auditConst.info[s.audit_status].class %>"><%- auditConst.info[s.audit_status].title %></span>
+                            <% } %>
+                        </td>
+                    </tr>
+                    <% } %>
+                    </tbody>
+                </table>
+            </div>
+        </div>
+    </div>
+</div>
+<script>
+    const stageList = JSON.parse('<%- JSON.stringify(stageList) %>');
+    const auditType = JSON.parse('<%- JSON.stringify(auditType) %>');
+    const auditConst = JSON.parse('<%- JSON.stringify(auditConst) %>');
+</script>

+ 79 - 0
app/view/cost/book_list_modal.ejs

@@ -0,0 +1,79 @@
+<div class="modal" id="add-qi" data-backdrop="static" aria-modal="true" role="dialog">
+    <div class="modal-dialog" role="document">
+        <form class="modal-content" action="pay/add" method="POST" onsubmit="return checkAddValid();">
+            <div class="modal-header">
+                <h5 class="modal-title">添加新一期</h5>
+            </div>
+            <div class="modal-body">
+                <div class="form-group form-group-sm">
+                    <label>支付期</label>
+                    <input class="form-control form-control-sm" value="第 <%- (phasePays.length + 1) %> 期" type="text" readonly="">
+                    <input type="hidden" value="<%- (phasePays.length + 1) %>" name="phase_order">
+                </div>
+                <div class="form-group form-group-sm">
+                    <label>支付年月</label>
+                    <input class="datepicker-here form-control form-control-sm" autocomplete="off" name="date" placeholder="点击选择年月" data-view="months" data-min-view="months" data-date-format="yyyy-MM" data-language="zh" type="text">
+                </div>
+                <div class="form-group form-group-sm">
+                    <label>计量期</label>
+                    <select class="form-control form-control-sm" name="stage">
+                        <% for (const s of validStages) { %>
+                            <option value="<%- s.order %>">第 <%- s.order %> 期</option>
+                        <% } %>
+                    </select>
+                </div>
+                <div class="form-group form-group-sm">
+                    <label>支付期备注</label>
+                    <textarea class="form-control form-control-sm" rows="3" name="memo"></textarea>
+                </div>
+            </div>
+            <div class="modal-footer">
+                <input type="hidden" name="_csrf_j" value="<%= ctx.csrf %>" />
+                <button type="button" class="btn btn-sm btn-secondary" data-dismiss="modal">关闭</button>
+                <button type="submit" class="btn btn-sm btn-primary">确定</button>
+            </div>
+        </form>
+    </div>
+</div>
+<!--审批流程/结果-->
+<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 modal-height-500" style="overflow: auto">
+                        <div class="card mt-3">
+                            <ul class="list-group list-group-flush" id="auditor-list">
+                            </ul>
+                        </div>
+                    </div>
+                    <div class="col-8 modal-height-500" style="overflow: auto" id="audit-list">
+                    </div>
+                </div>
+            </div>
+            <div class="modal-footer">
+                <button type="button" class="btn btn-secondary btn-sm" data-dismiss="modal">关闭</button>
+            </div>
+        </div>
+    </div>
+</div>
+<script>
+    $('.datepicker-here').datepicker({
+        autoClose: true,
+    });
+    const checkAddValid = function() {
+        if ($('[name=date]', '#add-qi').val() == '') {
+            toastr.error('请选择计量年月');
+            return false;
+        }
+    }
+    const checkEditValid = function() {
+        if ($('[name=date]', '#edit-qi').val() == '') {
+            toastr.error('请选择计量年月');
+            return false;
+        }
+    }
+</script>

+ 2 - 0
app/view/cost/book_menu_list.ejs

@@ -0,0 +1,2 @@
+<nav-menu title="返回" url="/sp/<%= ctx.subProject.id %>/cost/tender/<%- ctx.tender.id %>/book" tclass="text-primary" ml="1" icon="fa-chevron-left"></nav-menu>
+<nav-menu title="财务账面" url="/sp/<%= ctx.subProject.id %>/cost/tender/<%- ctx.tender.id %>/book/<%- ctx.costStage.stage_order %>" ml="3" active="<%= (ctx.url.indexOf('ledger') >= 0 ? 1 : -1) %>"></nav-menu>

+ 90 - 0
app/view/cost/ledger.ejs

@@ -0,0 +1,90 @@
+<% include ./stage_memu.ejs %>
+<div class="panel-content">
+    <div class="panel-title">
+        <div class="title-main d-flex">
+            <% include ./stage_mini_menu.ejs %>
+            <div>
+                <div class="d-inline-block">
+                    <div class="dropdown">
+                        <button class="btn btn-sm btn-light dropdown-toggle text-primary" type="button" id="dropdownMenuButton" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
+                            <i class="fa fa-list-ol"></i> 显示层级
+                        </button>
+                        <div class="dropdown-menu" aria-labelledby="dropdownMenuButton">
+                            <a class="dropdown-item" name="showLevel" tag="1" href="javascript: void(0);">第一层</a>
+                            <a class="dropdown-item" name="showLevel" tag="2" href="javascript: void(0);">第二层</a>
+                            <a class="dropdown-item" name="showLevel" tag="3" href="javascript: void(0);">第三层</a>
+                            <a class="dropdown-item" name="showLevel" tag="4" href="javascript: void(0);">第四层</a>
+                            <a class="dropdown-item" name="showLevel" tag="last" href="javascript: void(0);">最底层</a>
+                        </div>
+                    </div>
+                </div>
+                <% if (ctx.costStage.create_user_id === ctx.session.sessionUser.accountId) { %>
+                <div class="d-inline-block">
+                    <a href="javascript: void(0);" name="base-opr" type="add" class="btn btn-sm btn-light text-primary" data-toggle="tooltip" data-placement="bottom" title="" data-original-title="新增"><i class="fa fa-plus" aria-hidden="true"></i></a>
+                    <a href="javascript: void(0);" name="base-opr" type="delete" class="btn btn-sm btn-light text-primary" data-toggle="tooltip" data-placement="bottom" title="" data-original-title="删除"><i class="fa fa-remove" aria-hidden="true"></i></a>
+                    <a href="javascript: void(0);" name="base-opr" type="up-level" class="btn btn-sm btn-light text-primary" data-toggle="tooltip" data-placement="bottom" title="" data-original-title="升级"><i class="fa fa-arrow-left" aria-hidden="true"></i></a>
+                    <a href="javascript: void(0);" name="base-opr" type="down-level" class="btn btn-sm btn-light text-primary" data-toggle="tooltip" data-placement="bottom" title="" data-original-title="降级"><i class="fa fa-arrow-right" aria-hidden="true"></i></a>
+                    <a href="javascript: void(0);" name="base-opr" type="down-move" class="btn btn-sm btn-light text-primary" data-toggle="tooltip" data-placement="bottom" title="" data-original-title="下移"><i class="fa fa-arrow-down" aria-hidden="true"></i></a>
+                    <a href="javascript: void(0);" name="base-opr" type="up-move" class="btn btn-sm btn-light text-primary" data-toggle="tooltip" data-placement="bottom" title="" data-original-title="上移"><i class="fa fa-arrow-up" aria-hidden="true"></i></a>
+                </div>
+                <% } %>
+            </div>
+            <div class="ml-auto">
+            </div>
+        </div>
+    </div>
+    <div class="content-wrap row pr-46">
+        <div class="c-header p-0 col-12">
+        </div>
+        <!--核心内容(两栏)-->
+        <div class="row w-100 sub-content">
+            <!--左栏-->
+            <div class="c-body" id="left-view" style="width: 100%">
+                <div class="sjs-height-1" style="overflow: hidden" id="bills-spread">
+                </div>
+                <div class="bcontent-wrap">
+                    <div class="resize-y" id="detail-spr" r-Type="height" div1=".sjs-height-1" div2=".bcontent-wrap" r-parent="div2" title="调整大小"><!--调整上下高度条--></div>
+                    <div class="bc-bar mb-1">
+                        <ul class="nav nav-tabs">
+                            <li class="nav-item">
+                                <a class="nav-link active" href="javascript:void(0)">明细数据</a>
+                            </li>
+                        </ul>
+                    </div>
+                    <div class="sp-wrap" id="detail-spread">
+                    </div>
+                </div>
+            </div>
+            <div class="c-body" id="right-view" style="display: none; width: 33%;">
+                <div class="resize-x" id="right-spr" r-Type="width" div1="#left-view" div2="#right-view" title="调整大小" a-type="percent"><!--调整左右高度条--></div>
+                <div class="tab-content">
+                    <div id="fujian" class="tab-pane tab-select-show">
+                    </div>
+                    <div id="search" class="tab-pane tab-select-show">
+                    </div>
+                    <div id="tag" class="tab-pane tab-select-show">
+                    </div>
+                </div>
+            </div>
+        </div>
+        <!--右侧菜单-->
+        <div class="side-menu">
+            <ul class="nav flex-column right-nav" id="side-menu">
+                <li class="nav-item">
+                    <a class="nav-link" content="#search" href="javascript: void(0);">查找定位</a>
+                </li>
+                <li class="nav-item">
+                    <a class="nav-link" content="#fujian" href="javascript: void(0);">附件</a>
+                </li>
+                <li class="nav-item">
+                    <a class="nav-link" content="#tag" href="javascript: void(0);">书签</a>
+                </li>
+            </ul>
+        </div>
+    </div>
+</div>
+<script>
+    const readOnly = <%- ctx.costStage.readOnly %>;
+    const tenderId = parseInt('<%- ctx.tender.id %>');
+    const stageId = parseInt('<%- ctx.costStage.id %>');
+</script>

+ 92 - 0
app/view/cost/ledger_list.ejs

@@ -0,0 +1,92 @@
+<% include ./list_menu.ejs %>
+<div class="panel-content">
+    <div class="panel-title">
+        <div class="title-main d-flex">
+            <% include ./list_mini_menu.ejs %>
+            <h2>
+                审批列表
+            </h2>
+            <% if (ctx.permission.cost.ledger_add && (stages.length === 0 || stages[0].audit_status === auditConst.status.checked)) { %>
+            <div class="ml-auto">
+                <a href="#add-qi" data-toggle="modal" data-target="#add-qi" class="btn btn-primary btn-sm">新建报审</a>
+            </div>
+            <% } %>
+        </div>
+    </div>
+    <div class="content-wrap">
+        <div class="c-body">
+            <div class="sjs-height-0">
+                <table class="table table-bordered table-hover">
+                    <thead>
+                    <tr class="text-center">
+                        <th width="80px" rowspan="2">期数</th>
+                        <th width="70px" rowspan="2">报审月份</th>
+                        <th colspan="4">本期金额</th>
+                        <th colspan="4">截止本期金额</th>
+                        <th width="120px" rowspan="2">审批进度</th>
+                        <th width="100px" rowspan="2">操作</th>
+                    </tr>
+                    <tr class="text-center">
+                        <th>付款</th>
+                        <th>扣款</th>
+                        <th>应付</th>
+                        <th>实付</th>
+                        <th>付款</th>
+                        <th>扣款</th>
+                        <th>应付</th>
+                        <th>实付</th>
+                    </tr>
+                    </thead>
+                    <tbody>
+                    <% for (const s of stages) { %>
+                    <tr>
+                        <td>
+                            <a href="/sp/<%- ctx.subProject.id %>/cost/tender/<%- ctx.tender.id %>/ledger/<%- s.stage_order %>" target="_blank">第 <%- s.stage_order %> 期</a>
+                        </td>
+                        <td class="text-center"><%- s.stage_date %></td>
+                        <td class="text-right"><%- s.pay_tp %></td>
+                        <td class="text-right"><%- s.cut_tp %></td>
+                        <td class="text-right"><%- s.yf_tp %></td>
+                        <td class="text-right"><%- s.sf_tp %></td>
+                        <td class="text-right"><%- s.end_pay_tp %></td>
+                        <td class="text-right"><%- s.end_cut_tp %></td>
+                        <td class="text-right"><%- s.end_yf_tp %></td>
+                        <td class="text-right"><%- s.end_sf_tp %></td>
+                        <td class="<%- auditConst.info[s.audit_status].class %>">
+                            <% if (s.audit_status === auditConst.status.checked && s.final_auditor_str) { %>
+                            <a href="#sp-list" data-toggle="modal" data-target="#sp-list" stage-order="<%- s.stage_order %>"><%- s.final_auditor_str %></a>
+                            <% } else { %>
+                            <% if (s.curAuditors.length > 0) { %>
+                            <% if (s.curAuditors[0].audit_type === auditType.key.common) { %>
+                            <a href="#sp-list" data-toggle="modal" data-target="#sp-list" stage-order="<%- s.stage_order %>"><%- s.curAuditors[0].name %><%if (s.curAuditors[0].role !== '' && s.curAuditors[0].role !== null) { %>-<%- s.curAuditors[0].role %><% } %></a>
+                            <% } else { %>
+                            <a href="#sp-list" data-toggle="modal" data-target="#sp-list" stage-order="<%- s.stage_order %>"><%- ctx.helper.transFormToChinese(s.curAuditors[0].audit_order) + '审' %></a>
+                            <% } %>
+                            <% } %>
+                            <% } %>
+                            <%- auditConst.info[s.audit_status].title %>
+                        </td>
+                        <td class="text-center">
+                            <% if (s.audit_status === auditConst.status.uncheck && s.create_user_id === ctx.session.sessionUser.accountId) { %>
+                            <a href="/sp/<%- ctx.subProject.id %>/cost/tender/<%- ctx.tender.id %>/ledger/<%- s.stage_order %>" target="_blank" class="btn <%- auditConst.info[s.audit_status].btnClass %> btn-sm"><%- auditConst.info[s.audit_status].btnTitle %></a>
+                            <% } else if (s.status === auditConst.status.checkNo && s.user_id === ctx.session.sessionUser.accountId) { %>
+                            <a href="/sp/<%- ctx.subProject.id %>/cost/tender/<%- ctx.tender.id %>/ledger/<%- s.stage_order %>" target="_blank" class="btn <%- auditConst.info[s.audit_status].btnClass %> btn-sm"><%- auditConst.info[s.audit_status].btnTitle %></a>
+                            <% } else if ((s.status === auditConst.status.checking || s.status === auditConst.status.checkNoPre) && s.curAuditors && s.curAuditors.findIndex(x => { return x.aid === ctx.session.sessionUser.accountId; }) >= 0) { %>
+                            <a href="/sp/<%- ctx.subProject.id %>/cost/tender/<%- ctx.tender.id %>/ledger/<%- s.stage_order %>" target="_blank" class="btn <%- auditConst.info[s.audit_status].btnClass %> btn-sm"><%- auditConst.info[s.audit_status].btnTitle %></a>
+                            <% } else { %>
+                            <span class="<%- auditConst.info[s.audit_status].class %>"><%- auditConst.info[s.audit_status].title %></span>
+                            <% } %>
+                        </td>
+                    </tr>
+                    <% } %>
+                    </tbody>
+                </table>
+            </div>
+        </div>
+    </div>
+</div>
+<script>
+    const stages = JSON.parse('<%- JSON.stringify(stages) %>');
+    const auditType = JSON.parse('<%- JSON.stringify(auditType) %>');
+    const auditConst = JSON.parse('<%- JSON.stringify(auditConst) %>');
+</script>

+ 68 - 0
app/view/cost/ledger_list_modal.ejs

@@ -0,0 +1,68 @@
+<div class="modal" id="add-qi" data-backdrop="static" aria-modal="true" role="dialog">
+    <div class="modal-dialog" role="document">
+        <form class="modal-content" action="addStage" method="POST" onsubmit="return checkAddValid();">
+            <div class="modal-header">
+                <h5 class="modal-title">新建报审</h5>
+            </div>
+            <div class="modal-body">
+                <div class="form-group form-group-sm">
+                    <label>期数</label>
+                    <input class="form-control form-control-sm" value="第 <%- (stages.length + 1) %> 期" type="text" readonly="">
+                    <input type="hidden" value="<%- (stages.length + 1) %>" name="stage_order">
+                </div>
+                <div class="form-group form-group-sm">
+                    <label>报审年月</label>
+                    <input class="datepicker-here form-control form-control-sm" autocomplete="off" name="date" placeholder="点击选择年月" data-view="months" data-min-view="months" data-date-format="yyyy-MM" data-language="zh" type="text">
+                </div>
+            </div>
+            <div class="modal-footer">
+                <input type="hidden" name="_csrf_j" value="<%= ctx.csrf %>" />
+                <input type="hidden" name="stage_type" value="<%- stage_type %>" />
+                <button type="button" class="btn btn-sm btn-secondary" data-dismiss="modal">关闭</button>
+                <button type="submit" class="btn btn-sm btn-primary">确定</button>
+            </div>
+        </form>
+    </div>
+</div>
+<!--审批流程/结果-->
+<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 modal-height-500" style="overflow: auto">
+                        <div class="card mt-3">
+                            <ul class="list-group list-group-flush" id="auditor-list">
+                            </ul>
+                        </div>
+                    </div>
+                    <div class="col-8 modal-height-500" style="overflow: auto" id="audit-list">
+                    </div>
+                </div>
+            </div>
+            <div class="modal-footer">
+                <button type="button" class="btn btn-secondary btn-sm" data-dismiss="modal">关闭</button>
+            </div>
+        </div>
+    </div>
+</div>
+<script>
+    $('.datepicker-here').datepicker({
+        autoClose: true,
+    });
+    const checkAddValid = function() {
+        if ($('[name=date]', '#add-qi').val() == '') {
+            toastr.error('请选择报审年月');
+            return false;
+        }
+    }
+    const checkEditValid = function() {
+        if ($('[name=date]', '#edit-qi').val() == '') {
+            toastr.error('请选择报审年月');
+            return false;
+        }
+    }
+</script>

+ 3 - 0
app/view/cost/ledger_menu_list.ejs

@@ -0,0 +1,3 @@
+<nav-menu title="返回" url="/sp/<%= ctx.subProject.id %>/cost/tender/<%- ctx.tender.id %>/ledger" tclass="text-primary" ml="1" icon="fa-chevron-left"></nav-menu>
+<nav-menu title="成本审批" url="/sp/<%= ctx.subProject.id %>/cost/tender/<%- ctx.tender.id %>/ledger/<%- ctx.costStage.stage_order %>" ml="3" active="<%= (ctx.url.indexOf('ledger') >= 0 ? 1 : -1) %>"></nav-menu>
+<% include ./audit_btn.ejs %>

+ 4 - 0
app/view/cost/ledger_modal.ejs

@@ -0,0 +1,4 @@
+<% include ../shares/delete_hint_modal.ejs %>
+<% include ./audit_modal.ejs %>
+<% include ../shares/upload_att.ejs %>
+<% include ../shares/new_tag_modal.ejs %>

+ 14 - 0
app/view/cost/list_menu.ejs

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

+ 6 - 0
app/view/cost/list_menu_list.ejs

@@ -0,0 +1,6 @@
+<nav-menu title="返回" url="/sp/<%= ctx.subProject.id %>/cost" tclass="text-primary" ml="1" icon="fa-chevron-left"></nav-menu>
+<nav-menu title="成本报审" url="/sp/<%= ctx.subProject.id %>/cost/tender/<%- ctx.tender.id %>/ledger" ml="3" active="<%= (ctx.url.indexOf('ledger') >= 0 ? 1 : -1) %>"></nav-menu>
+<nav-menu title="财务账面" url="/sp/<%= ctx.subProject.id %>/cost/tender/<%- ctx.tender.id %>/book" ml="3" active="<%= (ctx.url.indexOf('book') >= 0 ? 1 : -1) %>"></nav-menu>
+<nav-menu title="成本分析" url="/sp/<%= ctx.subProject.id %>/cost/tender/<%- ctx.tender.id %>/analysis" ml="3" active="<%= (ctx.url.indexOf('analysis') >= 0 ? 1 : -1) %>"></nav-menu>
+<nav-menu title="汇总分析" url="/sp/<%= ctx.subProject.id %>/tender/<%- ctx.tender.id %>/cost/gather" ml="3" active="<%= (ctx.url.indexOf('gather') >= 0 ? 1 : -1) %>"></nav-menu>
+<nav-menu title="输出报表" url="/sp/<%= ctx.subProject.id %>/tender/<%- ctx.tender.id %>/cost/report" ml="3" active="<%= (ctx.url.indexOf('report') >= 0 ? 1 : -1) %>"></nav-menu>

+ 16 - 0
app/view/cost/list_mini_menu.ejs

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

+ 17 - 0
app/view/cost/stage_memu.ejs

@@ -0,0 +1,17 @@
+<div class="panel-sidebar" id="sub-menu">
+    <div class="sidebar-title" data-toggle="tooltip" data-placement="right" data-original-title="<%- ctx.tender.data.name %>">
+        <%- `第${ctx.costStage.stage_order}期` %> -
+        <%- (ctx.tender.data.name.length > 11 ? ctx.tender.data.name.substring(0,11) + '...' : ctx.tender.data.name) %>
+    </div>
+    <div class="scrollbar-auto">
+        <% if (ctx.costStage.stage_type === 'ledger') { %><% include ./ledger_menu_list.ejs %><% } %>
+        <% if (ctx.costStage.stage_type === 'book') { %><% include ./book_menu_list.ejs %><% } %>
+        <% if (ctx.costStage.stage_type === 'analysis') { %><% include ./analysis_menu_list.ejs %><% } %>
+        <div class="side-fold"><a href="javascript: void(0)" data-toggle="tooltip" data-placement="top" data-original-title="折叠侧栏" id="to-mini-menu"><i class="fa fa-upload fa-rotate-270"></i></a></div>
+    </div>
+    <script>
+        new Vue({
+            el: '.scrollbar-auto',
+        });
+    </script>
+</div>

+ 18 - 0
app/view/cost/stage_mini_menu.ejs

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

+ 27 - 0
app/view/cost/tender.ejs

@@ -0,0 +1,27 @@
+<div class="panel-content">
+    <div class="panel-title fluid">
+        <div class="title-main  d-flex">
+            <div class="d-inline-block" id="show-level"></div>
+            <div class="ml-auto">
+                <% if (ctx.session.sessionUser.is_admin) { %>
+                <a class="btn btn-sm btn-primary mr-2" href="#set-template" data-toggle="modal" data-target="#set-template">设置模板</a>
+                <% } %>
+            </div>
+        </div>
+    </div>
+    <div class="content-wrap">
+        <div class="sjs-height-0" style="background-color: #fff">
+            <div class="c-body">
+            </div>
+        </div>
+    </div>
+</div>
+<script>
+    const tenders = JSON.parse(unescape('<%- escape(JSON.stringify(tenderList)) %>'));
+    const category = JSON.parse(unescape('<%- escape(JSON.stringify(categoryData)) %>'));
+    const selfCategoryLevel = '<%- (selfCategoryLevel || '') %>';
+    const is_admin = <%- ctx.session.sessionUser.is_admin %>;
+
+    const pid = '<%- ctx.session.sessionProject.id %>';
+    const uphlname = 'user_<%- ctx.session.sessionUser.accountId %>_pro_<% ctx.session.sessionProject.id %>_category_hide_list';
+</script>

+ 54 - 0
app/view/cost/tender_modal.ejs

@@ -0,0 +1,54 @@
+<% include ../shares/tender_permission_modal.ejs %>
+<div class="modal" id="set-template" data-backdrop="static">
+    <div class="modal-dialog" role="document">
+        <div class="modal-content">
+            <div class="modal-header">
+                <h5 class="modal-title">设置模板</h5>
+            </div>
+            <div class="modal-body">
+                <form>
+                    <h6>成本台账</h6>
+                    <div class="form-group row">
+                        <label for="text" class="col-sm-2 col-form-label col-form-label-sm"><h6>数据模板</h6></label>
+                        <div class="col-sm-10">
+                            <select id="cost_ledger_template" class="form-control form-control-sm">
+                                <option value="">请选择成本台账的数据模板</option>
+                                <% for( const t of costLedgerTemplates) { %>
+                                <option value="<%- t.id %>" <% if (ctx.subProject.cost_ledger_template === t.id) { %>selected<% } %>><%- t.name %></option>
+                                <% } %>
+                            </select>
+                        </div>
+                    </div>
+                    <h6>成本分析</h6>
+                    <div class="form-group row">
+                        <label for="text" class="col-sm-2 col-form-label col-form-label-sm"><h6>数据模板</h6></label>
+                        <div class="col-sm-10">
+                            <select id="cost_analysis_template" class="form-control form-control-sm">
+                                <option value="">请选择成本分析的数据模板</option>
+                                <% for( const t of costAnalysisTemplates) { %>
+                                <option value="<%- t.id %>" <% if (ctx.subProject.cost_analysis_template === t.id) { %>selected<% } %>><%- t.name %></option>
+                                <% } %>
+                            </select>
+                        </div>
+                    </div>
+                    <div class="form-group row">
+                        <label for="text" class="col-sm-2 col-form-label col-form-label-sm"><h6>计算模板</h6></label>
+                        <div class="col-sm-10">
+                            <select id="cost_calc_template" class="form-control form-control-sm">
+                                <option value="">请选择成本台账的计算模板</option>
+                                <% for( const t of costCalcTemplates) { %>
+                                <option value="<%- t.id %>" <% if (ctx.subProject.cost_calc_template === t.id) { %>selected<% } %>><%- t.name %></option>
+                                <% } %>
+                            </select>
+                        </div>
+                    </div>
+                    <div class="alert alert-warning p-1 w-100"><i class="fa fa-warning mr-1"></i>修改模板不会影响已经创建的数据。</div>
+                </form>
+            </div>
+            <div class="modal-footer">
+                <button type="button" class="btn btn-sm btn-secondary" data-dismiss="modal">取消</button>
+                <button type="button" class="btn btn-sm btn-primary" id="set-template-ok">确定</button>
+            </div>
+        </div>
+    </div>
+</div>

+ 13 - 13
app/view/phase_pay/index.ejs

@@ -18,19 +18,19 @@
             <div class="sjs-height-0">
             <div class="sjs-height-0">
                 <table class="table table-bordered table-hover">
                 <table class="table table-bordered table-hover">
                     <thead>
                     <thead>
-                    <tr>
-                        <th class="text-center" width="80px">支付期数</th>
-                        <th class="text-center" width="70px">支付月份</th>
-                        <th class="text-center" width="70px">计量期</th>
-                        <th class="text-center" width="100px">本期付款</th>
-                        <th class="text-center" width="100px">本期扣款</th>
-                        <th class="text-center" width="100px">本期应付</th>
-                        <th class="text-center" width="100px">本期实付</th>
-                        <th class="text-center" width="100px">截止本期应付</th>
-                        <th class="text-center" width="100px">截止本期实付</th>
-                        <th class="text-center" width="200px">审批进度</th>
-                        <th class="text-center" width="90px">操作</th>
-                        <th class="text-center" width="200px">备注</th>
+                    <tr class="text-center">
+                        <th width="80px">支付期数</th>
+                        <th width="70px">支付月份</th>
+                        <th width="70px">计量期</th>
+                        <th width="100px">本期付款</th>
+                        <th width="100px">本期扣款</th>
+                        <th width="100px">本期应付</th>
+                        <th width="100px">本期实付</th>
+                        <th width="100px">截止本期应付</th>
+                        <th width="100px">截止本期实付</th>
+                        <th width="200px">审批进度</th>
+                        <th width="90px">操作</th>
+                        <th width="200px">备注</th>
                     </tr>
                     </tr>
                     </thead>
                     </thead>
                     <tbody>
                     <tbody>

+ 8 - 0
config/menu.js

@@ -237,6 +237,14 @@ const menu = {
         children: null,
         children: null,
         controller: 'financial',
         controller: 'financial',
     },
     },
+    cost: {
+        name: '成本管理',
+        icon: 'fa-calculator',
+        display: true,
+        caption: '成本管理',
+        children: null,
+        controller: 'cost',
+    },
     // sum: {
     // sum: {
     //     name: '总分包',
     //     name: '总分包',
     //     icon: 'fa-sitemap',
     //     icon: 'fa-sitemap',

+ 58 - 1
config/web.js

@@ -2522,7 +2522,64 @@ const JsFiles = {
                 ],
                 ],
                 mergeFile: 'cost_tmpl',
                 mergeFile: 'cost_tmpl',
             }
             }
-        }
+        },
+        cost: {
+            tender: {
+                files: [
+                    '/public/js/moment/moment.min.js',
+                ],
+                mergeFiles: [
+                    '/public/js/sub_menu.js',
+                    '/public/js/PinYinOrder.bundle.js',
+                    '/public/js/shares/tender_list_order.js',
+                    '/public/js/shares/show_level.js',
+                    '/public/js/shares/tender_permission.js',
+                    '/public/js/tender_showhide.js',
+                    '/public/js/tender_list_base.js',
+                    '/public/js/cost_tender.js',
+                ],
+                mergeFile: 'cost_tender',
+            },
+            cost_stage: {
+                files: [
+                    '/public/js/datepicker/datepicker.min.js',
+                    '/public/js/datepicker/datepicker.zh.js',
+                ],
+                mergeFiles: [
+                    '/public/js/component/menu.js',
+                    '/public/js/sub_menu.js',
+                    '/public/js/cost_stage.js',
+                ],
+                mergeFile: 'cost_stage',
+            },
+            cost_stage_ledger: {
+                files: [
+                    '/public/js/js-xlsx/xlsx.full.min.js',
+                    '/public/js/js-xlsx/xlsx.utils.js',
+                    '/public/js/spreadjs/sheets/v11/gc.spread.sheets.all.11.2.2.min.js',
+                    '/public/js/decimal.min.js',
+                    '/public/js/math.min.js',
+                    '/public/js/axios/axios.min.js', '/public/js/js-xlsx/jszip.min.js',
+                    '/public/js/file-saver/FileSaver.js',
+                    '/public/js/component/menu.js',
+                ],
+                mergeFiles: [
+                    '/public/js/sub_menu.js',
+                    '/public/js/div_resizer.js',
+                    '/public/js/spreadjs_rela/spreadjs_zh.js',
+                    '/public/js/shares/sjs_setting.js',
+                    '/public/js/shares/cs_tools.js',
+                    '/public/js/zh_calc.js',
+                    '/public/js/shares/ali_oss.js',
+                    '/public/js/shares/new_tag.js',
+                    '/public/js/path_tree.js',
+                    '/public/js/shares/tools_att.js',
+                    '/public/js/shares/common_audit.js',
+                    '/public/js/cost_stage_ledger.js',
+                ],
+                mergeFile: 'cost_stage_ledger',
+            }
+        },
     },
     },
 };
 };
 
 

+ 183 - 0
sql/update.sql

@@ -34,6 +34,189 @@ ADD COLUMN `audit_group_limit` tinyint(4) NOT NULL DEFAULT 0 COMMENT '组内是
 ADD COLUMN `audit_checkno_valid` tinyint(4) UNSIGNED NOT NULL DEFAULT 1 COMMENT '是否允许审批退回' AFTER `audit_group_limit`,
 ADD COLUMN `audit_checkno_valid` tinyint(4) UNSIGNED NOT NULL DEFAULT 1 COMMENT '是否允许审批退回' AFTER `audit_group_limit`,
 ADD COLUMN `audit_group_need` tinyint(4) UNSIGNED NOT NULL DEFAULT 1 COMMENT '组内最少审批人数' AFTER `audit_checkno_valid`;
 ADD COLUMN `audit_group_need` tinyint(4) UNSIGNED NOT NULL DEFAULT 1 COMMENT '组内最少审批人数' AFTER `audit_checkno_valid`;
 
 
+ALTER TABLE `zh_tender_permission`
+ADD COLUMN `cost` varchar(255) CHARACTER SET utf8 COLLATE utf8_unicode_ci NOT NULL DEFAULT '' COMMENT '成本管理权限(,分隔,具体见代码定义)' AFTER `schedule`;
+
+ALTER TABLE `zh_bills_template_list`
+ADD COLUMN `sub_type` tinyint(4) UNSIGNED NOT NULL DEFAULT 0 COMMENT '二级分类' AFTER `remark`;
+
+ALTER TABLE `zh_sub_project`
+ADD COLUMN `cost_ledger_template` int(11) UNSIGNED NOT NULL DEFAULT 0 COMMENT '成本台账模板id' AFTER `common_json`,
+ADD COLUMN `cost_analysis_template` int(11) UNSIGNED NOT NULL DEFAULT 0 COMMENT '成本分析模板id' AFTER `cost_ledger_template`,
+ADD COLUMN `cost_calc_template` varchar(36) NOT NULL DEFAULT '' COMMENT '成本分析,计算模板id' AFTER `cost_analysis_template`;
+
+CREATE TABLE `zh_cost_stage`  (
+  `id` varchar(36) NOT NULL COMMENT 'uuid',
+  `tid` int(11) UNSIGNED NOT NULL COMMENT '标段id(zh_tender.id)',
+  `stage_type` varchar(20) NULL COMMENT '期类型(台账ledger/账面book/分析analysis)',
+  `stage_order` int(11) UNSIGNED NOT NULL COMMENT '期序号',
+  `create_user_id` int(11) NOT NULL COMMENT '创建人id',
+  `create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+  `update_user_id` int(11) UNSIGNED NOT NULL COMMENT '最后修改人id',
+  `update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '最后修改时间',
+  `stage_date` varchar(20) NOT NULL COMMENT '报审月份',
+  `decimal` varchar(50) CHARACTER SET utf8 COLLATE utf8_unicode_ci NOT NULL DEFAULT '{"tp": 6}' COMMENT '小数位数',
+  `audit_times` tinyint(4) NOT NULL COMMENT '审批轮次',
+  `audit_status` tinyint(4) NOT NULL COMMENT '审批状态',
+  `audit_max_sort` tinyint(4) UNSIGNED NOT NULL DEFAULT 0 COMMENT '最大审批排序',
+  `audit_begin_time` timestamp NULL COMMENT '开始审批时间',
+  `audit_end_time` timestamp NULL COMMENT '审批结束时间',
+  `final_auditor_str` varchar(255) NOT NULL DEFAULT '' COMMENT '终审缓存信息',
+  `stage_tp` json NOT NULL COMMENT '金额',
+  `stage_pre_tp` json NOT NULL COMMENT '截止上期金额',
+  PRIMARY KEY (`id`),
+  INDEX `idx_tid_type`(`tid`, `stage_type`) USING BTREE
+);
+
+CREATE TABLE `zh_cost_stage_audit`  (
+  `id` bigint(20) NOT NULL AUTO_INCREMENT,
+  `tid` int(11) UNSIGNED NOT NULL COMMENT '标段id',
+  `stage_id` varchar(36) CHARACTER SET utf8 COLLATE utf8_unicode_ci NOT NULL DEFAULT '' COMMENT '期id',
+  `audit_id` int(11) UNSIGNED NOT NULL COMMENT '流程参与人id(含原报)',
+  `name` varchar(30) CHARACTER SET utf8 COLLATE utf8_unicode_ci NOT NULL DEFAULT '' COMMENT '参与人-姓名',
+  `company` varchar(60) CHARACTER SET utf8 COLLATE utf8_unicode_ci NOT NULL DEFAULT '' COMMENT '参与人-单位',
+  `role` varchar(30) CHARACTER SET utf8 COLLATE utf8_unicode_ci NOT NULL DEFAULT '' COMMENT '参与人-角色',
+  `mobile` varchar(15) CHARACTER SET utf8 COLLATE utf8_unicode_ci NOT NULL DEFAULT '' COMMENT '参与人-电话',
+  `audit_times` int(11) UNSIGNED NOT NULL DEFAULT 1 COMMENT '审批次数',
+  `audit_order` int(11) UNSIGNED NOT NULL DEFAULT 0 COMMENT '预定流程顺序',
+  `audit_type` int(11) UNSIGNED NOT NULL DEFAULT 0 COMMENT '流程类型',
+  `active_order` int(11) UNSIGNED NOT NULL DEFAULT 0 COMMENT '实际流程顺序',
+  `audit_status` int(11) UNSIGNED NOT NULL DEFAULT 0 COMMENT '流程状态',
+  `audit_time` timestamp NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '流程结束时间',
+  `create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+  `update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '最后更新时间',
+  `opinion` varchar(1000) CHARACTER SET utf8 COLLATE utf8_unicode_ci NOT NULL DEFAULT '' COMMENT '意见',
+  PRIMARY KEY (`id`) USING BTREE
+) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_unicode_ci ROW_FORMAT = Dynamic;
+
+CREATE TABLE `zh_cost_stage_file`  (
+  `id` varchar(36) CHARACTER SET utf8 COLLATE utf8_unicode_ci NOT NULL COMMENT 'uuid',
+  `tid` int(11) UNSIGNED NOT NULL COMMENT '标段id',
+  `stage_id` varchar(36) CHARACTER SET utf8 COLLATE utf8_unicode_ci NOT NULL COMMENT '期stage_id(zh_cost_stage.id)',
+  `stage_type` varchar(20) CHARACTER SET utf8 COLLATE utf8_unicode_ci NOT NULL DEFAULT '' COMMENT '类型(ledger/book/analysis/...)',
+  `rela_id` varchar(36) CHARACTER SET utf8 COLLATE utf8_unicode_ci NOT NULL COMMENT 'uuid(zh_cost_stage_ledger.id/...)',
+  `rela_sub_id` varchar(36) CHARACTER SET utf8 COLLATE utf8_unicode_ci NOT NULL DEFAULT '' COMMENT 'uuid(zh_cost_stage_detail.id/...)',
+  `filename` varchar(255) CHARACTER SET utf8 COLLATE utf8_unicode_ci NOT NULL COMMENT '文件名',
+  `fileext` varchar(10) CHARACTER SET utf8 COLLATE utf8_unicode_ci NOT NULL COMMENT '文件后缀',
+  `filesize` int(11) NOT NULL COMMENT '文件大小',
+  `filepath` varchar(255) CHARACTER SET utf8 COLLATE utf8_unicode_ci NOT NULL COMMENT '文件存储路径',
+  `user_id` int(11) UNSIGNED NOT NULL COMMENT '用户id(zh_project_account.id)',
+  `user_name` varchar(50) CHARACTER SET utf8 COLLATE utf8_unicode_ci NOT NULL DEFAULT '' COMMENT '用户名(缓存)',
+  `user_company` varchar(255) CHARACTER SET utf8 COLLATE utf8_unicode_ci NOT NULL COMMENT '公司(缓存)',
+  `user_role` varchar(50) CHARACTER SET utf8 COLLATE utf8_unicode_ci NOT NULL COMMENT '角色(缓存)',
+  `create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+  `update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '最后更新时间',
+  `is_deleted` tinyint(4) UNSIGNED NOT NULL DEFAULT 0 COMMENT '是否删除',
+  PRIMARY KEY (`id`) USING BTREE
+) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_unicode_ci ROW_FORMAT = Dynamic;
+
+CREATE TABLE `zh_cost_stage_ledger` (
+  `id` varchar(36) COLLATE utf8_unicode_ci NOT NULL DEFAULT '' COMMENT 'uuid',
+  `cost_id` varchar(36) COLLATE utf8_unicode_ci NOT NULL DEFAULT '' COMMENT 'uuid(不同期保持统一)',
+  `tender_id` int(11) unsigned NOT NULL COMMENT '标段id',
+  `stage_id` varchar(36) COLLATE utf8_unicode_ci NOT NULL DEFAULT '' COMMENT '期id',
+  `tree_id` int(11) NOT NULL COMMENT '节点id',
+  `tree_pid` int(11) NOT NULL COMMENT '父节点id',
+  `tree_level` tinyint(4) NOT NULL COMMENT '层级',
+  `tree_order` mediumint(4) NOT NULL COMMENT '同级排序',
+  `tree_full_path` varchar(255) COLLATE utf8_unicode_ci NOT NULL COMMENT '层级定位辅助字段parent.full_path-tree_id',
+  `tree_is_leaf` tinyint(4) unsigned NOT NULL DEFAULT '1' COMMENT '是否叶子节点,界面显示辅助字段',
+  `code` varchar(50) COLLATE utf8_unicode_ci NOT NULL DEFAULT '' COMMENT '编号',
+  `name` varchar(255) COLLATE utf8_unicode_ci NOT NULL DEFAULT '' COMMENT '名称',
+  `unit` varchar(255) COLLATE utf8_unicode_ci NOT NULL DEFAULT '' COMMENT '单位',
+  `pre_pay_tp` decimal(24,8) NOT NULL DEFAULT '0.00000000' COMMENT '截止上期-付款',
+  `pre_cut_tp` decimal(24,8) NOT NULL DEFAULT '0.00000000' COMMENT '截止上期-扣款',
+  `pre_yf_tp` decimal(24,8) NOT NULL DEFAULT '0.00000000' COMMENT '截止上期-应付',
+  `pre_sf_tp` decimal(24,8) NOT NULL DEFAULT '0.00000000' COMMENT '截止上期-实付',
+  `pay_tp` decimal(24,8) NOT NULL DEFAULT '0.00000000' COMMENT '付款',
+  `cut_tp` decimal(24,8) NOT NULL DEFAULT '0.00000000' COMMENT '扣款',
+  `yf_tp` decimal(24,8) NOT NULL DEFAULT '0.00000000' COMMENT '应付',
+  `sf_tp` decimal(24,8) NOT NULL DEFAULT '0.00000000' COMMENT '实付',
+  `postil` varchar(1000) COLLATE utf8_unicode_ci NOT NULL DEFAULT '' COMMENT '本期批注',
+  `memo` varchar(1000) COLLATE utf8_unicode_ci NOT NULL DEFAULT '' COMMENT '备注',
+  `add_user_id` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '创建人',
+  `add_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+  `update_user_id` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '最后编辑人',
+  `update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '最后编辑时间',
+  `read_pay_tp` decimal(24,8) NOT NULL DEFAULT '0.00000000' COMMENT '本期付款-只读',
+  `read_cut_tp` decimal(24,8) NOT NULL DEFAULT '0.00000000' COMMENT '本期扣款-只读',
+  `read_yf_tp` decimal(24,8) NOT NULL DEFAULT '0.00000000' COMMENT '本期应付-只读',
+  `read_sf_tp` decimal(24,8) NOT NULL DEFAULT '0.00000000' COMMENT '本期实付-只读',
+  `calc_his` json DEFAULT NULL COMMENT '本期历史',
+  `is_used` tinyint(4) UNSIGNED NOT NULL DEFAULT 0 COMMENT '是否已使用',
+  `is_deal` tinyint(4) UNSIGNED NOT NULL DEFAULT 0 COMMENT '是否为合同节点',
+  PRIMARY KEY (`id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;
+
+CREATE TABLE `zh_cost_stage_detail`  (
+  `id` varchar(36) NOT NULL COMMENT 'uuid',
+  `tender_id` int(11) NOT NULL COMMENT '标段id',
+  `stage_id` varchar(36) CHARACTER SET utf8 COLLATE utf8_unicode_ci NOT NULL COMMENT '期id',
+  `ledger_id` varchar(36) NOT NULL COMMENT '所属台账id(zh_stage_ledger.id)',
+  `cost_id` varchar(36) CHARACTER SET utf16le NOT NULL COMMENT '所属台账cost_id(不同期一致zh_stage_ledger.cost_id)',
+  `d_order` int(11) NOT NULL COMMENT '排序',
+  `code` varchar(50) CHARACTER SET utf8 COLLATE utf8_unicode_ci NOT NULL DEFAULT '' COMMENT '编号',
+  `name` varchar(255) CHARACTER SET utf16 NOT NULL DEFAULT '' COMMENT '名称',
+  `party_b` varchar(255) CHARACTER SET utf8 COLLATE utf8_unicode_ci NOT NULL DEFAULT '' COMMENT '乙方',
+  `pre_pay_tp` decimal(24, 8) NOT NULL DEFAULT 0.00000000 COMMENT '截止上期-付款',
+  `pre_cut_tp` decimal(24, 8) NOT NULL DEFAULT 0.00000000 COMMENT '截止上期-扣款',
+  `pre_yf_tp` decimal(24, 8) NOT NULL DEFAULT 0.00000000 COMMENT '截止上期-应付',
+  `pre_sf_tp` decimal(24, 8) NOT NULL DEFAULT 0.00000000 COMMENT '截止上期-实付',
+  `pay_tp` decimal(24, 8) NOT NULL DEFAULT 0.00000000 COMMENT '付款',
+  `cut_tp` decimal(24, 8) NOT NULL DEFAULT 0.00000000 COMMENT '扣款',
+  `yf_tp` decimal(24, 8) NOT NULL DEFAULT 0.00000000 COMMENT '应付',
+  `sf_tp` decimal(24, 8) NOT NULL DEFAULT 0.00000000 COMMENT '实付',
+  `postil` varchar(1000) CHARACTER SET utf8 COLLATE utf8_unicode_ci NOT NULL DEFAULT '' COMMENT '本期批注',
+  `memo` varchar(1000) CHARACTER SET utf8 COLLATE utf8_unicode_ci NOT NULL DEFAULT '' COMMENT '备注',
+  `add_user_id` int(11) UNSIGNED NOT NULL DEFAULT 0 COMMENT '创建人',
+  `add_time` timestamp(0) NOT NULL DEFAULT CURRENT_TIMESTAMP(0) COMMENT '创建时间',
+  `update_user_id` int(11) UNSIGNED NOT NULL DEFAULT 0 COMMENT '最后编辑人',
+  `update_time` timestamp(0) NOT NULL DEFAULT CURRENT_TIMESTAMP(0) ON UPDATE CURRENT_TIMESTAMP(0) COMMENT '最后编辑时间',
+  `read_pay_tp` decimal(24, 8) NOT NULL DEFAULT 0.00000000 COMMENT '本期付款-只读',
+  `read_cut_tp` decimal(24, 8) NOT NULL DEFAULT 0.00000000 COMMENT '本期扣款-只读',
+  `read_yf_tp` decimal(24, 8) NOT NULL DEFAULT 0.00000000 COMMENT '本期应付-只读',
+  `read_sf_tp` decimal(24, 8) NOT NULL DEFAULT 0.00000000 COMMENT '本期实付-只读',
+  `calc_his` json NULL COMMENT '本期历史',
+  `is_used` tinyint(4) UNSIGNED NOT NULL DEFAULT 0 COMMENT '是否已使用',
+  `is_deal` tinyint(4) UNSIGNED NOT NULL DEFAULT 0 COMMENT '是否为合同节点',
+  PRIMARY KEY (`id`)
+);
+
+CREATE TABLE `zh_cost_stage_tag`  (
+  `id` varchar(36) NOT NULL COMMENT 'uuid',
+  `tender_id` int(11) NOT NULL COMMENT '标段id(zh_tender.id)',
+  `stage_id` varchar(11) NOT NULL COMMENT '期id(zh_cost_stage.id)',
+  `stage_type` varchar(20) CHARACTER SET utf8 COLLATE utf8_unicode_ci NOT NULL DEFAULT '' COMMENT '期类型(ledger/book/analysis/...)',
+  `rela_id` varchar(36) NOT NULL COMMENT '关联id(zh_cost_stage_ledger.id/...)',
+  `rela_sub_id` varchar(36) NOT NULL COMMENT '次关联id(zh_cost_stage_detail.id/...)',
+  `create_user_id` int(11) NOT NULL COMMENT '创建人id(zh_project_accnount.id)',
+  `create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+  `update_user_id` int(11) NOT NULL COMMENT '最后修改人id(zh_project_account.id)',
+  `update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '最后修改时间',
+  `share` tinyint(1) NOT NULL DEFAULT 0 COMMENT '是否共享给其他参与人',
+  `color` varchar(7) CHARACTER SET ascii COLLATE ascii_general_ci NOT NULL DEFAULT '' COMMENT '书签颜色',
+  `comment` text CHARACTER SET utf8 COLLATE utf8_unicode_ci NULL COMMENT '批注',
+  PRIMARY KEY (`id`)
+);
+
+CREATE TABLE `zh_cost_stage_book` (
+  `id` varchar(36) COLLATE utf8_unicode_ci NOT NULL DEFAULT '' COMMENT 'uuid(zh_cost_stage_ledger.id)',
+  `cost_id` varchar(36) COLLATE utf8_unicode_ci NOT NULL DEFAULT '' COMMENT 'uuid(不同期保持统一zh_cost_stage_ledger.cost_id)',
+  `tender_id` int(11) unsigned NOT NULL COMMENT '标段id',
+  `stage_id` varchar(36) COLLATE utf8_unicode_ci NOT NULL DEFAULT '' COMMENT '期id',
+  `pre_in_tp` decimal(24,8) NOT NULL DEFAULT '0.00000000' COMMENT '截止上期-入账金额',
+  `in_tp` decimal(24,8) NOT NULL DEFAULT '0.00000000' COMMENT '入账金额',
+  `postil` varchar(1000) COLLATE utf8_unicode_ci NOT NULL DEFAULT '' COMMENT '本期批注',
+  `memo` varchar(1000) COLLATE utf8_unicode_ci NOT NULL DEFAULT '' COMMENT '备注',
+  `add_user_id` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '创建人',
+  `add_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+  `update_user_id` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '最后编辑人',
+  `update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '最后编辑时间',
+  `read_in_tp` decimal(24,8) NOT NULL DEFAULT '0.00000000' COMMENT '本期入账-只读',
+  `calc_his` json DEFAULT NULL COMMENT '本期历史',
+  PRIMARY KEY (`id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;
+
 ------------------------------------
 ------------------------------------
 -- 表数据
 -- 表数据
 ------------------------------------
 ------------------------------------