瀏覽代碼

Merge branch 'master' of http://192.168.1.41:3000/maixinrong/Calculation

TonyKang 5 年之前
父節點
當前提交
ffb57074a0
共有 81 個文件被更改,包括 4285 次插入545 次删除
  1. 9 0
      app/base/base_bills_service.js
  2. 1 1
      app/base/base_controller.js
  3. 9 15
      app/base/base_tree_service.js
  4. 1 1
      app/const/account_group.js
  5. 30 0
      app/const/material.js
  6. 10 1
      app/const/sms_type.js
  7. 2 1
      app/const/spread.js
  8. 5 9
      app/controller/ledger_controller.js
  9. 412 0
      app/controller/material_controller.js
  10. 2 5
      app/controller/revise_controller.js
  11. 50 44
      app/controller/stage_controller.js
  12. 21 1
      app/controller/tender_controller.js
  13. 23 1
      app/extend/helper.js
  14. 6 7
      app/lib/pay_calc.js
  15. 120 0
      app/middleware/material_check.js
  16. 10 7
      app/middleware/stage_check.js
  17. 3 3
      app/public/js/change.js
  18. 38 6
      app/public/js/global.js
  19. 188 52
      app/public/js/ledger.js
  20. 11 9
      app/public/js/ledger_audit.js
  21. 2 1
      app/public/js/ledger_tree_col.js
  22. 207 0
      app/public/js/material.js
  23. 147 0
      app/public/js/material_audit.js
  24. 56 0
      app/public/js/math.min.js
  25. 108 0
      app/public/js/measure_material.js
  26. 5 0
      app/public/js/measure_stage.js
  27. 4 21
      app/public/js/revise.js
  28. 0 17
      app/public/js/revise_history.js
  29. 3 3
      app/public/js/setting.js
  30. 53 6
      app/public/js/spreadjs_rela/spreadjs_zh.js
  31. 30 63
      app/public/js/stage.js
  32. 2 2
      app/public/js/stage_audit.js
  33. 1 1
      app/public/js/stage_change.js
  34. 8 3
      app/public/js/stage_compare.js
  35. 2 2
      app/public/js/stage_detail.js
  36. 1 1
      app/public/js/stage_im.js
  37. 408 111
      app/public/js/stage_pay.js
  38. 52 10
      app/public/js/tender.js
  39. 17 1
      app/router.js
  40. 10 0
      app/service/ledger.js
  41. 172 0
      app/service/material.js
  42. 549 0
      app/service/material_audit.js
  43. 77 0
      app/service/material_bills.js
  44. 77 0
      app/service/material_list.js
  45. 11 0
      app/service/pos.js
  46. 4 4
      app/service/project_account.js
  47. 35 44
      app/service/revise_audit.js
  48. 1 0
      app/service/stage.js
  49. 1 1
      app/service/stage_audit.js
  50. 4 10
      app/service/stage_bills.js
  51. 33 3
      app/service/stage_bills_final.js
  52. 1 1
      app/service/stage_change.js
  53. 19 14
      app/service/stage_pay.js
  54. 27 12
      app/service/stage_pos.js
  55. 3 3
      app/service/stage_pos_final.js
  56. 111 0
      app/service/tender_info.js
  57. 1 1
      app/view/change/index.ejs
  58. 0 21
      app/view/change/info.ejs
  59. 21 4
      app/view/layout/layout.ejs
  60. 0 2
      app/view/ledger/explode.ejs
  61. 21 0
      app/view/material/audit_btn.ejs
  62. 628 0
      app/view/material/audit_modal.ejs
  63. 87 0
      app/view/material/index.ejs
  64. 63 0
      app/view/material/info.ejs
  65. 2 0
      app/view/material/info_modal.ejs
  66. 30 0
      app/view/material/material_sub_menu.ejs
  67. 29 0
      app/view/material/material_sub_mini_menu.ejs
  68. 144 0
      app/view/material/modal.ejs
  69. 2 2
      app/view/measure/stage.ejs
  70. 2 2
      app/view/measure/stage_modal.ejs
  71. 1 1
      app/view/revise/info_modal.ejs
  72. 2 0
      app/view/stage/audit_modal.ejs
  73. 8 3
      app/view/stage/compare_modal.ejs
  74. 1 1
      app/view/stage/detail.ejs
  75. 2 2
      app/view/stage/gather.ejs
  76. 2 1
      app/view/stage/index.ejs
  77. 5 2
      app/view/stage/pay.ejs
  78. 11 1
      app/view/tender/detail.ejs
  79. 5 0
      app/view/tender/tender_sub_menu.ejs
  80. 6 1
      app/view/tender/tender_sub_mini_menu.ejs
  81. 20 4
      config/web.js

+ 9 - 0
app/base/base_bills_service.js

@@ -19,6 +19,15 @@ const tpFields = ['sgfh_tp', 'sjcl_tp', 'qtcl_tp', 'total_price', 'deal_tp'];
 
 class BaseBillsSerivce extends TreeService {
 
+    // 继承方法
+    clearParentingData(data) {
+        data.unit_price = null;
+        data.quantity = null;
+        data.total_price = null;
+        data.deal_qty = null;
+        data.deal_tp = null;
+    }
+
     /**
      * 从标准数据中提取有效数据
      * @param {Object} stdData - 从标准库中查询所得

+ 1 - 1
app/base/base_controller.js

@@ -133,7 +133,7 @@ class BaseController extends Controller {
         }
     }
 
-    async checkMeasureType (mt) {
+    checkMeasureType (mt) {
         if (this.ctx.tender.data.measure_type !== mt) {
             throw '该模式下不可提交此数据';
         }

+ 9 - 15
app/base/base_tree_service.js

@@ -632,6 +632,13 @@ class TreeService extends Service {
     }
 
     /**
+     * 节点成为父项时,可能需要修改父项数据,供子类继承
+     * @param data
+     */
+    clearParentingData(data) {
+    }
+
+    /**
      * 升级selectData, 同步修改所有子节点
      * @param {Object} selectData - 升级操作,选中节点
      * @return {Object}
@@ -763,11 +770,7 @@ class TreeService extends Service {
                     const nexts = await this.getNextsData(mid, parent[this.setting.kid], last[this.setting.order]);
                     if (nexts.length > 0) {
                         updateData.is_leaf = false;
-                        // updateData.unit_price = null;
-                        // updateData.quantity = null;
-                        // updateData.total_price = null;
-                        // updateData.deal_qty = null;
-                        // updateData.deal_tp = null;
+                        this.clearParentingData(updateData);
                     }
                 }
                 await this.transaction.update(this.tableName, updateData);
@@ -832,9 +835,6 @@ class TreeService extends Service {
      * @return {Array} - 发生改变的数据
      */
     async downLevelNode(mid, kid, count) {
-        console.log(mid);
-        console.log(kid);
-        console.log(count);
         if (!count) count = 1;
         const selects = await this.getDataByKidAndCount(mid, kid, count);
         if (!selects) throw '降级节点数据错误';
@@ -842,8 +842,6 @@ class TreeService extends Service {
         const pre = await this.getDataByParentAndOrder(mid, first[this.setting.pid], first[this.setting.order] - 1);
         if (!pre) throw '节点不可降级';
         const preLastChild = await this.getLastChildData(mid, pre[this.setting.kid]);
-        console.log(selects);
-        console.log(pre);
 
         const newPath = [];
         this.transaction = await this.db.beginTransaction();
@@ -868,11 +866,7 @@ class TreeService extends Service {
             // 选中节点--前兄弟节点 is_leaf应为false, 清空计算相关字段
             const updateData2 = { id: pre.id };
             updateData2[this.setting.isLeaf] = false;
-            // updateData2.unit_price = null;
-            // updateData2.quantity = null;
-            // updateData2.total_price = null;
-            // updateData2.deal_qty = null;
-            // updateData2.deal_tp = null;
+            this.clearParentingData(updateData2);
             await this.transaction.update(this.tableName, updateData2);
             await this.transaction.commit();
             this.transaction = null;

+ 1 - 1
app/const/account_group.js

@@ -18,10 +18,10 @@ const accountGroup = {
 
 const group = [];
 group[accountGroup.JSDW] = '建设单位';
+group[accountGroup.DJDW] = '代建单位';
 group[accountGroup.JLDW] = '监理单位';
 group[accountGroup.SGDW] = '施工单位';
 group[accountGroup.SJDW] = '设计单位';
-group[accountGroup.DJDW] = '代建单位';
 group[accountGroup.GZSJ] = '跟踪审计';
 
 module.exports = {

+ 30 - 0
app/const/material.js

@@ -0,0 +1,30 @@
+'use strict';
+
+/**
+ * 材料调差
+ *
+ * @author ELlisran
+ * @date 2019/10/20
+ * @version
+ */
+
+// 调差类型
+const t_type = [
+    { text: '消耗量', value: 1 },
+    { text: '费用', value: 2 },
+];
+// 工料分类
+const m_type = [
+    { text: '分类', value: 1 },
+    { text: '钢材', value: 2 },
+    { text: '地材', value: 3 },
+    { text: '油料', value: 4 },
+    { text: '水泥', value: 5 },
+    { text: '半成品', value: 6 },
+    { text: '其他', value: 7 },
+];
+
+module.exports = {
+    t_type,
+    m_type,
+};

+ 10 - 1
app/const/sms_type.js

@@ -11,11 +11,12 @@ const smsConst = {
     TZ: 'TZ',
     JL: 'JL',
     BG: 'BG',
+    XD: 'XD'
 };
 const judgeConst = {
     approval: 1,
     result: 2,
-}
+};
 const smsType = {
     TZ: {
         name: '台账审批',
@@ -25,6 +26,14 @@ const smsType = {
             { title: '审批结果', value: 2 },
         ],
     },
+    XD: {
+        name: '修订审批',
+        path: '',
+        children: [
+            { title: '需要我审批', value: 1 },
+            { title: '审批结果', value: 2 },
+        ],
+    },
     JL: {
         name: '计量审批',
         path: '',

+ 2 - 1
app/const/spread.js

@@ -415,6 +415,7 @@ measure.compare.pos = {
     defaultRowHeight: 21,
 };
 
+
 module.exports = {
     withCl,
     withoutCl,
@@ -425,4 +426,4 @@ module.exports = {
     stageCompare,
     filterCols: { tzWithoutCols, dgnCols, clCols, stageDgnCols},
     measure,
-};
+};

+ 5 - 9
app/controller/ledger_controller.js

@@ -115,7 +115,6 @@ module.exports = app => {
                 const content = auditors.length > 0 ? await ctx.service.ledgerAuditContent.getAllDataByCondition({
                     where: { tender_id: tender.id, times, audit_id: auditors[0].audit_id },
                 }) : null;
-                const ledgerData = await ctx.service.ledger.getData(tender.id);
                 const user = await ctx.service.projectAccount.getAccountInfoById(ctx.tender.data.user_id);
                 const auditHistory = [];
                 if (ctx.tender.data.ledger_times > 1) {
@@ -133,7 +132,6 @@ module.exports = app => {
                     user,
                     auditHistory,
                     content,
-                    ledger: JSON.stringify(ledgerData),
                     ledgerSpreadSetting: JSON.stringify(ledgerSpread),
                     posSpreadSetting: JSON.stringify(posSpread),
                     tenderMenu,
@@ -423,14 +421,12 @@ module.exports = app => {
          * @param ctx
          * @return {Promise<void>}
          */
-        async pos(ctx) {
+        async loadExplodeData(ctx) {
             try {
-                await this.checkMeasureType(measureType.tz.value);
-                const condition = JSON.parse(ctx.request.body.data) || {};
-                condition.tid = ctx.tender.id;
-
-                const posData = await ctx.service.pos.getPosData(condition);
-                ctx.body = { err: 0, msg: '', data: posData };
+                const ledgerData = await ctx.service.ledger.getData(ctx.tender.id);
+                const posData = this.ctx.tender.data.measure_type === measureType.tz.value
+                    ? await ctx.service.pos.getPosData({tid: ctx.tender.id}) : [];
+                ctx.body = { err: 0, msg: '', data: {bills: ledgerData, pos: posData} };
             } catch (err) {
                 this.log(err);
                 ctx.body = { err: 1, msg: err.toString(), data: [] };

+ 412 - 0
app/controller/material_controller.js

@@ -0,0 +1,412 @@
+'use strict';
+
+/**
+ *
+ *
+ * @author EllisRan
+ * @date 2018/6/20
+ * @version
+ */
+
+const moment = require('moment');
+const auditConst = require('../const/audit').material;
+const auditStageConst = require('../const/audit').stage;
+const tenderConst = require('../const/tender');
+const measureType = tenderConst.measureType;
+const accountGroup = require('../const/account_group').group;
+const materialConst = require('../const/material');
+
+module.exports = app => {
+    class MaterialController extends app.BaseController {
+        /**
+         * 构造函数
+         *
+         * @param {Object} ctx - egg全局变量
+         * @return {void}
+         */
+        constructor(ctx) {
+            super(ctx);
+            ctx.showProject = true;
+            ctx.showTender = true;
+            ctx.showTitle = true;
+        }
+
+        /**
+         * 期列表(Get)
+         * @param ctx
+         * @returns {Promise<void>}
+         */
+        async index(ctx) {
+            try {
+                const renderData = {
+                    tender: ctx.tender.data,
+                    tenderMenu: this.menu.tenderMenu,
+                    preUrl: '/tender/' + ctx.tender.id,
+                    auditConst,
+                    auditConst2: JSON.stringify(auditConst),
+                };
+                renderData.materials = await ctx.service.material.getValidMaterials(ctx.tender.id);
+                // 获取未选中和已完成的计量期
+                const stages = await ctx.service.stage.getAllDataByCondition({ where: { tid: ctx.tender.id, status: auditStageConst.status.checked } });
+                for (const s of renderData.materials) {
+                    // s.curAuditor = null;
+                    // 根据期状态返回展示用户
+                    s.curAuditor = await ctx.service.materialAudit.getAuditorByStatus(s.id, s.status, s.times);
+                    const materialStageList = s.stage_id.split(',');
+                    for (const ms of materialStageList) {
+                        const index = stages.findIndex(function(item) {
+                            return item.id === parseInt(ms);
+                        });
+                        stages.splice(index, 1);
+                    }
+                }
+                renderData.stages = stages;
+                await this.layout('material/index.ejs', renderData, 'material/modal.ejs');
+            } catch (err) {
+                this.log(err);
+                ctx.redirect(this.menu.menu.dashboard.url);
+            }
+        }
+
+        /**
+         * 期审批流程(Get)
+         * @param ctx
+         * @returns {Promise<void>}
+         */
+        async materialAuditors(ctx) {
+            try {
+                const responseData = {
+                    err: 0, msg: '', data: {},
+                };
+                const order = JSON.parse(ctx.request.body.data).order;
+                const tenderId = ctx.params.id;
+                const materialInfo = await ctx.service.material.getDataByCondition({ tid: tenderId, order });
+                // 获取审批流程中右边列表
+                const auditHistory = [];
+                const times = materialInfo.status === auditConst.status.checkNo ? materialInfo.times - 1 : materialInfo.times;
+                if (times >= 1) {
+                    for (let i = 1; i <= times; i++) {
+                        auditHistory.push(await ctx.service.materialAudit.getAuditors(materialInfo.id, i));
+                    }
+                }
+                responseData.data.auditHistory = auditHistory;
+                // 获取审批流程中左边列表
+                responseData.data.auditors = await ctx.service.materialAudit.getAuditGroupByList(materialInfo.id, times);
+                // 获取原报信息
+                const materialAuditor = await ctx.service.projectAccount.getAccountInfoById(materialInfo.user_id);
+                responseData.data.materialAuditor = materialAuditor;
+                ctx.body = responseData;
+            } catch (error) {
+                this.log(error);
+                ctx.body = { err: 1, msg: error.toString(), data: null };
+            }
+        }
+
+        /**
+         * 新增期(Post)
+         * @param ctx
+         * @returns {Promise<void>}
+         */
+        async add(ctx) {
+            try {
+                if (ctx.session.sessionUser.accountId !== ctx.tender.data.user_id) {
+                    throw '您无权创建材料调差期';
+                }
+                const data = ctx.request.body;
+                if (data.s_order === '') {
+                    throw '没有选中计量期';
+                }
+                const newMaterial = await ctx.service.material.addMaterial(ctx.tender.id, data);
+                if (!newMaterial) {
+                    throw '新增材料调差期失败,请重试';
+                }
+                ctx.redirect('/tender/' + ctx.tender.id + '/measure/material/' + newMaterial.order);
+            } catch (err) {
+                this.log(err);
+                ctx.redirect(ctx.request.header.referer);
+            }
+        }
+
+        /**
+         * 删除期(Post)
+         * @param ctx
+         * @returns {Promise<void>}
+         */
+        async delete(ctx) {
+            try {
+                const material_id = ctx.request.body.material_id;
+                const materialInfo = await ctx.service.material.getDataById(material_id);
+                // 获取最新的期数
+                const material_highOrder = await ctx.service.material.count({
+                    tid: ctx.tender.id,
+                });
+                if (materialInfo === undefined || ctx.session.sessionUser.accountId !== materialInfo.user_id || material_highOrder !== materialInfo.order) {
+                    throw '您无权删除材料调差期';
+                }
+                const result = await ctx.service.material.deleteMaterial(material_id);
+                if (!result) {
+                    throw '删除材料调差期失败,请重试';
+                }
+                ctx.redirect('/tender/' + ctx.tender.id + '/measure/material/');
+            } catch (err) {
+                this.log(err);
+                console.log(err);
+                ctx.redirect(ctx.request.header.referer);
+            }
+        }
+
+        /**
+         * 获取通用的renderData(用于layout, Menu, subMenu部分)
+         * @param ctx
+         * @returns {{tender, tenderMenu, auditConst}}
+         * @private
+         */
+        async _getDefaultRenderData(ctx) {
+            const data = {
+                tender: ctx.tender.data,
+                tenderMenu: JSON.parse(JSON.stringify(this.menu.stageMenu)),
+                auditConst,
+                measureType,
+                preUrl: '/tender/' + ctx.tender.id + '/measure/material/' + ctx.params.order,
+                material: ctx.material,
+            };
+            if ((ctx.material.status === auditConst.status.uncheck || ctx.material.status === auditConst.status.checkNo) && ctx.session.sessionUser.accountId === ctx.material.user_id) {
+                data.accountGroup = accountGroup;
+                // 获取所有项目参与者
+                const accountList = await ctx.service.projectAccount.getAllDataByCondition({
+                    where: { project_id: ctx.session.sessionProject.id, enable: 1 },
+                    columns: ['id', 'name', 'company', 'role', 'enable', 'is_admin', 'account_group'],
+                });
+                data.accountList = accountList;
+            }
+            data.tenderMenu.back.children[0].url = '/tender/' + ctx.tender.id + '/measure/material';
+            return data;
+        }
+
+        _materialReadOnly(material) {
+            return material.status === auditConst.status.checked;
+        }
+
+        /**
+         * 获取审批界面所需的 原报、审批人数据等
+         * @param ctx
+         * @returns {Promise<void>}
+         * @private
+         */
+        async _getMaterialAuditViewData(ctx) {
+            const times = ctx.material.status === auditConst.status.checkNo ? ctx.material.times - 1 : ctx.material.times;
+            ctx.material.user = await ctx.service.projectAccount.getAccountInfoById(ctx.material.user_id);
+            ctx.material.auditHistory = [];
+            if (ctx.material.times > 1) {
+                for (let i = 1; i < ctx.material.times; i++) {
+                    ctx.material.auditHistory.push(await ctx.service.materialAudit.getAuditors(ctx.material.id, i));
+                }
+            }
+            // 获取审批流程中左边列表
+            ctx.material.auditors2 = await ctx.service.materialAudit.getAuditGroupByList(ctx.material.id, times);
+            if (ctx.material.status === auditConst.status.uncheck || ctx.material.status === auditConst.status.checkNo) {
+                ctx.material.auditorList = await ctx.service.materialAudit.getAuditors(ctx.material.id, ctx.material.times);
+            }
+        }
+
+        /**
+         * 调差工料页面 (Get)
+         * @param {Object} ctx - egg全局变量
+         * @returns {Promise<void>}
+         */
+        async info(ctx) {
+            try {
+                await this._getMaterialAuditViewData(ctx);
+                const renderData = await this._getDefaultRenderData(ctx);
+                // 取所有工料表
+                renderData.materialBillsData = await ctx.service.materialBills.getAllDataByCondition({ where: { tid: ctx.tender.id } });
+                // 取所有已被调用的工料清单表
+                renderData.materialListData = await ctx.service.materialList.getAllDataByCondition({ where: { tid: ctx.tender.id } });
+                renderData.materialType = JSON.stringify(materialConst);
+                renderData.jsFiles = this.app.jsFiles.common.concat(this.app.jsFiles.material.info);
+                await this.layout('material/info.ejs', renderData, 'material/info_modal.ejs');
+            } catch (err) {
+                this.log(err);
+                ctx.redirect('/tender/' + ctx.tender.id + '/measure/material');
+            }
+        }
+
+        /**
+         * 调差工料 - 编辑工料项 (Ajax)
+         * @param ctx
+         * @returns {Promise<void>}
+         */
+        async saveBillsData(ctx) {
+            try {
+                const data = JSON.parse(ctx.request.body.data);
+                const responseData = {
+                    err: 0,
+                    msg: '',
+                    data: {},
+                };
+                switch (data.type) {
+                    case 'add':
+                        responseData.data = await ctx.service.materialBills.add();
+                        break;
+                    case 'del':
+                        await ctx.service.materialBills.del(data.id);
+                        break;
+                    case 'update':
+                        if (data.updateData.code === '' || data.updateData.code === null) {
+                            throw '请先输入编号';
+                        }
+                        // 判断编号为纯数字时,不能为小数
+                        if (!isNaN(data.updateData.code) && data.updateData.code.indexOf('.') !== -1) {
+                            throw '编号为纯数字时,不能为小数';
+                        }
+                        if (data.updateData.code.length > 15) {
+                            throw '长度不超过15个字符';
+                        }
+                        // 判断编号是否已存在
+                        const billData = await ctx.service.materialBills.getAllDataByCondition({ where: { mid: ctx.material.id, code: data.updateData.code } });
+                        if (billData.length > 1 || (billData.length > 0 && billData[0].id !== data.updateData.id)) {
+                            throw '该编号已存在,请重新输入。';
+                        }
+                        await ctx.service.materialBills.save(data.updateData);
+                        break;
+                    default: throw '参数有误';
+                }
+
+                ctx.body = responseData;
+            } catch (err) {
+                this.log(err);
+                ctx.body = { err: 1, msg: err.toString(), data: null };
+            }
+        }
+
+        // 审批相关
+        /**
+         * 添加审批人
+         * @param ctx
+         * @returns {Promise<void>}
+         */
+        async addAudit(ctx) {
+            try {
+                const data = JSON.parse(ctx.request.body.data);
+                const id = this.app._.toInteger(data.auditorId);
+                if (isNaN(id) || id <= 0) {
+                    throw '参数错误';
+                }
+                // 检查权限等
+                if (ctx.material.user_id !== ctx.session.sessionUser.accountId) {
+                    throw '您无权添加审核人';
+                }
+                if (ctx.material.status === auditConst.status.checking || ctx.material.status === auditConst.status.checked) {
+                    throw '当前不允许添加审核人';
+                }
+
+                ctx.material.auditorList = await ctx.service.materialAudit.getAuditors(ctx.material.id, ctx.material.times);
+                // 检查审核人是否已存在
+                const exist = this.app._.find(ctx.material.auditorList, {aid: id});
+                if (exist) {
+                    throw '该审核人已存在,请勿重复添加';
+                }
+
+                const result = await ctx.service.materialAudit.addAuditor(ctx.material.id, id, ctx.material.times);
+                if (!result) {
+                    throw '添加审核人失败';
+                }
+
+                const audit = await ctx.service.materialAudit.getAuditor(ctx.material.id, id, ctx.material.times);
+                ctx.body = {err: 0, msg: '', data: audit};
+            } catch (err) {
+                this.log(err);
+                ctx.body = {err: 1, msg: err.toString(), data: null};
+            }
+        }
+        /**
+         * 移除审批人
+         * @param ctx
+         * @returns {Promise<void>}
+         */
+        async deleteAudit(ctx) {
+            try {
+                const data = JSON.parse(ctx.request.body.data);
+                const id = data.auditorId instanceof Number ? data.auditorId : this.app._.toNumber(data.auditorId);
+                if (isNaN(id) || id <= 0) {
+                    throw '参数错误';
+                }
+
+                const result = await ctx.service.materialAudit.deleteAuditor(ctx.material.id, id, ctx.material.times);
+                if (!result) {
+                    throw '移除审核人失败';
+                }
+
+                const auditors = await ctx.service.materialAudit.getAuditors(ctx.material.id, ctx.material.times);
+                ctx.body = {err: 0, msg: '', data: auditors};
+            } catch (err) {
+                ctx.body = {err: 1, msg: err.toString(), data: null};
+            }
+        }
+        /**
+         * 上报
+         * @param ctx
+         * @returns {Promise<void>}
+         */
+        async startAudit(ctx) {
+            try {
+                // 检查权限等
+                if (!ctx.material) {
+                    throw '数据错误';
+                }
+                if (ctx.material.user_id !== ctx.session.sessionUser.accountId) {
+                    throw '您无权上报该期数据';
+                }
+                if (ctx.material.status === auditConst.status.checking || ctx.material.status === auditConst.status.checked) {
+                    throw '该材料调差期数据当前无法上报';
+                }
+
+                await ctx.service.materialAudit.start(ctx.material.id, ctx.material.times);
+
+                ctx.redirect(ctx.request.header.referer);
+            } catch (err) {
+                this.log(err);
+                ctx.session.postError = err.toString();
+                ctx.redirect(ctx.request.header.referer);
+            }
+        }
+        /**
+         * 审批
+         * @param ctx
+         * @returns {Promise<void>}
+         */
+        async checkAudit(ctx) {
+            try {
+                if (!this.ctx.material || this.ctx.material.status !== auditConst.status.checking) {
+                    throw '当前材料调差期数据有误';
+                }
+                if (!this.ctx.material.curAuditor || this.ctx.material.curAuditor.aid !== ctx.session.sessionUser.accountId) {
+                    throw '您无权进行该操作';
+                }
+                const data = {
+                    checkType: parseInt(ctx.request.body.checkType),
+                    opinion: ctx.request.body.opinion,
+                };
+                if (!data.checkType || isNaN(data.checkType)) {
+                    throw '提交数据错误';
+                }
+                if (data.checkType === auditConst.status.checkNo) {
+                    if (!data.checkType || isNaN(data.checkType)) {
+                        throw '提交数据错误';
+                    }
+                }
+
+                await ctx.service.materialAudit.check(ctx.material.id, data, ctx.material.times);
+
+                ctx.redirect(ctx.request.header.referer);
+            } catch (err) {
+                console.log(err);
+                this.log(err);
+                ctx.session.postError = err.toString();
+                ctx.redirect(ctx.request.header.referer);
+            }
+        }
+    }
+
+    return MaterialController;
+};

+ 2 - 5
app/controller/revise_controller.js

@@ -22,7 +22,6 @@ const fs = require('fs');
 const LzString = require('lz-string');
 
 module.exports = app => {
-
     class ReviseController extends app.BaseController {
 
         /**
@@ -42,11 +41,9 @@ module.exports = app => {
          * @private
          */
         async _getAddReviseValid(ctx) {
-            const stage  = await ctx.service.stage.getLastestStage(ctx.tender.id, true);
             const revise = await ctx.service.ledgerRevise.getLastestRevise(ctx.tender.id);
             return (ctx.tender.data.user_id === ctx.session.sessionUser.accountId) &&
                 (ctx.tender.data.ledger_status === audit.revise.status.checked) &&
-                (!stage || stage.status === audit.stage.status.checked) &&
                 (!revise || !revise.valid || revise.status === audit.revise.status.checked);
         }
 
@@ -296,9 +293,9 @@ module.exports = app => {
                     b.used = usedPreBills.indexOf(b.id) >= 0 || usedCurBills.indexOf(b.id) >= 0;
                 }
                 const usedPrePos = lastStage.order > 1 ? await ctx.service.stagePosFinal.getUsedPos(ctx.tender.id, lastStage.order - 1) : [];
-                const usedCurPos = await ctx.service.stageBills.getStageUsedPos(ctx.tender.id, lastStage.id);
+                const usedCurPos = await ctx.service.stagePos.getStageUsedPos(ctx.tender.id, lastStage.id);
                 for (const p of renderData.revisePos) {
-                    p.used = usedPrePos.indexOf(b.id) >= 0 || usedCurPos.indexOf(p.id) >= 0;
+                    p.used = usedPrePos.indexOf(p.id) >= 0 || usedCurPos.indexOf(p.id) >= 0;
                 }
             }
             // 修订历史

+ 50 - 44
app/controller/stage_controller.js

@@ -65,10 +65,6 @@ module.exports = app => {
             return data;
         }
 
-        _stageReadOnly(stage) {
-            return stage.status === auditConst.status.checked;
-        }
-
         /**
          * 获取SpreadSetting
          * @private
@@ -101,7 +97,7 @@ module.exports = app => {
                 removeFieldCols(ledger, spreadConst.filterCols.stageDgnCols);
             }
             const pos = JSON.parse(JSON.stringify(stageSetting.pos));
-            if (this.ctx.stage.readOnly) {
+            if (this.ctx.stage.readOnly || this.ctx.stage.revising) {
                 ledger.readOnly = true;
                 pos.readOnly = true;
             }
@@ -130,6 +126,16 @@ module.exports = app => {
             }
         }
 
+        _checkStageCanModify(ctx) {
+            // 检查登录用户,是否可操作
+            if (ctx.stage.readOnly) {
+                throw '该计量期当前您无权操作';
+            }
+            if (ctx.stage.revising) {
+                throw '台账修订中,请勿修改提交期数据';
+            }
+        }
+
         /**
          * 期计量页面 (Get)
          * @param {Object} ctx - egg全局变量
@@ -235,10 +241,7 @@ module.exports = app => {
          */
         async updateStageData(ctx) {
             try {
-                // 检查登录用户,是否可操作
-                if (ctx.stage.readOnly) {
-                    throw '该计量期当前您无权操作';
-                }
+                this._checkStageCanModify(ctx);
 
                 const data = JSON.parse(ctx.request.body.data);
                 const responseData = { err: 0, msg: '', data: {}, };
@@ -266,10 +269,6 @@ module.exports = app => {
          */
         async searchValidChange(ctx) {
             try {
-                // if (ctx.stage.readOnly) {
-                //     throw '您无权调用变更令';
-                // }
-
                 const data = JSON.parse(ctx.request.body.data);
                 if (!data.bills && !data.pos) {
                     throw '数据错误';
@@ -291,9 +290,7 @@ module.exports = app => {
          */
         async useChange(ctx) {
             try {
-                if (ctx.stage.readOnly) {
-                    throw '您无权使用变更令';
-                }
+                this._checkStageCanModify(ctx);
 
                 const data = JSON.parse(ctx.request.body.data);
                 if (!data.target || (!data.target.bills && !data.target.pos) || !data.change) {
@@ -357,10 +354,7 @@ module.exports = app => {
          */
         async buildDetailData (ctx) {
             try {
-                // 检查登录用户,是否可操作
-                if (ctx.stage.readOnly) {
-                    throw '该计量期当前您无权操作';
-                }
+                this._checkStageCanModify(ctx);
 
                 const data = JSON.parse(ctx.request.body.data);
                 await ctx.service.stage.buildDetailData(ctx.tender.id, ctx.stage.order, data);
@@ -425,10 +419,7 @@ module.exports = app => {
          */
         async setAdvancedConfig(ctx) {
             try {
-                // 检查登录用户,是否可操作
-                if (ctx.stage.readOnly) {
-                    throw '该计量期当前您无权操作';
-                }
+                this._checkStageCanModify(ctx);
 
                 const data = JSON.parse(ctx.request.body.data);
                 await ctx.service.stage.update(data, { id: ctx.stage.id });
@@ -448,10 +439,7 @@ module.exports = app => {
          */
         async saveDetailData(ctx) {
             try {
-                // 检查登录用户,是否可操作
-                if (ctx.stage.readOnly) {
-                    throw '该计量期当前您无权操作';
-                }
+                this._checkStageCanModify(ctx);
 
                 const data = JSON.parse(ctx.request.body.data);
                 const responseData = { err: 0, msg: '', data: {}, };
@@ -475,9 +463,8 @@ module.exports = app => {
          */
         async addCalcImage(ctx) {
             try {
-                if (ctx.stage.readOnly) {
-                    throw '该计量期当前您无权操作';
-                }
+                this._checkStageCanModify(ctx);
+
                 const stream = await ctx.getFileStream();
                 const create_time = Date.parse(new Date()) / 1000;
                 const fileInfo = path.parse(stream.filename);
@@ -498,10 +485,8 @@ module.exports = app => {
          */
         async mergeCalcImage(ctx) {
             try {
-                // 检查登录用户,是否可操作
-                if (ctx.stage.readOnly) {
-                    throw '该计量期当前您无权操作';
-                }
+                this._checkStageCanModify(ctx);
+
                 const data = JSON.parse(ctx.request.body.data);
 
                 if (data.updateType === 'update') {
@@ -538,6 +523,8 @@ module.exports = app => {
          */
         async doneDetail(ctx) {
             try {
+                this._checkStageCanModify(ctx);
+
                 await ctx.service.stage.updateCheckDetailFlag(ctx.stage.id, false);
                 ctx.stage.check_detail = false;
                 ctx.body = {err: 0, msg: '', data: false}
@@ -572,6 +559,12 @@ module.exports = app => {
                 renderData.calcBase = await ctx.service.stage.getStagePayCalcBase(ctx.stage, ctx.tender.info);
                 renderData.jsFiles = this.app.jsFiles.common.concat(this.app.jsFiles.stage.pay);
                 renderData.whiteList = this.ctx.app.config.multipart.whitelist;
+                if (ctx.stage.order > 1) {
+                    renderData.pre = await ctx.service.stageBillsFinal.getSumTotalPrice(ctx.stage.tid, ctx.stage.order - 1);
+                    renderData.pre.gather_tp = ctx.helper.add(renderData.pre.contract_tp, renderData.pre.qc_tp);
+                } else {
+                    renderData.pre = {contract_tp: null, qc_tp: null, gather_tp: null};
+                }
 
                 // 用户有无权限上传和删除附件
                 renderData.uploadPermission = ((ctx.stage.status === auditConst.status.uncheck || ctx.stage.status === auditConst.status.checkNo) && ctx.session.sessionUser.accountId === ctx.stage.user_id) ||
@@ -603,10 +596,7 @@ module.exports = app => {
                 || data.minus !== undefined || data.is_yf !== undefined || data.dl_type !== undefined
             }
             try {
-                // 检查登录用户,是否可操作
-                if (ctx.stage.readOnly) {
-                    throw '该计量期当前您无权操作';
-                }
+                this._checkStageCanModify(ctx);
 
                 const data = JSON.parse(ctx.request.body.data);
                 const responseData = { err: 0, msg: '', data: {}, };
@@ -781,11 +771,13 @@ module.exports = app => {
          */
         async addAudit(ctx) {
             try {
+                this._checkStageCanModify(ctx);
                 const data = JSON.parse(ctx.request.body.data);
                 const id = this.app._.toInteger(data.auditorId);
                 if (isNaN(id) || id <= 0) {
                     throw '参数错误';
                 }
+
                 // 检查权限等
                 if (ctx.stage.user_id !== ctx.session.sessionUser.accountId) {
                     throw '您无权添加审核人';
@@ -820,6 +812,7 @@ module.exports = app => {
          */
         async deleteAudit(ctx) {
             try {
+                this._checkStageCanModify(ctx);
                 const data = JSON.parse(ctx.request.body.data);
                 const id = data.auditorId instanceof Number ? data.auditorId : this.app._.toNumber(data.auditorId);
                 if (isNaN(id) || id <= 0) {
@@ -844,10 +837,8 @@ module.exports = app => {
          */
         async startAudit(ctx) {
             try {
-                // 检查权限等
-                if (!ctx.stage) {
-                    throw '数据错误';
-                }
+                this._checkStageCanModify(ctx);
+
                 if (ctx.stage.user_id !== ctx.session.sessionUser.accountId) {
                     throw '您无权上报该期数据';
                 }
@@ -874,6 +865,8 @@ module.exports = app => {
          */
         async checkAudit(ctx) {
             try {
+                this._checkStageCanModify(ctx);
+
                 if (!this.ctx.stage || (this.ctx.stage.status !== auditConst.status.checking && this.ctx.stage.status !== auditConst.status.checkNoPre)) {
                     throw '当前期数据有误';
                 }
@@ -906,7 +899,6 @@ module.exports = app => {
                 ctx.redirect(ctx.request.header.referer);
             }
         }
-
         /**
          * 重新审批
          * @param ctx
@@ -914,6 +906,10 @@ module.exports = app => {
          */
         async checkAuditAgain(ctx) {
             try {
+                if (ctx.stage.revising) {
+                    throw '台账修订中,请勿修改提交期数据';
+                }
+
                 if (ctx.stage.auditors[ctx.stage.auditors.length - 1].aid === ctx.session.sessionUser.accountId && ctx.stage.status === auditConst.status.checked && ctx.stage.order === ctx.stage.highOrder) {
                     await ctx.service.stageAudit.checkAgain(ctx.stage.id, ctx.stage.times);
                     console.log('success');
@@ -1086,6 +1082,8 @@ module.exports = app => {
             };
             let stream;
             try {
+                this._checkStageCanModify(ctx);
+
                 const parts = ctx.multipart({ autoFields: true });
                 const files = [];
                 let index = 0;
@@ -1194,6 +1192,8 @@ module.exports = app => {
                 data: '',
             };
             try {
+                this._checkStageCanModify(ctx);
+
                 const data = JSON.parse(ctx.request.body.data);
                 const fileInfo = await ctx.service.stageAtt.getDataById(data.id);
                 if (fileInfo !== undefined && fileInfo !== '') {
@@ -1232,6 +1232,8 @@ module.exports = app => {
             };
             let stream;
             try {
+                this._checkStageCanModify(ctx);
+
                 stream = await ctx.getFileStream({ requireFile: false });
                 let fileData = {};
                 if (stream.filename !== undefined) {
@@ -1285,6 +1287,8 @@ module.exports = app => {
             };
             let stream;
             try {
+                this._checkStageCanModify(ctx);
+
                 const parts = ctx.multipart({ autoFields: true });
                 const files = [];
                 let index = 0;
@@ -1390,6 +1394,8 @@ module.exports = app => {
                 data: '',
             };
             try {
+                this._checkStageCanModify(ctx);
+
                 const data = JSON.parse(ctx.request.body.data);
                 const payInfo = await ctx.service.stagePay.getDataById(data.id);
                 if (payInfo !== undefined) {

+ 21 - 1
app/controller/tender_controller.js

@@ -303,7 +303,27 @@ module.exports = app => {
                 if (!data) {
                     throw '提交数据错误';
                 }
-                await ctx.service.tenderInfo.saveTenderInfo(ctx.tender.id, data);
+                if (ctx.tender.data.ledger_status === auditConst.ledger.status.checked) {
+                    if (data.deal_param) {
+                        const lastStage = await this.ctx.service.stage.getLastestStage(ctx.tender.id, true);
+                        if (lastStage) {
+                            if (lastStage.order > 1 || (lastStage.status === auditConst.stage.status.checked || lastStage.status === auditConst.stage.status.checking))
+                                throw '第一期上报后不可修改合同参数';
+                            if (lastStage.user_id !== ctx.session.sessionUser.accountId) throw '仅原报可修改合同参数';
+                        }
+                    }
+                }
+
+                if (data.decimal) {
+                    if (ctx.tender.data.user_id !== ctx.session.sessionUser.accountId) throw '仅原报可修改小数位数';
+                    await ctx.service.tenderInfo.saveDecimal(ctx.tender.id, data.decimal, ctx.tender.info.decimal);
+                } else if (data.precision) {
+                    if (ctx.tender.data.user_id !== ctx.session.sessionUser.accountId) throw '仅原报可修改清单精度';
+                    await ctx.service.tenderInfo.savePrecision(ctx.tender.id,
+                        data.precision, ctx.tender.info.precision, ctx.tender.info.decimal);
+                } else {
+                    await ctx.service.tenderInfo.saveTenderInfo(ctx.tender.id, data);
+                }
                 ctx.body = { err: 0, msg: '', data: JSON.parse(ctx.request.body.data) };
             } catch (err) {
                 this.log(err);

+ 23 - 1
app/extend/helper.js

@@ -15,6 +15,7 @@ const _ = require('lodash');
 const bc = require('../lib/base_calc.js');
 const Decimal = require('decimal.js');
 Decimal.set({precision: 50, defaults: true});
+const SMS = require('../lib/sms');
 
 module.exports = {
     _: _,
@@ -589,7 +590,6 @@ module.exports = {
      * @param fields - 有效数据的数组
      */
     filterValidFields(data, fields) {
-
         if (data) {
             const result = {};
             for (const prop in data) {
@@ -763,5 +763,27 @@ module.exports = {
             t += l[i] + ((i + 1) % 3 == 0 && (i + 1) != l.length ? dot : "");
         }
         return t.split("").reverse().join("") + "." + r;
+    },
+
+    async sendUserSms(userId, type, judge, msg) {
+        const mobiles = [];
+        if (!userId || (userId instanceof Array && userId.length === 0)) return;
+        const smsUser = await this.ctx.service.projectAccount.getAllDataByCondition({where: {id: userId}});
+        for (const su of smsUser) {
+            if (!su.auth_mobile || su.auth_mobile === '') continue;
+            if (!su.sms_type || su.sms_type === '') continue;
+
+            const smsType = JSON.parse(su.sms_type);
+            if (smsType[type] && smsType.indexOf(judge) !== -1) {
+                mobiles.push(su.auth_mobile);
+            }
+        }
+
+        if (mobiles.length > 0) {
+            const sms = new SMS(this.ctx);
+            const tenderName = await sms.contentChange(this.ctx.tender.data.name);
+            const content = '【纵横计量支付】' + tenderName + msg;
+            sms.send(mobiles, content);
+        }
     }
 };

+ 6 - 7
app/lib/pay_calc.js

@@ -42,10 +42,10 @@ class PayCalculate {
         }
     }
 
-    calculateTpExpr(pay, first, addRela) {
+    calculateTpExpr(pay, addRela) {
         let formula = pay.expr;
         for (const b of this.bases) {
-            if ((b.code === 'bqwc') && (first && pay.sprice)) {
+            if ((b.code === 'bqwc') && (!pay.pre_used && pay.sprice)) {
                 formula = formula.replace(b.reg, this.ctx.helper.sub(addRela.gather_tp, pay.sprice));
             } else {
                 formula = formula.replace(b.reg, b.value);
@@ -114,7 +114,7 @@ class PayCalculate {
      */
     async getAddCalcRela() {
         // todo 获取截止上期数据
-        const pre = null;
+        const pre = this.stage.order > 1 ? await this.ctx.service.stageBillsFinal.getSumTotalPrice(this.stage.tid, this.stage.order - 1) : null;
         const cur = await this.ctx.service.stageBills.getSumTotalPrice(this.stage);
         const add = {};
         if (pre) {
@@ -151,18 +151,17 @@ class PayCalculate {
      */
     async calculate(pays) {
         await this.getCalcBase();
-        const addRela = await this.getAddCalcRela();
         const yfPay = pays.find(function (p) {
             return p.ptype === payType.yf;
         });
+        const addRela = await this.getAddCalcRela();
         if (!yfPay) return false;
         yfPay.tp = 0;
         for (const p of pays) {
             if (p.ptype === payType.normal || p.ptype === payType.wc) {
-                if (!p.pause && (!p.sprice || addRela.gather_tp > p.sprice)) {
+                if (!p.pause && (!p.sprice || addRela.gather_tp >= p.sprice)) {
                     if (p.expr && p.expr !== '') {
-                        const first = !(p.pre_tp && !this.ctx.helper.checkZero(p.pre_tp));
-                        const value = this.ctx.helper.round(this.calculateTpExpr(p, first, addRela), this.decimal);
+                        const value = this.ctx.helper.round(this.calculateTpExpr(p, addRela), this.decimal);
                         if (p.rprice) {
                             if (this.checkDeadline(p, addRela)) {
                                 p.tp = this.ctx.helper.sub(p.rprice, p.pre_tp);

+ 120 - 0
app/middleware/material_check.js

@@ -0,0 +1,120 @@
+'use strict';
+
+/**
+ *
+ * @author EllisRan
+ * @date
+ * @version
+ */
+
+const status = require('../const/audit').material.status;
+const _ = require('lodash');
+
+module.exports = options => {
+    /**
+     * 材料调差校验 中间件
+     * 1. 读取材料调差数据
+     * 2. 检验用户是否参与材料调差(不校验具体权限)
+     *
+     * 写入ctx.material数据
+     * 其中:
+     * material.auditors: 审批人列表(退回原报时,加载上一流程)
+     * material.curAuditor: 当前审批人(未上报为空,审批通过 or 退回原报时,为空)
+     * material.readonly: 登录人,是否可操作
+     * material.curTimes: 当前登录人,操作、查阅数据times
+     * material.curOrder: 当前登录人,操作、查阅数据order
+     *
+     * 该方法为通用方法,如需material其他数据,请在controller中查询
+     *
+     * @param {function} next - 中间件继续执行的方法
+     * @return {void}
+     */
+    return function* materialCheck(next) {
+        try {
+            // 读取标段数据
+            const materialOrder = parseInt(this.params.order);
+            if (materialOrder <= 0) {
+                throw '您访问的期不存在';
+            }
+            const material = yield this.service.material.getDataByCondition({
+                tid: this.tender.id,
+                order: materialOrder,
+            });
+            if (!material) {
+                throw '材料调差期数据错误';
+            }
+
+            // 读取原报、审核人数据
+            material.auditors = yield this.service.materialAudit.getAuditors(material.id, material.times);
+            material.curAuditor = yield this.service.materialAudit.getCurAuditor(material.id, material.times);
+
+            // 权限相关
+            // todo 校验权限 (标段参与人、分享)
+            const accountId = this.session.sessionUser.accountId, auditorIds = _.map(material.auditors, 'aid'), shareIds = [];
+            if (accountId === material.user_id) { // 原报
+                // if (material.curAuditor) {
+                //     material.readOnly = material.status === status.checking && material.curAuditor.user_id === accountId;
+                // } else {
+                //     material.readOnly = material.status !== status.uncheck && material.status !== status.checkNo;
+                // }
+                material.curTimes = material.times;
+                if (material.status === status.uncheck || material.status === status.checkNo) {
+                    material.curOrder = 0;
+                } else if (material.status === status.checked) {
+                    material.curOrder = _.max(_.map(material.auditors, 'order'));
+                } else {
+                    material.curOrder = material.curAuditor.aid === accountId ? material.curAuditor.order : material.curAuditor.order - 1;
+                }
+            } else if (auditorIds.indexOf(accountId) !== -1) { // 审批人
+                if (material.status === status.uncheck) {
+                    throw '您无权查看该数据';
+                }
+                // material.readOnly = material.status !== status.checking || accountId !== material.curAuditor.aid;
+                material.curTimes = material.status === status.checkNo ? material.times - 1 : material.times;
+                if (material.status === status.checked) {
+                    material.curOrder = _.max(_.map(material.auditors, 'order'));
+                } else if (material.status === status.checkNo) {
+                    const audit = this.service.materialAudit.getDataByCondition({
+                        mid: material.id, times: material.times, status: status.checkNo
+                    });
+                    material.curOrder = audit.order;
+                } else {
+                    material.curOrder = accountId === material.curAuditor.aid ? material.curAuditor.order : material.curAuditor.order - 1;
+                }
+            } else if (shareIds.indexOf(accountId) !== -1) { // 分享人
+                if (material.status === status.uncheck) {
+                    throw '您无权查看该数据';
+                }
+                // material.readOnly = true;
+                material.curTimes = material.status === status.checkNo ? material.times - 1 : material.times;
+                material.curOrder = material.status === status.checked ? _.max(_.map(material.auditors, 'order')) : material.curAuditor.order - 1;
+            } else { // 其他不可见
+                throw '您无权查看该数据';
+            }
+
+            // 获取最新的期
+            material.highOrder = yield this.service.material.count({
+                tid: this.tender.id,
+            });
+            // 调差的readOnly 指表格和页面只能看不能改,和审批无关
+            material.readOnly = !((material.status === status.uncheck || material.status === status.checkNo) && accountId === material.user_id);
+            this.material = material;
+            yield next;
+        } catch (err) {
+            console.log(err);
+            // 输出错误到日志
+            if (err.stack) {
+                this.logger.error(err);
+            } else {
+                this.getLogger('fail').info(JSON.stringify({
+                    error: err,
+                    project: this.session.sessionProject,
+                    user: this.session.sessionUser,
+                    body: this.session.body,
+                }));
+            }
+            // 重定向值标段管理
+            this.redirect(this.request.headers.referer);
+        }
+    };
+};

+ 10 - 7
app/middleware/stage_check.js

@@ -8,6 +8,7 @@
  */
 
 const status = require('../const/audit').stage.status;
+const reviseStatus = require('../const/audit').revise.status;
 const _ = require('lodash');
 
 module.exports = options => {
@@ -48,6 +49,10 @@ module.exports = options => {
             stage.auditors = yield this.service.stageAudit.getAuditors(stage.id, stage.times);
             stage.curAuditor = yield this.service.stageAudit.getCurAuditor(stage.id, stage.times);
 
+            // 获取最新的期
+            stage.highOrder = yield this.service.stage.count({
+                tid: this.tender.id,
+            });
             // 权限相关
             // todo 校验权限 (标段参与人、分享)
             const accountId = this.session.sessionUser.accountId, auditorIds = _.map(stage.auditors, 'aid'), shareIds = [];
@@ -69,18 +74,18 @@ module.exports = options => {
                 if (stage.status === status.uncheck) {
                     throw '您无权查看该数据';
                 }
-                stage.readOnly = (stage.status !== status.checking && stage.status !== status.checkNoPre) || accountId !== stage.curAuditor.aid;
                 stage.curTimes = stage.status === status.checkNo ? stage.times - 1 : stage.times;
                 if (stage.status === status.checked) {
                     stage.curOrder = _.max(_.map(stage.auditors, 'order'));
                 } else if (stage.status === status.checkNo) {
-                    const audit = this.service.stageAudit.getDataByCondition({
-                        sid: stage.id, times: stage.times, status: status.checkNo
+                    const audit = yield this.service.stageAudit.getDataByCondition({
+                        sid: stage.id, times: stage.times - 1, status: status.checkNo
                     });
                     stage.curOrder = audit.order;
                 } else {
                     stage.curOrder = accountId === stage.curAuditor.aid ? stage.curAuditor.order : stage.curAuditor.order - 1;
                 }
+                stage.readOnly = (stage.status !== status.checking && stage.status !== status.checkNoPre) || accountId !== stage.curAuditor.aid;
             } else if (shareIds.indexOf(accountId) !== -1) { // 分享人
                 if (stage.status === status.uncheck) {
                     throw '您无权查看该数据';
@@ -92,10 +97,8 @@ module.exports = options => {
                 throw '您无权查看该数据';
             }
 
-            // 获取最新的期
-            stage.highOrder = yield this.service.stage.count({
-                tid: this.tender.id,
-            });
+            const lastRevise = yield this.service.ledgerRevise.getLastestRevise(this.tender.id);
+            stage.revising = (lastRevise && lastRevise.status !== reviseStatus.checked) || false;
             this.stage = stage;
             yield next;
         } catch (err) {

+ 3 - 3
app/public/js/change.js

@@ -73,7 +73,7 @@ class codeRuleSet {
                 case ruleConst.ruleType.text: {
                     rule.text = $('#text>input', obj).val();
                     if (rule.text === '') {
-                        toast('文本内容不允许为空。', 'error');
+                        toastr.error('文本内容不允许为空。');
                         return false;
                     }
                     preview = rule.text;
@@ -87,7 +87,7 @@ class codeRuleSet {
                     rule.format = parseInt($('#format>input', obj).val());
                     rule.start = parseInt($('#text>input', obj).val());
                     if ($('#text>input', obj).val().length !== rule.format) {
-                        toast('起始编号位数和自动编号位数不一致。', 'error');
+                        toastr.error('起始编号位数和自动编号位数不一致。');
                         return false;
                     }
                     const s = '0000000000';
@@ -95,7 +95,7 @@ class codeRuleSet {
                     break;
                 }
                 default: {
-                    toast('请选择组件再添加', 'error');
+                    toastr.error('请选择组件再添加');
                     return false;
                 }
             }

+ 38 - 6
app/public/js/global.js

@@ -99,6 +99,7 @@ $(function(){
     });
 });
 
+
 /**
  * 提示框
  *
@@ -148,7 +149,7 @@ const postData = function (url, data, successCallback, errorCallBack, showWaitin
                     successCallback(result.data);
                 }
             } else {
-                toast('error: ' + result.msg, 'error', 'exclamation-circle');
+                toastr.error('error: ' + result.msg);
                 if (errorCallBack) {
                     errorCallBack(result.msg);
                 }
@@ -156,7 +157,7 @@ const postData = function (url, data, successCallback, errorCallBack, showWaitin
             if (showWaiting) closeWaitingView();
         },
         error: function(jqXHR, textStatus, errorThrown){
-            toast('error ' + textStatus + " " + errorThrown, 'error', 'exclamation-circle');
+            toastr.error('error: ' + textStatus + " " + errorThrown);
             if (errorCallBack) {
                 errorCallBack();
             }
@@ -191,7 +192,7 @@ const postDataCompress = function (url, data, successCallback, errorCallBack, sh
                     successCallback(result.data);
                 }
             } else {
-                toast('error: ' + result.msg, 'error', 'exclamation-circle');
+                toastr.error('error: ' + result.msg);
                 if (errorCallBack) {
                     errorCallBack(result.msg);
                 }
@@ -199,7 +200,7 @@ const postDataCompress = function (url, data, successCallback, errorCallBack, sh
             if (showWaiting) closeWaitingView();
         },
         error: function(jqXHR, textStatus, errorThrown){
-            toast('error ' + textStatus + " " + errorThrown, 'error', 'exclamation-circle');
+            toastr.error('error: ' + textStatus + " " + errorThrown);
             if (errorCallBack) {
                 errorCallBack();
             }
@@ -238,7 +239,7 @@ const postDataWithFile = function (url, formData, successCallback, errorCallBack
                     successCallback(result.data);
                 }
             } else {
-                toast('error: ' + result.msg, 'error', 'exclamation-circle');
+                toastr.error('error: ' + result.msg);
                 if (errorCallBack) {
                     errorCallBack();
                 }
@@ -246,7 +247,7 @@ const postDataWithFile = function (url, formData, successCallback, errorCallBack
             if (showWaiting) closeWaitingView();
         },
         error: function(jqXHR, textStatus, errorThrown){
-            toast('error ' + textStatus + " " + errorThrown, 'error', 'exclamation-circle');
+            toastr.error('error: ' + textStatus + " " + errorThrown);
             if (errorCallBack) {
                 errorCallBack();
             }
@@ -373,3 +374,34 @@ function removeLocalCache(key) {
     }
     return storage.removeItem(key);
 }
+
+//params: 需要复制的对象(元素)
+function copyToClipboard( text ) {
+    var isRTL = document.documentElement.getAttribute('dir') == 'rtl';
+    var aux =  document.createElement('textarea');// 创建元素用于复制
+    // Prevent zooming on iOS
+    aux.style.fontSize = '12pt';
+    // Reset box model
+    aux.style.border = '0';
+    aux.style.padding = '0';
+    aux.style.margin = '0';
+    // Move element out of screen horizontally
+    aux.style.position = 'absolute';
+    aux.style[isRTL ? 'right' : 'left'] = '-9999px';
+    // Move element to the same position vertically
+    var yPosition = window.pageYOffset || document.documentElement.scrollTop;
+    aux.style.top = yPosition + 'px';
+
+    aux.setAttribute('readonly', '');
+    // 设置元素内容
+    aux.value = text;
+    // 将元素插入页面进行调用
+    document.body.appendChild(aux);
+    // 复制内容
+    aux.select();
+    aux.setSelectionRange(0, text.length);
+    // 将内容复制到剪贴板
+    document.execCommand("copy", true);
+    // 删除创建元素
+    document.body.removeChild(aux);
+}

+ 188 - 52
app/public/js/ledger.js

@@ -16,6 +16,12 @@ function getTenderId() {
 }
 
 const copyBlockTag = 'zh.calc.copyBlock';
+const invalidFields = {
+    parent: ['sgfh_qty', 'sgfh_tp', 'sjcl_qty', 'sjcl_tp', 'qtcl_qty', 'qtcl_tp', 'deal_qty', 'deal_tp', 'unit_price'],
+    gcl: ['dgn_qty1', 'dgn_qty2'],
+    posCode: ['b_code'],
+    posCalc: ['sgfh_qty', 'sgfh_tp', 'sjcl_qty', 'sjcl_tp', 'qtcl_qty', 'qtcl_tp'],
+};
 
 $(document).ready(function() {
     autoFlashHeight();
@@ -40,13 +46,6 @@ $(document).ready(function() {
         node.dgn_price = ZhCalc.round(ZhCalc.div(node.total_price, node.dgn_qty1), 2);
     };
     const ledgerTree = createNewPathTree('ledger', treeSetting);
-    ledgerTree.loadDatas(ledger);
-    treeCalc.calculateAll(ledgerTree);
-    ledgerTreeCol.initSpreadSetting(ledgerSpreadSetting);
-    SpreadJsObj.initSheet(ledgerSpread.getActiveSheet(), ledgerSpreadSetting);
-    // 加载台账数据到界面
-    SpreadJsObj.loadSheetData(ledgerSpread.getActiveSheet(), 'tree', ledgerTree);
-    SpreadJsObj.loadTopAndSelect(ledgerSpread.getActiveSheet(), ckBillsSpread);
     // 初始化 部位明细
     const pos = new PosData({
         id: 'id', ledgerId: 'lid',
@@ -93,7 +92,7 @@ $(document).ready(function() {
                     const rNode = sheet.zh_tree.nodes[sel.row + r];
                     if (rNode.level > node.level) continue;
                     if ((rNode.level < node.level) || (rNode.level === node.level && rNode.pid !== node.pid)) {
-                        toast('请选择同一清单下的节点,进行该操作');
+                        toastr.error('请选择同一清单下的节点,进行该操作');
                         return;
                     }
                     count += 1;
@@ -136,12 +135,12 @@ $(document).ready(function() {
             const valid = !sheet.zh_setting.readOnly;
 
             setObjEnable($('#insert'), valid && first && first.level > 1);
-            setObjEnable($('#delete'), valid && first && sameParent && first.level > 1 && !first.node_type);
+            setObjEnable($('#delete'), valid && first && sameParent && !(first.level === 1 && first.node_type));
             setObjEnable($('#up-move'), valid && first && sameParent && first.level > 1 && preNode);
             setObjEnable($('#down-move'), valid && first && sameParent && first.level > 1 && !tree.isLastSibling(last));
             if (checkTzMeasureType()) {
                 const posRange = last ? pos.getLedgerPos(last.id) : [];
-                setObjEnable($('#up-level'), valid && first && sameParent && tree.getParent(first) && first.level > 2 && (!posRange || posRange.length === 0));
+                setObjEnable($('#up-level'), valid && first && sameParent && tree.getParent(first) && first.level > 2 && ((!posRange || posRange.length === 0) || tree.isLastSibling(last)));
                 const preNodePosRange = preNode ? pos.getLedgerPos(preNode.id) : [];
                 setObjEnable($('#down-level'), valid && first && sameParent && first.level > 1 && preNode && (!preNodePosRange || preNodePosRange.length === 0));
             } else {
@@ -369,7 +368,7 @@ $(document).ready(function() {
                         if (!node.children || node.children.length ===0) {
                             const lPos = pos.getLedgerPos(node.id);
                             if (lPos && lPos.length > 0) {
-                                toast('清单含有部位明细,请在部位明细输入数量', 'error');
+                                toastr.error('清单含有部位明细,请在部位明细输入数量');
                                 SpreadJsObj.reLoadRowData(info.sheet, info.row);
                                 return;
                             }
@@ -378,7 +377,7 @@ $(document).ready(function() {
                     if (col.field === 'b_code' && (info.editingText === '' || !info.editingText)) {
                         const lPos = pos.getLedgerPos(node.id);
                         if (lPos && lPos.length > 0) {
-                            toast('清单含有部位明细,请先删除部位明细,再删除清单编号', 'error');
+                            toastr.error('清单含有部位明细,请先删除部位明细,再删除清单编号');
                             SpreadJsObj.reLoadRowData(info.sheet, info.row);
                             return;
                         }
@@ -425,7 +424,7 @@ $(document).ready(function() {
                             if (lPos && lPos.length > 0) {
                                 if (value === '' && colSetting.field === 'b_code') {
                                     if (!bHint) {
-                                        toast('清单含有部位明细,请先删除部位明细,再删除清单编号', 'warning');
+                                        toastr.warning('清单含有部位明细,请先删除部位明细,再删除清单编号');
                                         bHint = true;
                                     }
                                     continue;
@@ -434,7 +433,7 @@ $(document).ready(function() {
                                     colSetting.field === 'sjcl_qty' || colSetting.field === 'sjcl_tp' ||
                                     colSetting.field === 'qtcl_qty' || colSetting.field === 'qtcl_tp') {
                                     if (!bHint) {
-                                        toast('清单含有部位明细,数量金额根据部位明细汇总计算所得,不可编辑', 'warning');
+                                        toastr.warning('清单含有部位明细,数量金额根据部位明细汇总计算所得,不可编辑');
                                         bHint = true;
                                     }
                                     continue;
@@ -459,7 +458,7 @@ $(document).ready(function() {
                     }
                 }
                 if (datas.length > 0) {
-                    postData(window.location.pathname + '/update', {postType: 'update', postData: data}, function (result) {
+                    postData(window.location.pathname + '/update', {postType: 'update', postData: datas}, function (result) {
                         const refreshNode = tree.loadPostData(result);
                         if (refreshNode.update) {
                             refreshNode.update = refreshNode.update.concat(filterNodes);
@@ -473,6 +472,90 @@ $(document).ready(function() {
                 }
             }
         },
+        clipboardPasting: function (e, info) {
+            const sheet = info.sheet, tree = info.sheet.zh_tree, setting = info.sheet.zh_setting;
+            console.log(info);
+            info.cancel = true;
+            if (!setting || !tree) return;
+            const pasteData = info.pasteData.html
+                ? SpreadJsObj.analysisPasteHtml(info.pasteData.html)
+                : (info.pasteData.text === ''
+                    ? SpreadJsObj.Clipboard.getAnalysisPasteText()
+                    : SpreadJsObj.analysisPasteText(info.pasteData.text));
+            let bParentHint = false, bGclHint = false, bCodeHint = false, bQtyHint = false, bNumHint = false;
+            const datas = [], filterNodes = [];
+            for (let iRow = 0; iRow < info.cellRange.rowCount; iRow++) {
+                const curRow = info.cellRange.row + iRow;
+                const node = tree.nodes[curRow];
+                if (!node) continue;
+
+                let bPaste = false;
+                const data = info.sheet.zh_tree.getNodeKeyData(node);
+                for (let iCol = 0; iCol < info.cellRange.colCount; iCol++) {
+                    const curCol = info.cellRange.col + iCol;
+                    const colSetting = info.sheet.zh_setting.cols[curCol];
+                    const value = pasteData[iRow][iCol];
+                    const lPos = pos.getLedgerPos(node.id);
+                    if (node.children && node.children.length > 0 && invalidFields.parent.indexOf(colSetting.field) >= 0) {
+                        if (!bParentHint) bParentHint = true;
+                        continue;
+                    }
+                    if (!_.isEmpty(node.b_code) && invalidFields.gcl.indexOf(colSetting.field) >= 0) {
+                        if (!bGclHint) bGclHint = true;
+                        continue;
+                    }
+                    if (lPos && lPos.length > 0) {
+                        if (value === '' && invalidFields.posCode.indexOf(colSetting.field) >= 0) {
+                            if (!bCodeHint) bCodeHint = true;
+                            continue;
+                        }
+                        if (invalidFields.posCalc.indexOf(colSetting.field) >= 0) {
+                            if (!bQtyHint) bQtyHint = true;
+                            continue;
+                        }
+                    }
+                    if (colSetting.type === 'Number') {
+                        const num = _.toNumber(value);
+                        if (num) {
+                            data[colSetting.field] = num;
+                            bPaste = true;
+                        } else {
+                            bNumHint = true;
+                        }
+                    } else {
+                        data[colSetting.field] = value;
+                        bPaste = true;
+                    }
+                }
+                if (bPaste) {
+                    datas.push(data);
+                } else {
+                    filterNodes.push(node);
+                }
+            }
+            const hint = [];
+            if (bParentHint) hint.push('含有子项的清单,不可粘贴数量、单价、金额');
+            if (bGclHint) hint.push('工程量清单,不可粘贴设计数量');
+            if (bQtyHint) hint.push('含有部位明细的清单,不可粘贴数量');
+            if (bCodeHint) hint.push('含有部位明细的清单,清单编号不可粘贴空值');
+            if (bNumHint) hint.push('单价、数量等应输入数字');
+            // if (hint.length > 0) hint.unshift('复制粘贴的数据中,存在非法数据,已过滤:');
+            // if (hint.length > 0) toastr.warning(hint.join('</br>'));
+            if (hint.length > 0) toastr.warning('复制粘贴的数据中,存在非法数据,已过滤');
+            if (datas.length > 0) {
+                postData(window.location.pathname + '/update', {postType: 'update', postData: datas}, function (result) {
+                    const refreshNode = tree.loadPostData(result);
+                    if (refreshNode.update) {
+                        refreshNode.update = refreshNode.update.concat(filterNodes);
+                    }
+                    treeOperationObj.refreshTree(info.sheet, refreshNode);
+                }, function () {
+                    SpreadJsObj.reLoadRowData(info.sheet, info.cellRange.row, info.cellRange.rowCount);
+                });
+            } else {
+                SpreadJsObj.reLoadRowData(info.sheet, info.cellRange.row, info.cellRange.rowCount);
+            }
+        },
         /**
          * 删除按钮响应事件
          * @param sheet
@@ -500,9 +583,9 @@ $(document).ready(function() {
                                 if (lPos && lPos.length > 0) {
                                     if (!bHint) {
                                         if (col.field === 'code') {
-                                            toast('清单含有部位明细,请先删除部位明细,再删除清单编号', 'warning');
+                                            toastr.warning('清单含有部位明细,请先删除部位明细,再删除清单编号');
                                         } else {
-                                            toast('清单含有部位明细,数量金额根据部位明细汇总计算所得,不可删除', 'warning');
+                                            toastr.warning('清单含有部位明细,数量金额根据部位明细汇总计算所得,不可删除');
                                         }
                                         bHint = true;
                                     }
@@ -574,6 +657,10 @@ $(document).ready(function() {
         },
         cut: function (sheet, sel, callback) {
             if (!sheet || !sel) return;
+            if (sel.colCount >= sheet.zh_setting.cols.length) {
+                toastr.warning('请勿选中整行剪切');
+                return;
+            }
 
             const sortData = SpreadJsObj.getSortData(sheet), datas = [];
             for (let iRow = sel.row; iRow < sel.row + sel.rowCount; iRow++) {
@@ -587,13 +674,13 @@ $(document).ready(function() {
                             col.field === 'qtcl_qty' || col.field === 'qtcl_tp') {
                             const lPos = pos.getLedgerPos(node.id);
                             if (lPos && lPos.length > 0) {
-                                toast('不可剪切', 'warning');
+                                toastr.error('不可剪切');
                                 return;
                             }
                         }
                         const style = sheet.getStyle(iRow, iCol);
                         if (style.locked) {
-                            toast('不可剪切', 'warning');
+                            toastr.error('不可剪切');
                             return;
                         }
                         const colSetting = sheet.zh_setting.cols[iCol];
@@ -605,31 +692,56 @@ $(document).ready(function() {
 
             if (datas.length > 0) {
                 postData(window.location.pathname + '/update', {postType: 'update', postData: datas}, function (result) {
-                    const refreshNode = tree.loadPostData(result);
+                    const refreshNode = sheet.zh_tree.loadPostData(result);
                     callback();
                     treeOperationObj.refreshTree(sheet, refreshNode);
                 });
             }
         },
+        editStarting(e, info) {
+            if (!info.sheet.zh_setting || !info.sheet.zh_tree) return;
+            const col = info.sheet.zh_setting.cols[info.col];
+            const node = info.sheet.zh_tree.nodes[info.row];
+            switch (col.field) {
+                case 'unit_price':
+                case 'sgfh_qty':
+                case 'sgfh_tp':
+                case 'sjcl_qty':
+                case 'sjcl_tp':
+                case 'qtcl_qty':
+                case 'qtcl_tp':
+                case 'deal_qty':
+                case 'deal_tp':
+                    info.cancel = node.children && node.children.length > 0;
+                    break;
+                case 'dgn_qty1':
+                case 'dgn_qty2':
+                    info.cancel = !_.isEmpty(node.b_code);
+                    break;
+            }
+        }
     };
+    ledgerTreeCol.initSpreadSetting(ledgerSpreadSetting);
+    SpreadJsObj.initSheet(ledgerSpread.getActiveSheet(), ledgerSpreadSetting);
     // 绑定事件
     ledgerSpread.bind(GC.Spread.Sheets.Events.SelectionChanged, treeOperationObj.selectionChanged);
     ledgerSpread.bind(spreadNS.Events.TopRowChanged, treeOperationObj.topRowChanged);
+
     if (!ledgerSpreadSetting.readOnly) {
         ledgerSpread.bind(GC.Spread.Sheets.Events.SelectionChanged, function (e, info) {
             treeOperationObj.refreshOperationValid(info.sheet, info.newSelections);
         });
         ledgerSpread.bind(GC.Spread.Sheets.Events.EditEnded, treeOperationObj.editEnded);
-        ledgerSpread.bind(GC.Spread.Sheets.Events.ClipboardPasted, treeOperationObj.clipboardPasted);
+        //ledgerSpread.bind(GC.Spread.Sheets.Events.ClipboardPasted, treeOperationObj.clipboardPasted);
         SpreadJsObj.addDeleteBind(ledgerSpread, treeOperationObj.deletePress);
         ledgerSpread.bind(GC.Spread.Sheets.Events.ClipboardChanging, function (e, info) {
             const copyText = SpreadJsObj.getFilterCopyText(info.sheet);
+            //console.log(info.copyData);
+            SpreadJsObj.Clipboard.setCopyData(copyText);
+            //copyToClipboard(copyText);
         });
-        ledgerSpread.bind(GC.Spread.Sheets.Events.ClipboardChanged, function (e, info) {
-            console.log(info.copyData.text);
-        });
-        ledgerSpread.bind(GC.Spread.Sheets.Events.ClipboardPasting, function (e, info) {
-        });
+        ledgerSpread.bind(GC.Spread.Sheets.Events.ClipboardPasting, treeOperationObj.clipboardPasting);
+        ledgerSpread.bind(spreadNS.Events.EditStarting, treeOperationObj.editStarting);
         SpreadJsObj.addCutEvents(ledgerSpread, treeOperationObj.cut);
 
         // 绑定 删除等 顶部按钮
@@ -651,6 +763,24 @@ $(document).ready(function() {
         $('#down-level').click(function () {
             treeOperationObj.downLevel(ledgerSpread.getActiveSheet());
         });
+        $('#copy').click(function () {
+            ledgerSpread.commandManager().execute({
+                cmd: 'copy',
+                sheetName: ledgerSpread.getActiveSheet().name()
+            });
+        });
+        $('#paste').click(function () {
+            ledgerSpread.commandManager().execute({
+                cmd: 'paste',
+                sheetName: ledgerSpread.getActiveSheet().name()
+            });
+        });
+        $('#cut').click(function () {
+            ledgerSpread.commandManager().execute({
+                cmd: 'cut',
+                sheetName: ledgerSpread.getActiveSheet().name()
+            });
+        });
 
         let batchInsertObj;
         // 右键菜单
@@ -718,7 +848,7 @@ $(document).ready(function() {
                             }
                         }
                         const valid = !sheet.zh_setting.readOnly;
-                        return !(valid && first && sameParent && first.level > 1 && !first.node_type);
+                        return !(valid && first && sameParent && !(first.level === 1 && first.node_type));
                     }
                 },
                 'copyBlock': {
@@ -738,7 +868,7 @@ $(document).ready(function() {
                         while (iRow < sel.row + sel.rowCount) {
                             const node = sheet.zh_tree.nodes[iRow];
                             if (node.ledger_pid !== pid) {
-                                toast('error: 仅可同时选中同层节点', 'error', 'exclamation-circle');
+                                toastr.error('仅可同时选中同层节点');
                                 return;
                             }
                             copyBlockList.push(node.ledger_id);
@@ -819,8 +949,6 @@ $(document).ready(function() {
         SpreadJsObj.forbiddenSpreadContextMenu('#ledger-spread', ledgerSpread);
     }
 
-    treeOperationObj.refreshOperationValid(ledgerSpread.getActiveSheet());
-
     const posSearch = $.posSearch({selector: '#pos-search', searchSpread: posSpread});
     // 台账模式加载部位明细数据
     if (checkTzMeasureType()) {
@@ -834,11 +962,6 @@ $(document).ready(function() {
             }
         });
         SpreadJsObj.initSheet(posSpread.getActiveSheet(), posSpreadSetting);
-        postData('/tender/' + getTenderId() + '/pos', null, function (data) {
-            pos.loadDatas(data);
-            posOperationObj.loadCurPosData();
-            SpreadJsObj.resetTopAndSelect(posSpread.getActiveSheet());
-        });
     }
     // 绑定部位明细编辑事件
     const posOperationObj = {
@@ -875,15 +998,15 @@ $(document).ready(function() {
                 //const node = treeOperationObj.getSelectNode(ledgerSpread.getActiveSheet());
                 const node = posOperationObj.ledgerTreeNode;
                 if (!node) {
-                    toast('数据错误,请选择台账节点后再试', 'error');
+                    toastr.error('数据错误,请选择台账节点后再试');
                     SpreadJsObj.reLoadRowData(info.sheet, info.row);
                     return;
                 } else if (newText && newText !== '' && node.children && node.children.length > 0) {
-                    toast('父节点不可插入部位明细', 'error');
+                    toastr.error('父节点不可插入部位明细');
                     SpreadJsObj.reLoadRowData(info.sheet, info.row);
                     return;
                 } else if (newText && newText !== '' && (!node.b_code || node.b_code === '')) {
-                    toast('项目节不可插入部位明细', 'error');
+                    toastr.error('项目节不可插入部位明细');
                     SpreadJsObj.reLoadRowData(info.sheet, info.row);
                     return;
                 }
@@ -891,7 +1014,7 @@ $(document).ready(function() {
                 const data = {};
                 if (col.field === 'name') {
                     if (newText === '' && posData) {
-                        toast('部位名称不可为空', 'error', 'exclamation-circle');
+                        toastr.error('部位名称不可为空');
                         SpreadJsObj.reLoadRowData(info.sheet, info.row);
                         return;
                     } else if (!posData) {
@@ -908,7 +1031,7 @@ $(document).ready(function() {
                         data.updateData = {id: posData.id, name: newText};
                     }
                 } else if (!posData) {
-                    toast('新增部位请先输入名称', 'warning');
+                    toastr.warning('新增部位请先输入名称');
                     SpreadJsObj.reLoadRowData(info.sheet, info.row);
                     return;
                 } else {
@@ -955,7 +1078,7 @@ $(document).ready(function() {
                         for (let iCol = sel.col; iCol < sel.col + sel.colCount; iCol++) {
                             const colSetting = sheet.zh_setting.cols[iCol];
                             if (colSetting.field === 'name') {
-                                toast('部位名称不能为空', 'error');
+                                toastr.error('部位名称不能为空');
                                 return;
                             }
                             const style = sheet.getStyle(iRow, iCol);
@@ -1019,12 +1142,12 @@ $(document).ready(function() {
         clipboardPasted: function (e, info) {
             const node = treeOperationObj.getSelectNode(ledgerSpread.getActiveSheet());
             if (node.code && (node.code !== '')) {
-                toast('项目节不可含有清单明细', 'error');
+                toastr.error('项目节不可含有清单明细');
                 posOperationObj.loadCurPosData();
                 return;
             }
             if (node.children && (node.children.length > 0)) {
-                toast('仅清单子项可以含有部位明细', 'error');
+                toastr.error('仅清单子项可以含有部位明细');
                 posOperationObj.loadCurPosData();
                 return;
             }
@@ -1034,7 +1157,7 @@ $(document).ready(function() {
                 const sortData = info.sheet.zh_data || [];
                 if (sortData.length === 0 || info.cellRange.row + info.cellRange.rowCount > sortData.length) {
                     if (info.cellRange.col !== 0) {
-                        toast('新增部位请先输入名称', 'warning');
+                        toastr.warning('新增部位请先输入名称');
                         posOperationObj.loadCurPosData();
                         return;
                     }
@@ -1101,6 +1224,19 @@ $(document).ready(function() {
         SpreadJsObj.forbiddenSpreadContextMenu('#pos-spread', posSpread);
     }
 
+    postData(window.location.pathname + '/load', {}, function (data) {
+        ledgerTree.loadDatas(data.bills);
+        treeCalc.calculateAll(ledgerTree);
+        SpreadJsObj.loadSheetData(ledgerSpread.getActiveSheet(), 'tree', ledgerTree);
+        SpreadJsObj.loadTopAndSelect(ledgerSpread.getActiveSheet(), ckBillsSpread);
+
+        pos.loadDatas(data.pos);
+        posOperationObj.loadCurPosData();
+        SpreadJsObj.resetTopAndSelect(posSpread.getActiveSheet());
+
+        treeOperationObj.refreshOperationValid(ledgerSpread.getActiveSheet());
+    }, null, true);
+
     let stdXmj, stdGcl, dealBills, searchLedger;
 
     $.divResizer({
@@ -1286,9 +1422,9 @@ $(document).ready(function() {
                 const sel = mainSheet.getSelections()[0];
                 const mainNode = mainTree.nodes[sel.row];
                 if (!stdNode) { return; }
-                if (stdType === 'bills') {
-                    if (!(mainNode.b_code && mainNode.b_code !== '') && !mainTree.isLeafXmj(mainNode)) {
-                        toast('非最底层项目下,不应添加清单', 'warning');
+                if (stdType === 'gcl') {
+                    if (mainNode.code && mainNode.code !== '' && !mainTree.isLeafXmj(mainNode)) {
+                        toastr.warning('非最底层项目下,不应添加清单');
                         return;
                     }
                 }
@@ -1351,7 +1487,7 @@ $(document).ready(function() {
                 if (!mainNode || !mainTree) { return; }
 
                 if (mainNode.code && mainNode.code !== '' && !mainTree.isLeafXmj(mainNode)) {
-                    toast('非最底层项目下,不应添加清单', 'error');
+                    toastr.error('非最底层项目下,不应添加清单');
                     return;
                 }
 
@@ -1621,7 +1757,7 @@ $(document).ready(function() {
                 $('p', resultDiv).text(data.company);
                 resultDiv.show();
             } else {
-                toast('未查询到该审核人', 'info');
+                toastr.info('未查询到该审核人');
                 resultDiv.hide();
             }
         }, () => {
@@ -1739,14 +1875,14 @@ $(document).ready(function() {
                         $(this).removeClass('border-primary');
                     })
                 } else {
-                    toast('选择的Excel无有效数据,请重新选择', 'hint');
+                    toastr.info('选择的Excel无有效数据,请重新选择');
                     $('#upload-ledger-sheets').hide();
                 }
                 $('#select-excel-loading').hide();
             });
         } catch(err) {
             $('#select-excel-loading').hide();
-            toast('加载excel异常,请刷新当前页面', 'error');
+            toastr.error('加载excel异常,请刷新当前页面');
             $('#upload-ledger-sheets').hide();
         }
     });
@@ -1807,7 +1943,7 @@ $(document).ready(function() {
 // 检查上报情况
 function checkAuditorFrom () {
     if ($('#auditors li').length === 0) {
-        toast('请先选择审批人,再上报数据', 'error', 'exclamation-circle');
+        toastr.error('请先选择审批人,再上报数据');
         return false;
     }
 }

+ 11 - 9
app/public/js/ledger_audit.js

@@ -35,11 +35,7 @@ $(document).ready(() => {
         treeSetting.calcFields = ['deal_tp', 'sgfh_tp', 'sjcl_tp', 'qtcl_tp', 'total_price'];
     }
     const ledgerTree = createNewPathTree('fx', treeSetting);
-    ledgerTree.loadDatas(ledger);
-    treeCalc.calculateAll(ledgerTree);
     SpreadJsObj.initSheet(ledgerSpread.getActiveSheet(), ledgerSpreadSetting);
-    SpreadJsObj.loadSheetData(ledgerSpread.getActiveSheet(), 'tree', ledgerTree);
-    SpreadJsObj.loadTopAndSelect(ledgerSpread.getActiveSheet(), ckBillsSpread);
 
     // 初始化 部位明细
     const pos = new PosData({
@@ -87,13 +83,19 @@ $(document).ready(() => {
             }
         });
         SpreadJsObj.initSheet(posSpread.getActiveSheet(), posSpreadSetting);
-        postData('/tender/' + getTenderId() + '/pos', null, function (data) {
-            pos.loadDatas(data);
-            loadCurPosData();
-            SpreadJsObj.resetTopAndSelect(posSpread.getActiveSheet());
-        });
     }
 
+    postData('/tender/' + getTenderId() + '/ledger/load', null, function (data) {
+        ledgerTree.loadDatas(ledger);
+        treeCalc.calculateAll(ledgerTree);
+        SpreadJsObj.loadSheetData(ledgerSpread.getActiveSheet(), 'tree', ledgerTree);
+        SpreadJsObj.loadTopAndSelect(ledgerSpread.getActiveSheet(), ckBillsSpread);
+
+        pos.loadDatas(data.pos);
+        loadCurPosData();
+        SpreadJsObj.resetTopAndSelect(posSpread.getActiveSheet());
+    }, null, true);
+
     ledgerSpread.bind(spreadNS.Events.SelectionChanged, function (e, info) {
         loadCurPosData();
         SpreadJsObj.saveTopAndSelect(info.sheet, ckBillsSpread);

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

@@ -42,7 +42,8 @@ const ledgerTreeCol = {
     initSpreadSetting(setting) {
         for (const col of setting.cols) {
             if (col.readOnly && Object.prototype.toString.apply(col.readOnly) === "[object String]") {
-                col.readOnly = this.getEvent(col.readOnly);
+                //col.readOnly = this.getEvent(col.readOnly);
+                delete col.readOnly;
             }
         }
     }

+ 207 - 0
app/public/js/material.js

@@ -0,0 +1,207 @@
+'use strict';
+
+/**
+ * 材料调差 - 调差工料
+ *
+ * @author Mai
+ * @date 2019/1/16
+ * @version
+ */
+
+function loadUpdateMaterials(newMaterial, fields) {
+    const newMaterials = newMaterial instanceof Array ? newMaterial : [newMaterial];
+    for (const nm of newMaterials) {
+        const om = _.find(materialBillsData, {id: nm.id});
+        for (const prop in nm) {
+            if (!fields || fields.indexOf(prop) >= 0) {
+                om[prop] = nm[prop];
+            }
+        }
+    }
+}
+$(document).ready(() => {
+    autoFlashHeight();
+    const materialSpread = SpreadJsObj.createNewSpread($('#material-spread')[0]);
+    const materialSpreadSetting = {
+        cols: [
+            {title: '调差类型', colSpan: '1', rowSpan: '2', field: 't_type', hAlign: 0, width: 80, formatter: '@', readOnly: 'readOnly.isEdit', cellType: 'customizeCombo', comboItems: materialType.t_type, cellTypeKey: 1},
+            {title: '编号', colSpan: '1', rowSpan: '2', field: 'code', hAlign: 0, width: 60, formatter: '@', readOnly: 'readOnly.isEdit'},
+            {title: '名称', colSpan: '1', rowSpan: '2', field: 'name', hAlign: 0, width: 180, formatter: '@', readOnly: 'readOnly.isEdit'},
+            {title: '单位', colSpan: '1', rowSpan: '2', field: 'unit', hAlign: 0, width: 60, formatter: '@', readOnly: 'readOnly.isEdit'},
+            {title: '规格', colSpan: '1', rowSpan: '2', field: 'spec', hAlign: 0, width: 230, formatter: '@', readOnly: 'readOnly.isEdit'},
+            {title: '工料分类', colSpan: '1', rowSpan: '2', field: 'm_type', hAlign: 0, width: 60, readOnly: 'readOnly.isEdit', cellType: 'customizeCombo', comboItems: materialType.m_type, cellTypeKey: 2},
+            {title: '本期应耗数量', colSpan: '1', rowSpan: '2', field: 'quantity', hAlign: 2, width: 100, type: 'Number', readOnly: 'readOnly.isEdit'},
+            {title: '基准价', colSpan: '1', rowSpan: '2', field: 'basic_price', hAlign: 2, width: 60, type: 'Number', readOnly: 'readOnly.isEdit'},
+            {title: '基准时间', colSpan: '1', rowSpan: '2', field: 'basic_times', hAlign: 0, width: 60, formatter: '@', readOnly: 'readOnly.isEdit'},
+            {title: '本期信息价|单价', colSpan: '3|1', rowSpan: '1|1', field: 'msg_tp', hAlign: 2, width: 60, type: 'Number', readOnly: 'readOnly.isEdit'},
+            {title: '|时间', colSpan: '|1', rowSpan: '|1', field: 'msg_times', hAlign: 0, width: 60, formatter: '@', readOnly: 'readOnly.isEdit'},
+            {title: '|价差', colSpan: '1', rowSpan: '1|1', field: 'msg_spread', hAlign: 2, width: 60, type: 'Number', readOnly: true},
+            {title: '本期材料调差|风险幅度(%)', colSpan: '3|1', rowSpan: '1|1', field: 'm_risk', hAlign: 2, width: 100, type: 'Number', readOnly: 'readOnly.isEdit'},
+            {title: '|有效价差', colSpan: '|1', rowSpan: '|1', field: 'm_spread', hAlign: 2, width: 80, type: 'Number', readOnly: true},
+            {title: '|调整金额', colSpan: '|1', rowSpan: '1|1', field: 'm_tp', hAlign: 2, width: 80, type: 'Number', readOnly: true},
+            {title: '截止上期调差金额', colSpan: '1', rowSpan: '2', field: 'pre_tp', hAlign: 2, width: 120, type: 'Number', readOnly: true},
+            {title: '备注', colSpan: '1', rowSpan: '2', field: 'remark', hAlign: 0, width: 60, formatter: '@', readOnly: 'readOnly.remark'},
+        ],
+        emptyRows: 0,
+        headRows: 2,
+        headRowHeight: [32, 32],
+        defaultRowHeight: 30,
+        headerFont: '10pt 微软雅黑',
+        readOnly: readOnly,
+        font: '10pt 微软雅黑',
+    };
+
+    const materialBase = {
+        isEdit: function (data) {
+            return materialListData.find(function (item) {
+                return item.mbid === data.id;
+            });
+        }
+    }
+
+    const materialCol = {
+        readOnly: {
+            isEdit: function (data) {
+                return !(readOnly || materialBase.isEdit(data) === undefined);
+            },
+            remark: function () {
+                return readOnly;
+            },
+        },
+    };
+    SpreadJsObj.initSpreadSettingEvents(materialSpreadSetting, materialCol);
+    SpreadJsObj.initSheet(materialSpread.getActiveSheet(), materialSpreadSetting);
+    SpreadJsObj.loadSheetData(materialSpread.getActiveSheet(), SpreadJsObj.DataType.Data, materialBillsData);
+
+    const materialSpreadObj = {
+        refreshActn: function () {
+            const setObjEnable = function (obj, enable) {
+                if (enable) {
+                    obj.removeClass('disabled');
+                } else {
+                    obj.addClass('disabled');
+                }
+            };
+            const sheet = materialSpread.getActiveSheet();
+            const select = SpreadJsObj.getSelectObject(sheet);
+            // 还需判断是否已被调差清单调用
+            setObjEnable($('#del'), !readOnly && select && materialBase.isEdit(select) === undefined);
+        },
+        add: function () {
+            const sheet = materialSpread.getActiveSheet();
+            postData(window.location.pathname + '/save', {type: 'add'}, function (result) {
+                if (result) {
+                    materialBillsData.push(result);
+                    sheet.addRows(materialBillsData.length - 1, 1);
+                    SpreadJsObj.reLoadRowData(sheet, materialBillsData.length - 1);
+                    sheet.setSelection(materialBillsData.length - 1, 0, 1, 1);
+                    materialSpreadObj.refreshActn();
+                }
+            });
+        },
+        del: function () {
+            const sheet = materialSpread.getActiveSheet();
+            const select = SpreadJsObj.getSelectObject(sheet);
+            console.log(select);
+            postData(window.location.pathname + '/save', {type: 'del', id: select.id}, function (result) {
+                const index = materialBillsData.indexOf(select);
+                materialBillsData.splice(index, 1);
+                sheet.deleteRows(index, 1);
+                SpreadJsObj.reLoadSheetData(materialSpread.getActiveSheet());
+                const sel = sheet.getSelections();
+                sheet.setSelection(index > 0 ? index - 1 : 0, sel.length > 0 ? sel[0].col : 0, 1, 1);
+                materialSpreadObj.refreshActn();
+            });
+        },
+        selectionChanged: function (e, info) {
+            materialSpreadObj.refreshActn();
+            const sel = info.sheet.getSelections()[0];
+            const col = info.sheet.zh_setting.cols[sel.col];
+            const data = SpreadJsObj.getSelectObject(info.sheet);
+            materialSpreadObj.setReadOnly(true);
+        },
+        editEnded: function (e, info) {
+            if (info.sheet.zh_setting) {
+                const select = SpreadJsObj.getSelectObject(info.sheet);
+                const col = info.sheet.zh_setting.cols[info.col];
+                // 未改变值则不提交
+                const validText = info.editingText ? (typeof(info.editingText) === 'String' ? info.editingText.replace('\n', '') : info.editingText) : null;
+                const orgValue = select[col.field];
+                if (orgValue == validText || ((!orgValue || orgValue === '') && (validText === ''))) {
+                    SpreadJsObj.reLoadRowData(info.sheet, info.row);
+                    return;
+                }
+                select[col.field] = validText;
+                // 更新至服务器
+                postData(window.location.pathname + '/save', { type:'update', updateData: select }, function (result) {
+                    SpreadJsObj.reLoadRowData(info.sheet, info.row);
+                }, function () {
+                    select[col.field] = orgValue;
+                    SpreadJsObj.reLoadRowData(info.sheet, info.row);
+                });
+            }
+        },
+        setReadOnly: function(readOnly) {
+            SpreadJsObj.resetFieldReadOnly(materialSpread.getActiveSheet(), 'msg_spread', 'm_spread', 'm_tp', 'pre_tp', readOnly);
+        }
+    };
+    materialSpreadObj.refreshActn();
+    materialSpread.bind(spreadNS.Events.SelectionChanged, materialSpreadObj.selectionChanged);
+    if (!readOnly) {
+        $('#add').click(materialSpreadObj.add);
+        $('#del').click(materialSpreadObj.del);
+        materialSpread.bind(spreadNS.Events.EditEnded, materialSpreadObj.editEnded);
+        // 右键菜单
+        $.contextMenu({
+            selector: '#material-spread',
+            build: function ($trigger, e) {
+                const target = SpreadJsObj.safeRightClickSelection($trigger, e, materialSpread);
+                return target.hitTestType === GC.Spread.Sheets.SheetArea.viewport || target.hitTestType === GC.Spread.Sheets.SheetArea.rowHeader;
+            },
+            items: {
+                'create': {
+                    name: '新增材料',
+                    icon: 'fa-sign-in',
+                    callback: function (key, opt) {
+                        materialSpreadObj.add(materialSpread.getActiveSheet());
+                    },
+                },
+                'delete': {
+                    name: '删除材料',
+                    icon: 'fa-remove',
+                    callback: function (key, opt) {
+                        materialSpreadObj.del(materialSpread.getActiveSheet());
+                    },
+                    disabled: function (key, opt) {
+                        const sheet = materialSpread.getActiveSheet();
+                        const select = SpreadJsObj.getSelectObject(sheet);
+                        materialSpreadObj.refreshActn();
+                        if (!readOnly && select && materialBase.isEdit(select) === undefined) {
+                            return false;
+                        } else {
+                            return true;
+                        }
+                    }
+                },
+            }
+        });
+    } else {
+        SpreadJsObj.forbiddenSpreadContextMenu('#material-spread', materialSpread);
+    }
+    $.subMenu({
+        menu: '#sub-menu', miniMenu: '#sub-mini-menu', miniMenuList: '#mini-menu-list',
+        toMenu: '#to-menu', toMiniMenu: '#to-mini-menu',
+        //key: 'stage.compare.memu.1.0.0',
+        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();
+            materialSpread.refresh();
+        }
+    });
+});

+ 147 - 0
app/public/js/material_audit.js

@@ -0,0 +1,147 @@
+'use strict';
+
+/**
+ *
+ *
+ * @author Mai
+ * @date 2019/2/27
+ * @version
+ */
+
+$(document).ready(function () {
+    // 获取审核相关url
+    function getUrlPre () {
+        const path = window.location.pathname.split('/');
+        return _.take(path, 6).join('/');
+    }
+
+    // 搜索审批人
+    $('#searchAccount').click(() => {
+        const data = {
+            keyword: $('#searchName').val(),
+        };
+        postData('/search/user', data, (data) => {
+            const resultDiv = $('#searchResult');
+            if (data) {
+                $('h5>span', resultDiv).text(data.name);
+                $('#addAuditor').attr('auditorId', data.id);
+                $('h6', resultDiv).text(data.role);
+                $('p', resultDiv).text(data.company);
+                resultDiv.show();
+            } else {
+                toast('未查询到该审核人', 'info');
+                resultDiv.hide();
+            }
+        }, () => {
+            $('#searchResult').hide();
+        });
+    });
+    // 添加审批人
+    $('#addAuditor').click(() => {
+        postData(getUrlPre() + '/audit/add', { auditorId: $('#addAuditor').attr('auditorId') }, (data) => {
+            const html = [];
+            html.push('<li class="list-group-item" auditorId="'+ data.aid +'"><a href="javascript: void(0)" class="text-danger pull-right">移除</a>');
+            html.push('<span>');
+            html.push(data.order + ' ');
+            html.push(data.name + ' ');
+            html.push('</span>');
+            html.push('<small class="text-muted">');
+            html.push(data.role);
+            html.push('</small></li>');
+            $('#auditors').append(html.join(''));
+        });
+    });
+    // 审批人分组选择
+    $('#account_group').change(function () {
+        let account_html = '<option value="0">选择审批人</option>';
+        for (const account of accountList) {
+            if (parseInt($(this).val()) === 0 || parseInt($(this).val()) === account.account_group) {
+                const role = account.role !== '' ? '(' + account.role + ')' : '';
+                const company = account.company !== '' ? ' -' + account.company : '';
+                account_html += '<option value="' + account.id + '">' + account.name + role + company + '</option>';
+            }
+        }
+        $('#account_list').html(account_html);
+    });
+    // 添加到审批流程中
+    $('body').on('change', '#account_list', function () {
+        let id = $(this).val();
+        id = parseInt(id);
+        if (id !== 0) {
+            postData(getUrlPre() + '/audit/add', { auditorId: id }, (data) => {
+                const html = [];
+                html.push('<li class="list-group-item" auditorId="'+ data.aid +'"><a href="javascript: void(0)" class="text-danger pull-right">移除</a>');
+                html.push('<span>');
+                html.push(data.order + ' ');
+                html.push(data.name + ' ');
+                html.push('</span>');
+                html.push('<small class="text-muted">');
+                html.push(data.role);
+                html.push('</small></li>');
+                $('#auditors').append(html.join(''));
+
+                // 如果是重新上报,添加到重新上报列表中
+                const auditorshtml = [];
+                // 重新上报时。令其它的审批人流程图标转换
+                $('#auditors-list li i').removeClass('fa-stop-circle').addClass('fa-chevron-circle-down');
+                // 添加新审批人
+                auditorshtml.push('<li class="list-group-item" data-auditid="' + data.aid + '">');
+                auditorshtml.push('<i class="fa fa-stop-circle"></i> ');
+                auditorshtml.push(data.name + ' <small class="text-muted">' + data.role + '</small>');
+                auditorshtml.push('</li>');
+                $('#auditors-list').append(auditorshtml.join(''));
+
+                const auditorshtml2 = [];
+                // 重新上报时。令其它的审批人流程图标转换
+                $('#auditors-list2 li i').removeClass('fa-stop-circle').addClass('fa-chevron-circle-down');
+                // 添加新审批人
+                auditorshtml2.push('<li class="list-group-item" data-auditid="' + data.aid + '">');
+                auditorshtml2.push('<h5 class="card-title"><i class="fa fa-stop-circle"></i> ');
+                auditorshtml2.push(data.name + ' <small class="text-muted">' + data.role + '</small></h5>');
+                auditorshtml2.push('</li>');
+                $('#auditors-list2').append(auditorshtml2.join(''));
+            });
+        }
+    });
+    // 删除审批人
+    $('body').on('click', '#auditors li>a', function () {
+        const li = $(this).parent();
+        const data = {
+            auditorId: parseInt(li.attr('auditorId')),
+        };
+        postData(getUrlPre() + '/audit/delete', data, (result) => {
+            li.remove();
+            for (const rst of result) {
+                const aLi = $('li[auditorId=' + rst.aid + ']');
+                $('span', aLi).text(rst.order + ' ' + rst.name + ' ');
+            }
+
+            // 如果是重新上报
+            // 令最后一个图标转换
+            $('#auditors-list li[data-auditid="' + data.auditorId + '"]').remove();
+            if ($('#auditors-list li').length !== 0 && !$('#auditors-list li i').hasClass('fa-stop-circle')) {
+                $('#auditors-list li').eq($('#auditors-list li').length-1).children('i')
+                    .removeClass('fa-chevron-circle-down').addClass('fa-stop-circle');
+            }
+            $('#auditors-list2 li[data-auditid="' + data.auditorId + '"]').remove();
+            if ($('#auditors-list2 li').length !== 0 && !$('#auditors-list2 li i').hasClass('fa-stop-circle')) {
+                $('#auditors-list2 li').eq($('#auditors-list2 li').length-1).children('i')
+                    .removeClass('fa-chevron-circle-down').addClass('fa-stop-circle');
+            }
+        });
+    });
+    // 退回选择修改审批人流程
+    $('#hideSp').click(function () {
+        $('#sp-list2').modal('hide');
+    });
+    $('a[f-target]').click(function () {
+        $($(this).attr('f-target')).modal('show');
+    })
+});
+// 检查上报情况
+function checkAuditorFrom () {
+    if ($('#auditors li').length === 0) {
+        toast('请先选择审批人,再上报数据', 'error', 'exclamation-circle');
+        return false;
+    }
+}

文件差異過大導致無法顯示
+ 56 - 0
app/public/js/math.min.js


+ 108 - 0
app/public/js/measure_material.js

@@ -0,0 +1,108 @@
+'use strict';
+
+/**
+ * 期计量 - 期列表页面 js
+ *
+ * @author Mai
+ * @date 2018/12/7
+ * @version
+ */
+$(function () {
+    // 获取审批流程
+    $('a[data-target="#sp-list" ]').on('click', function () {
+        const data = {
+            order: $(this).attr('m-order'),
+        };
+        postData('/tender/' + tenderId + '/measure/material/auditors', data, function (result) {
+            const materialAuditor = result.materialAuditor;
+            const auditors = result.auditors;
+            const auditHistory = result.auditHistory;
+            // 生成左边列表流程
+            const lefthtml = [];
+            lefthtml.push('<li class="list-group-item"><i class="fa fa fa-play-circle fa-rotate-90"></i> '+ materialAuditor.name +'  <small class="text-muted">'+ materialAuditor.role +'</small></li>');
+            for (const [index,a] of auditors.entries()) {
+                if (index+1 === auditors.length) {
+                    lefthtml.push('<li class="list-group-item"><i class="fa fa-stop-circle"></i> '+ a.name +'  <small class="text-muted">'+ a.role +'</small></li>');
+                } else {
+                    lefthtml.push('<li class="list-group-item"><i class="fa fa-chevron-circle-down"></i> '+ a.name +'  <small class="text-muted">'+ a.role +'</small></li>');
+                }
+            }
+            $('#auditor-list').html(lefthtml.join(''));
+
+            // 生成右边列表流程
+            const righthtml = [];
+            for(const ah of auditHistory) {
+                righthtml.push('<div class="card mt-3"><ul class="list-group list-group-flush">');
+                for (let iA = 0; iA < ah.length; iA++) {
+                    if (iA === 0) {
+                        righthtml.push('<li class="list-group-item">');
+                        righthtml.push('<span class="text-success pull-right">'+ (auditHistory.indexOf(ah) > 0 ? '重新' : '') +'上报</span>');
+                        righthtml.push('<h5 class="card-title">');
+                        righthtml.push('<i class="fa fa-play-circle fa-rotate-90 text-success"></i> '+ materialAuditor.name +' <small class="text-muted">'+ materialAuditor.role +'</small></h5>');
+                        righthtml.push('<p class="card-text"><small class="text-muted">' + (ah[iA].begin_time ? moment(ah[iA].begin_time).format('YYYY-MM-DD') : '') + '</small></p></li>');
+                        righthtml.push('<li class="list-group-item">');
+                        if (ah[iA].status !== auditConst.status.uncheck) {
+                            righthtml.push('<span class="'+ auditConst.statusClass[ah[iA].status] +' pull-right">' + auditConst.statusString[ah[iA].status] + (ah[iA].status === auditConst.status.checkNo ? ' ' + materialAuditor.name : '') + '</span>');
+                        }
+                        righthtml.push('<h5 class="card-title"><i class="fa '+ (iA === ah.length - 1 ? 'fa-stop-circle ' : 'fa-chevron-circle-down ') + auditConst.statusClass[ah[iA].status] +'"></i> '+ ah[iA].name +' <small class="text-muted">'+ ah[iA].role +'</small></h5>');
+                        if (ah[iA].status === auditConst.status.checked || ah[iA].status === auditConst.status.checkNo) {
+                            righthtml.push('<p class="card-text mb-1">'+ ah[iA].opinion +'</p>');
+                            righthtml.push('<p class="card-text"><small class="text-muted">'+ (ah[iA].end_time ? moment(ah[iA].end_time).format('YYYY-MM-DD') : '') +'</small></p>');
+                        }
+                        righthtml.push('</li>');
+                    } else if (iA === ah.length - 1) {
+                        righthtml.push('<li class="list-group-item">');
+                        if (ah[iA].status !== auditConst.status.uncheck) {
+                            righthtml.push('<span class="'+ auditConst.statusClass[ah[iA].status] +' pull-right">' + auditConst.statusString[ah[iA].status] + (ah[iA].status === auditConst.status.checkNo ? ' ' + materialAuditor.name : '') + '</span>');
+                        }
+                        righthtml.push('<h5 class="card-title"><i class="fa fa-stop-circle '+ auditConst.statusClass[ah[iA].status] +'"></i> '+ ah[iA].name +' <small class="text-muted">'+ ah[iA].role +'</small></h5>');
+                        if (ah[iA].status === auditConst.status.checked || ah[iA].status === auditConst.status.checkNo) {
+                            righthtml.push('<p class="card-text mb-1">'+ ah[iA].opinion +'</p>');
+                            righthtml.push('<p class="card-text"><small class="text-muted">'+ (ah[iA].end_time ? moment(ah[iA].end_time).format('YYYY-MM-DD') : '') +'</small></p>');
+                        }
+                        righthtml.push('</li>');
+                    } else {
+                        righthtml.push('<li class="list-group-item">');
+                        if (ah[iA].status !== auditConst.status.uncheck) {
+                            righthtml.push('<span class="'+ auditConst.statusClass[ah[iA].status] +' pull-right">' + auditConst.statusString[ah[iA].status] + (ah[iA].status === auditConst.status.checkNo ? ' ' + materialAuditor.name : '') + '</span>');
+                        }
+                        righthtml.push('<h5 class="card-title"><i class="fa fa-chevron-circle-down '+ auditConst.statusClass[ah[iA].status] +'"></i> '+ ah[iA].name +' <small class="text-muted">'+ ah[iA].role +'</small></h5>');
+                        if (ah[iA].status === auditConst.status.checked || ah[iA].status === auditConst.status.checkNo) {
+                            righthtml.push('<p class="card-text mb-1">'+ ah[iA].opinion +'</p>');
+                            righthtml.push('<p class="card-text"><small class="text-muted">'+ (ah[iA].end_time ? moment(ah[iA].end_time).format('YYYY-MM-DD') : '') +'</small></p>');
+                        }
+                        righthtml.push('</li>');
+                    }
+                }
+                righthtml.push('</ul></div>');
+            }
+            $('#auditor-list2').html(righthtml.join(''));
+        });
+    });
+
+    // 计量期选中
+    $('.select-stage-order').on('click', function () {
+        const stageList = $('.select-stage-order:checked');
+        if (stageList.length === 0) {
+            $('#show_order').hide();
+            $('#s_order').val('');
+        } else {
+            const order_array = [];
+            for (let s = 0; s < stageList.length; s++) {
+                order_array.push(stageList.eq(s).data('order'));
+                // console.log(stageList.eq(s));
+            }
+            $('#show_order').html('第<b class="mx-2">' + order_array.join(',') + '</b>期');
+            $('#show_order').show();
+            $('#s_order').val(order_array.join(','));
+        }
+    });
+
+    // 提交表单判断
+    $('#addMaterial').click(function () {
+        if ($('#s_order').val() == '') {
+            toastr.error('请选择计量期');
+            return false;
+        }
+    })
+});

+ 5 - 0
app/public/js/measure_stage.js

@@ -79,3 +79,8 @@ $('a[data-target="#sp-list" ]').on('click', function () {
        $('#auditor-list2').html(righthtml.join(''));
    })
 });
+
+function checkValidForm() {
+    $('#add-stage-btn').attr('disabled', true);
+    return true;
+}

+ 4 - 21
app/public/js/revise.js

@@ -11,23 +11,6 @@
 const ckBillsSpread = window.location.pathname + '-billsSelect';
 
 $(document).ready(() => {
-    toastr.options = {
-        "closeButton": false,
-        "debug": false,
-        "newestOnTop": false,
-        "progressBar": false,
-        "positionClass": "toast-top-center",
-        "preventDuplicates": false,
-        "onclick": null,
-        "showDuration": "300",
-        "hideDuration": "1000",
-        "timeOut": "5000",
-        "extendedTimeOut": "1000",
-        "showEasing": "swing",
-        "hideEasing": "linear",
-        "showMethod": "fadeIn",
-        "hideMethod": "fadeOut"
-    };
     autoFlashHeight();
     // 初始化spread
     const billsSpread = SpreadJsObj.createNewSpread($('#bills-spread')[0]);
@@ -142,7 +125,7 @@ $(document).ready(() => {
             setObjEnable($('#down-move'), valid && first && sameParent && first.level > 1 && !tree.isLastSibling(last));
             if (isTz) {
                 const posRange = last ? pos.getLedgerPos(last.id) : [];
-                setObjEnable($('#up-level'), valid && first && sameParent && tree.getParent(first) && first.level > 2 && (!posRange || posRange.length === 0));
+                setObjEnable($('#up-level'), valid && first && sameParent && tree.getParent(first) && first.level > 2 && ((!posRange || posRange.length === 0) || tree.isLastSibling(last)));
                 const preNodePosRange = preNode ? pos.getLedgerPos(preNode.id) : [];
                 setObjEnable($('#down-level'), valid && first && sameParent && first.level > 1 && preNode && (!preNodePosRange || preNodePosRange.length === 0));
             } else {
@@ -735,7 +718,7 @@ $(document).ready(() => {
             const sortData = info.sheet.zh_data || [];
             if (sortData.length === 0 || info.cellRange.row + info.cellRange.rowCount > sortData.length) {
                 if (info.cellRange.col !== 0) {
-                    toast('新增部位请先输入名称', 'warning');
+                    toastr.warning('新增部位请先输入名称');
                     posSpreadObj.loadCurPosData();
                     return;
                 }
@@ -839,8 +822,8 @@ $(document).ready(() => {
                     const mainNode = mainTree.nodes[sel.row];
                     if (!stdNode) return;
 
-                    if (stdType === 'bills') {
-                        if (!(mainNode.b_code && mainNode.b_code !== '') && !mainTree.isLeafXmj(mainNode)) {
+                    if (stdType === 'gcl') {
+                        if (mainNode.code && mainNode.code !== '' && !mainTree.isLeafXmj(mainNode)) {
                             toastr.warning('非最底层项目下,不应添加清单');
                             return;
                         }

+ 0 - 17
app/public/js/revise_history.js

@@ -10,23 +10,6 @@
 const ckBillsSpread = window.location.pathname + '-billsSelect';
 
 $(document).ready(() => {
-    toastr.options = {
-        "closeButton": false,
-        "debug": false,
-        "newestOnTop": false,
-        "progressBar": false,
-        "positionClass": "toast-top-center",
-        "preventDuplicates": false,
-        "onclick": null,
-        "showDuration": "300",
-        "hideDuration": "1000",
-        "timeOut": "5000",
-        "extendedTimeOut": "1000",
-        "showEasing": "swing",
-        "hideEasing": "linear",
-        "showMethod": "fadeIn",
-        "hideMethod": "fadeOut"
-    };
     autoFlashHeight();
     // 初始化spread
     const billsSpread = SpreadJsObj.createNewSpread($('#bills-spread')[0]);

+ 3 - 3
app/public/js/setting.js

@@ -79,11 +79,11 @@ $(document).ready(() => {
                         throw response.msg;
                     }
                     $("#reset-password").val('');
-                    toast('重置成功', 'success', 'check');
+                    toastr.success('重置成功');
                 }
             });
         } catch (error) {
-            toast(error, 'error', 'exclamation-circle');
+            toastr.error(error);
             console.log(error);
         }
     });
@@ -194,7 +194,7 @@ function checkUserForm(status) {
             }
         }
     } catch (err) {
-        toast(err, 'error', 'exclamation-circle');
+        toastr.error(err);
         return false;
     }
 }

+ 53 - 6
app/public/js/spreadjs_rela/spreadjs_zh.js

@@ -441,12 +441,13 @@ const SpreadJsObj = {
             sheet.getRange(-1, col, -1, 1).cellType(sheet.extendCellType.unit);
         }
         if (colSetting.cellType === 'customizeCombo') {
-            if (!sheet.extendCellType.unit) {
-                sheet.extendCellType.unit = this.CellType.getCustomizeComboCellType(colSetting.comboItems);
+            const cellKey = colSetting.cellTypeKey ? 'customizeCombo-' + colSetting.cellTypeKey : 'customizeCombo';
+            if (!sheet.extendCellType[cellKey]) {
+                sheet.extendCellType[cellKey] = this.CellType.getCustomizeComboCellType(colSetting.comboItems);
                 if (!sheet.AcitveComboRefresh) {
                     sheet.bind(spreadNS.Events.LeaveCell, function (e, info) {
                         const cellType = info.sheet.getCell(info.row, info.col).cellType();
-                        if (cellType === sheet.extendCellType.unit) {
+                        if (cellType === sheet.extendCellType[cellKey]) {
                             info.sheet.leaveCell = {row: info.row, col: info.col};
                         } else {
                             delete info.sheet.leaveCell;
@@ -460,7 +461,7 @@ const SpreadJsObj = {
                     sheet.AcitveComboRefresh = true;
                 }
             }
-            sheet.getRange(-1, col, -1, 1).cellType(sheet.extendCellType.unit);
+            sheet.getRange(-1, col, -1, 1).cellType(sheet.extendCellType[cellKey]);
         }
         if (colSetting.formatter) {
             sheet.getRange(-1, col, -1, 1).formatter(colSetting.formatter);
@@ -659,6 +660,21 @@ const SpreadJsObj = {
         }
         return copyData.join('\n');
     },
+    analysisPasteText: function (text) {
+        const result = [];
+        if (text === '') return result;
+
+        const rows = text.split('\n');
+        for (const r of rows) {
+            const cols = r.split('\t');
+            result.push(cols);
+        }
+        return result;
+    },
+    analysisPasteHtml: function (html) {
+        const result = [];
+        if (!html || html === '') return result;
+    },
     /**
      * 树表结构,定位至指定的节点
      * @param {GC.Spread.Sheets.Worksheet} sheet - 需要定位的sheet
@@ -1609,5 +1625,36 @@ const SpreadJsObj = {
             combo.itemHeight(10).editorValueType(spreadNS.CellTypes.EditorValueType.value).items(items);
             return combo;
         },
-    }
-};
+    },
+
+    Clipboard: (function () {
+        let cText = '', cHtml;
+        const setCopyData = function (text, html) {
+            cText = text;
+            cHtml = html;
+        };
+        const getPasteData = function () {
+            return {text: cText, html: cHtml};
+        };
+        const clearData = function () {
+            cText = '';
+            cHtml = undefined;
+        };
+        const setSheetFilterCopyData = function (sheet) {
+            cText = SpreadJsObj.getFilterCopyText(sheet);
+            cHtml = SpreadJsObj.getFilterCopyHTML(sheet);
+        };
+        const getAnalysisPasteText = function () {
+            return SpreadJsObj.analysisPasteText(cText);
+        };
+        const getAnalysisPasteHtml = function () {
+            return SpreadJsObj.analysisPasteHtml(cHtml);
+        }
+        return {
+            setCopyData, getPasteData, clearData,
+            setSheetFilterCopyData,
+            getAnalysisPasteText, getAnalysisPasteHtml,
+        }
+    })(),
+};
+

+ 30 - 63
app/public/js/stage.js

@@ -32,7 +32,7 @@ function customColDisplay () {
     if (checkTzMeasureType()) {
         defaultSetting.splice(0, 1);
     }
-    const settingStr = Cookies.get('stage-col-visible-1.0.2-' + tender.id);
+    const settingStr = Cookies.get(ckColSetting);
     return settingStr ? JSON.parse(settingStr) : defaultSetting;
 }
 
@@ -124,24 +124,6 @@ function getNodeList(node) {
 }
 
 $(document).ready(() => {
-    // 提示窗
-    toastr.options = {
-        "closeButton": false,
-        "debug": false,
-        "newestOnTop": false,
-        "progressBar": false,
-        "positionClass": "toast-top-center",
-        "preventDuplicates": false,
-        "onclick": null,
-        "showDuration": "300",
-        "hideDuration": "1000",
-        "timeOut": "5000",
-        "extendedTimeOut": "1000",
-        "showEasing": "swing",
-        "hideEasing": "linear",
-        "showMethod": "fadeIn",
-        "hideMethod": "fadeOut"
-    };
     // 界面布局
     autoFlashHeight();
     // 初始化 台账树结构 数据结构
@@ -315,18 +297,9 @@ $(document).ready(() => {
                     if (c.uamount) {
                         const vamount = (!c.vamount || checkZero(c.vamount)) ? 0 : c.vamount;
                         if (c.uamount > vamount) {
-                            toast('变更令:' + c.code + ' 超计,请修改本期计量后,再提交', 'error');
+                            toastr.error('变更令:' + c.code + ' 超计,请修改本期计量后,再提交');
                             return;
                         }
-                        // if (!c.vamount || checkZero(c.vamount)) {
-                        //     toast('变更令:' + c.code + ' 当前不可使用', 'error');
-                        //     return;
-                        // } else {
-                        //     if (c.uamount > c.vamount) {
-                        //         toast('变更令:' + c.code + ' 超计,请修改本期计量后,再提交', 'error');
-                        //         return;
-                        //     }
-                        // }
                         data.change.push({ cid: c.cid, cbid: c.cbid, qty: c.uamount });
                     }
                 }
@@ -413,11 +386,10 @@ $(document).ready(() => {
             const sheet = this.spread.getActiveSheet();
             if (this.changes) {
                 sheet.setSelection(0, 0, 1, 1);
-                //SpreadJsObj.loadSheetData(sheet, SpreadJsObj.DataType.Data, this.changes);
                 this._filterChange(!$('#filterEmpty')[0].checked, $('#matchPos')[0].checked);
                 this._loadChangeDetail(this.changes[0]);
             } else {
-                toast('查询变更令有误,请刷新页面后重试', 'warning');
+                toastr.error('查询变更令有误,请刷新页面后重试');
             }
         }
         _filterChange(filterEmpty, matchPosName) {
@@ -428,10 +400,8 @@ $(document).ready(() => {
                 if ((filterVisible && matchVisible) || (c.org_uamount)) {
                     this.displayChanges.push(c);
                 }
-                //c.visible = filterVisible && matchVisible;
             }
             SpreadJsObj.loadSheetData(this.spread.getActiveSheet(), SpreadJsObj.DataType.Data, this.displayChanges);
-            //SpreadJsObj.refreshTreeRowVisible(this.spread.getActiveSheet());
         }
         loadChanges(data) {
             this.callData = data;
@@ -527,19 +497,19 @@ $(document).ready(() => {
                 }
                 if (col.field.indexOf('_dgn_') > 0) {
                     if (node.b_code && node.b_code !== '') {
-                        toast('仅项目节可输入设计数量');
+                        toastr.error('仅项目节可输入设计数量');
                         SpreadJsObj.reLoadRowData(info.sheet, info.row);
                         return;
                     }
                 } else if (col.field !== 'postil') {
                     if (node.children && node.children.length > 0) {
-                        toast('清单父项不可计量', 'error');
+                        toastr.error('清单父项不可计量');
                         SpreadJsObj.reLoadRowData(info.sheet, info.row);
                         return;
                     } else {
                         const nodePos = stagePos.getLedgerPos(node.id);
                         if (nodePos && nodePos.length > 0) {
-                            toast('该清单有部位明细,请在部位明细处计量', 'error');
+                            toastr.error('该清单有部位明细,请在部位明细处计量');
                             SpreadJsObj.reLoadRowData(info.sheet, info.row);
                             return;
                         }
@@ -638,7 +608,7 @@ $(document).ready(() => {
                 for (let iCol = range.col; iCol < range.col + range.colCount; iCol++) {
                     const col = info.sheet.zh_setting.cols[iCol];
                     if ((stageField.indexOf(col.field) === -1) && setting.dgnUpFields.indexOf(col.field) === -1) {
-                        toast('不可修改此数据', 'error');
+                        toastr.error('不可修改此数据');
                         info.cancel = true;
                         return;
                     }
@@ -698,10 +668,7 @@ $(document).ready(() => {
             }
         },
         measureAllPosInNode(node, ratio = 1) {
-            const posterity = stageTree.getPosterity(node);
-            console.log(node);
-            console.log(posterity.length);
-            console.log(posterity);
+            const posterity = stageTree.getPosterity(node)
             const data = {updateType: 'update', updateData: []};
             for (const p of posterity) {
                 if (p.children && p.children.length > 0) continue;
@@ -895,7 +862,7 @@ $(document).ready(() => {
                 }
                 // 台账模式下,不可新增
                 if (checkTzMeasureType() && !posData) {
-                    toast('台账模式不可新增部位明细数据', 'error');
+                    toastr.error('台账模式不可新增部位明细数据');
                     SpreadJsObj.reLoadRowData(info.sheet, info.row);
                     return ;
                 }
@@ -903,15 +870,15 @@ $(document).ready(() => {
                 //const node = SpreadJsObj.getSelectObject(slSpread.getActiveSheet());
                 const node = stagePosSpreadObj.stageTreeNode;
                 if (!node) {
-                    toast('数据错误, 请刷新页面后再试', 'warning');
+                    toastr.warning('数据错误, 请刷新页面后再试');
                     SpreadJsObj.reLoadRowData(info.sheet, info.row);
                     return;
                 } else if (info.editingText !== '' && node.children && node.children > 0) {
-                    toast('父节点不可插入部位明细', 'error');
+                    toastr.error('父节点不可插入部位明细');
                     SpreadJsObj.reLoadRowData(info.sheet, info.row);
                     return;
                 } else if (info.editingText !== '' && !node.b_code || node.b_code === '') {
-                    toast('项目节不可插入部位明细', 'error');
+                    toastr.error('项目节不可插入部位明细');
                     SpreadJsObj.reLoadRowData(info.sheet, info.row);
                     return;
                 }
@@ -919,7 +886,7 @@ $(document).ready(() => {
                 const data = {};
                 if (col.field === 'name') {
                     if ((!info.editingText || info.editingText === '') && posData) {
-                        toast('部位名称不可为空', 'error', 'exclamation-circle');
+                        toastr.error('部位名称不可为空');
                         SpreadJsObj.reLoadRowData(info.sheet, info.row);
                         return;
                     } else if (!posData) {
@@ -936,7 +903,7 @@ $(document).ready(() => {
                         data.updateData = {pid: posData.id, lid: posData.lid, name: info.editingText};
                     }
                 } else if (!posData) {
-                    toast('新增部位请先输入名称', 'warning');
+                    toastr.warning('新增部位请先输入名称');
                 } else {
                     data.updateType = 'update';
                     data.updateData = {pid: posData.id, lid: posData.lid};
@@ -972,14 +939,14 @@ $(document).ready(() => {
                     const col = info.sheet.zh_setting.cols[iCol];
                     if (validField.indexOf(col.field) === -1) {
                         if (checkTzMeasureType()) {
-                            toast('不可修改此数据', 'error');
+                            toastr.error('不可修改此数据');
                             info.cancel = true;
                             return;
                         } else {
                             for (let iRow = range.row; iRow < range.row + range.rowCount; iRow) {
                                 const pos = sortData(iRow);
                                 if (pos.add_stage !== stage.id || pos.add_times !== stage.times) {
-                                    toast('不可修改此数据', 'error');
+                                    toastr.error('不可修改此数据');
                                     info.cancel = true;
                                     return;
                                 }
@@ -998,7 +965,7 @@ $(document).ready(() => {
                 if (sortData && (info.cellRange.row >= sortData.length)) {
                     data.updateType = 'add';
                     if (info.cellRange.col !== 0) {
-                        toast('新增部位请先输入名称', 'warning');
+                        toastr.warning('新增部位请先输入名称');
                         self.loadCurPosData();
                         return;
                     }
@@ -1071,7 +1038,7 @@ $(document).ready(() => {
                         for (const iCol of validCols) {
                             const colSetting = sheet.zh_setting.cols[iCol];
                             if (colSetting.field === 'name') {
-                                toast('部位名称不能为空', 'error');
+                                toastr.error('部位名称不能为空');
                                 return;
                             }
                             data[colSetting.field] = null;
@@ -1245,7 +1212,7 @@ $(document).ready(() => {
         }
         customizeStageTreeSetting(ledgerSpreadSetting, customDisplay);
         SpreadJsObj.refreshColumnVisible(slSpread.getActiveSheet());
-        Cookies.set('stage-col-visible-1.0.1-' + tender.id, JSON.stringify(customDisplay), 30*24*60*60*1000);
+        Cookies.set(ckColSetting, JSON.stringify(customDisplay), 30*24*60*60*1000);
         $('#row-view').modal('hide');
     });
 
@@ -1323,13 +1290,13 @@ $(document).ready(() => {
             for (const node of sortData) {
                 if (node.children && node.children.length > 0) continue;
                 if (node.end_gather_qty) {
-                    if (!node.quantity || Math.abs(node.end_gather_qty) > Math.abs(node.quantity)) {
+                    if (!node.quantity || Math.abs(node.end_gather_qty) > Math.abs(ZhCalc.add(node.quantity, node.end_qc_qty))) {
                         const data = JSON.parse(JSON.stringify(node));
                         data.visible = true;
                         this.searchResult.push(data);
                     }
                 } else if (node.end_gather_tp) {
-                    if (!node.total_price || Math.abs(node.end_gather_tp) > Math.abs(node.total_price)) {
+                    if (!node.total_price || Math.abs(node.end_gather_tp) > Math.abs(ZhCalc.add(node.total_price, node.end_qc_tp))) {
                         const data = JSON.parse(JSON.stringify(node));
                         data.visible = true;
                         this.searchResult.push(data);
@@ -1401,7 +1368,7 @@ $(document).ready(() => {
                         let match = false;
                         if (checkOver) {
                             if (sd.end_gather_qty) {
-                                if (!sd.quantity || Math.abs(sd.end_gather_qty) > Math.abs(sd.quantity)) match = true;
+                                if (!sd.quantity || Math.abs(sd.end_gather_qty) > Math.abs(ZhCalc.add(sd.quantity, sd.end_qc_qty))) match = true;
                             }
                         }
                         if (checkEmpty) {
@@ -1598,17 +1565,17 @@ $(document).ready(() => {
         formData.append('lid', node.id);
         for (const file of files) {
             if (file === undefined) {
-                toast('未选择上传文件!', 'error');
+                toastr.error('未选择上传文件!');
                 return false;
             }
             const filesize = file.size;
             if (filesize > 10 * 1024 * 1024) {
-                toast('存在上传文件大小过大!', 'error');
+                toastr.error('存在上传文件大小过大!');
                 return false;
             }
             const fileext = '.' + file.name.toLowerCase().split('.').splice(-1)[0];
             if (whiteList.indexOf(fileext) === -1) {
-                toast('只能上传指定格式的附件!', 'error');
+                toastr.error('只能上传指定格式的附件!');
                 return false;
             }
             formData.append('size', filesize);
@@ -1622,7 +1589,7 @@ $(document).ready(() => {
             getAllList();
             getNodeList(node.id);
         }, function () {
-            toast('附件上传失败', 'error');
+            toastr.error('附件上传失败');
         });
         $('#upload-file').val('');
     });
@@ -1664,7 +1631,7 @@ $(document).ready(() => {
         } else {
             $('#showAttachment').hide();
             $('#showAttachment').attr('file-id', '');
-            toast('附件信息获取失败', 'error');
+            toastr.error('附件信息获取失败');
         }
     });
     $('body').on('click', '#btn-att a', function () {
@@ -1729,7 +1696,7 @@ $(document).ready(() => {
                 $('#btn-att a').eq(2).hide();
                 $('#btn-att a').eq(3).hide();
             }, function () {
-                toast('附件上传失败', 'error');
+                toastr.error('附件上传失败');
             });
             $('#change-att-btn').val('');
         } else if (content === 'del') {
@@ -1757,12 +1724,12 @@ $(document).ready(() => {
         const fileext = name.substr(name.indexOf("."));
         const filesize = file.size;
         if (filesize > 10 * 1024 * 1024) {
-            toast('文件大小过大!', 'error');
+            toastr.error('文件大小过大!');
             $('#change-att-btn').val('');
             return false;
         }
         if (whiteList.indexOf(fileext) === -1) {
-            toast('只能上传指定格式的附件!', 'error');
+            toastr.error('只能上传指定格式的附件!');
             $('#change-att-btn').val('');
             return false;
         }

+ 2 - 2
app/public/js/stage_audit.js

@@ -29,7 +29,7 @@ $(document).ready(function () {
                 $('p', resultDiv).text(data.company);
                 resultDiv.show();
             } else {
-                toast('未查询到该审核人', 'info');
+                toastr.info('未查询到该审核人');
                 resultDiv.hide();
             }
         }, () => {
@@ -145,7 +145,7 @@ $(document).ready(function () {
 // 检查上报情况
 function checkAuditorFrom () {
     if ($('#auditors li').length === 0) {
-        toast('请先选择审批人,再上报数据', 'error', 'exclamation-circle');
+        toast.error('请先选择审批人,再上报数据');
         return false;
     }
 }

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

@@ -311,7 +311,7 @@ $(document).ready(() => {
     // 本期已用变更
     $('#used-change').click(function () {
         if (usedChangesId.length === 0 && this.checked) {
-            toast('本期无已用变更令', 'hint');
+            toastr.info('本期无已用变更令');
             this.checked = false;
         }
         changeSpreadObj.filterUsedChange(this.checked);

+ 8 - 3
app/public/js/stage_compare.js

@@ -64,7 +64,8 @@ $(document).ready(function () {
             const field = data.quantity ? 'gather_qty' : 'gather_tp';
             const base = data[field + fieldSufs[0]];
             for (let i = 1; i< fieldSufs.length; i++) {
-                if (data[field + fieldSufs[i]] !== base) return true;
+                const compare = data[field + fieldSufs[i]];
+                if ((base || compare) && (compare !== base)) return true;
             }
         }
         return data.children.length === 0 && checkDiffer(data) ? '#F2DEDE' : defaultColor;
@@ -75,7 +76,8 @@ $(document).ready(function () {
             if (fieldSufs.length <= 1) return false;
             const base = data['gather_qty' + fieldSufs[0]];
             for (let i = 1; i< fieldSufs.length; i++) {
-                if (data['gather_qty' + fieldSufs[i]] !== base) return true;
+                const compare = data['gather_qty' + fieldSufs[i]];
+                if ((base || compare) && (compare!== base)) return true;
             }
         }
         return checkDiffer(data) ? '#F2DEDE' : defaultColor;
@@ -139,7 +141,10 @@ $(document).ready(function () {
 
     postData(window.location.pathname + '/load', {main: true, roles: scRoles}, function (result) {
         for (const r of scRoles) {
-            if (r != 0) $('input[audit-order=' + r + ']')[0].checked = true;
+            if (r && r != 0) {
+                const check = $('input[audit-order=' + r + ']')[0];
+                if (check) check.checked = true;
+            }
         }
         scTree.loadDatas(result.main.ledger);
         scPos.loadDatas(result.main.pos);

+ 2 - 2
app/public/js/stage_detail.js

@@ -176,7 +176,7 @@ $(document).ready(() => {
             if (info.sheet.zh_setting && info.sheet.zh_data) {
                 const col = info.sheet.zh_setting.cols[info.cellRange.col];
                 if (info.cellRange.colCount > 1) {
-                    toast('请勿同时复制粘贴多列数据', 'warning');
+                    toastr.warning('请勿同时复制粘贴多列数据');
                     SpreadJsObj.reLoadSheetData(paySpread.getActiveSheet());
                     return;
                 }
@@ -684,7 +684,7 @@ $(document).ready(() => {
         const ext = file.name.toLowerCase().split('.').splice(-1)[0];
         const imgStr = /(jpg|jpeg|png|bmp|BMP|JPG|PNG|JPEG)$/;
         if (!imgStr.test(ext)) {
-            toast('请上传正确的图片格式文件', 'error');
+            toastr.error('请上传正确的图片格式文件');
             return
         }
         if ($(this).val()) {

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

@@ -334,7 +334,7 @@ const stageIm = (function () {
             }
             let imData = _.find(ImData, {lid: d.lid, uuid: d.uuid});
             if (!imData) {
-                imData = _.find(ImData, {lid: d.lid, code: d.code, name: d.name, unit: d.unit});
+                imData = _.find(ImData, {lid: d.lid, code: d.code, name: d.name, unit: d.unit, unit_price: d.unit_price});
             }
             if (imData) {
                 _.assignInWith(imData, d, function (oV, sV, key) {

+ 408 - 111
app/public/js/stage_pay.js

@@ -29,11 +29,14 @@ function loadUpdateDealPays(newPay, fields) {
     }
 }
 
-function makeAttTable(id, attachment) {
+function makeAttTable(payNode) {
+    const id = payNode.id, attachment = payNode.attachment;
     let html = '';
     if (attachment !== null) {
         for (const [index, att] of attachment.entries()) {
-            const delhtml = uploadPermission && parseInt(att.uid) === parseInt(userID) ? '<a class="delete-att text-danger" href="javascript:void(0);" data-payid="'+ id +'" data-attindex="'+ index +'" title="删除"><i class="fa fa-remove "></i></a>' : '';
+            const delhtml = !readOnly && uploadPermission && (parseInt(att.uid) === userID || payNode.uid === userID || (payNode.uid === -1 && userID === stage.user_id))
+                ? '<a class="delete-att text-danger" href="javascript:void(0);" data-payid="'+ id +'" data-attindex="'+ index +'" title="删除"><i class="fa fa-remove "></i></a>'
+                : '';
             html += '<tr><td style="width: 200px">' + att.filename + att.fileext + '</td><td>' + att.username + '</td><td>' + att.in_time + '</td>' +
                 '<td><a href="/tender/'+ tender.id + '/measure/stage/' + tender.ledger_times +'/pay/download/file/'+ id +'/'+ index +'" title="下载"><i class="fa fa-download "></i></a> ' +
                 delhtml +
@@ -45,8 +48,44 @@ function makeAttTable(id, attachment) {
 
 $(document).ready(() => {
     autoFlashHeight();
+    const payCalc = (function (b) {
+        class PayCalc {
+            constructor (bases) {
+                this.percentReg = /[0-9]+%/g;
+                this.bases = bases;
+                this.bases.sort(function (a, b) {
+                    return a.sort - b.sort;
+                });
+                for (const b of this.bases) {
+                    b.reg = new RegExp(b.code, 'igm');
+                }
+            }
 
+            calculateExpr(expr) {
+                let formula = expr;
+                for (const b of this.bases) {
+                    formula = formula.replace(b.reg, b.value);
+                }
+                const percent = formula.match(this.percentReg);
+                if (percent) {
+                    for (const p of percent) {
+                        const v = math.eval(p.replace('%', '/100'));
+                        formula = formula.replace(p, v);
+                    }
+                }
+                try {
+                    const value = math.eval(formula);
+                    return value;
+                } catch(err) {
+                    return 0;
+                }
+            }
+        }
+
+        return new PayCalc(b);
+    })(calcBase);
     const paySpread = SpreadJsObj.createNewSpread($('#pay-spread')[0]);
+    const wcPay = dealPay.find(function (x) {return x.ptype === 4});
 
     $.subMenu({
         menu: '#sub-menu', miniMenu: '#sub-mini-menu', miniMenuList: '#mini-menu-list',
@@ -88,7 +127,7 @@ $(document).ready(() => {
         pos: SpreadJsObj.getObjPos($('#pay-spread')[0]),
     };
     paySpreadSetting.imageClick = function (data) {
-        makeAttTable(data.id, data.attachment);
+        makeAttTable(data);
         $('#file').modal('show');
     };
     paySpreadSetting.getColor = function (sheet, data, col, defaultColor) {
@@ -106,6 +145,42 @@ $(document).ready(() => {
             return defaultColor;
         }
     };
+    const payBase = {
+        isNonZero: function (num) {
+            return !(!num || num === 0)
+        },
+        isYF: function (data) {
+            return data.ptype === 2;
+        },
+        isWC: function (data) {
+            return data.ptype === 4;
+        },
+        isSF: function (data) {
+            return data.ptype === 3;
+        },
+        isSpecial: function (data) {
+            return data.ptype !== 1;
+        },
+        isOld: function (data) {
+            if (data.csorder === 0 && payBase.isYB) {
+                return stage.order > 1;
+            } else {
+                return stage.order > data.csorder;
+            }
+        },
+        isYB: function () {
+            return userID === stage.user_id;
+        },
+        isOwner: function (data) {
+            return data.uid === 0 ? payCol.isYB() : data.uid === userID;
+        },
+        isStarted: function (data) {
+            return data.pre_used;
+        },
+        isFinish: function (data) {
+            return data.pre_finish;
+        }
+    };
     const payCol = {
         getValue: {
             attachment: function (data) {
@@ -123,79 +198,243 @@ $(document).ready(() => {
             },
         },
         readOnly: {
-            isNonZero: function (num) {
-                return !(!num || num === 0)
-            },
-            isYF: function (data) {
-                return data.ptype === 2;
-            },
-            isSpecial: function (data) {
-                return data.ptype !== 1;
-            },
-            isOld: function (data) {
-                if (data.csorder === 0 && payCol.readOnly.isYB(data)) {
-                    return stage.order > 1 || stage.times > 1 || stage.curOrder > 0;
-                } else {
-                    return stage.order > data.csorder || stage.times > data.cstimes || stage.curOrder > data.csaorder;
-                }
-            },
-            isYB: function (data) {
-                return (stage.status === 1 || stage.status === 4);
-            },
-            isStarted: function (data) {
-                return ((payCol.readOnly.isNonZero(data.end_tp) || payCol.readOnly.isNonZero(data.tp)) && data.tp !== data.end_tp) || payCol.readOnly.isNonZero(data.pre_tp) || data.pre_used;
-            },
             name: function (data) {
-                return payCol.readOnly.isSpecial(data); //所有人,轮到自己时,均可修改
+                return payBase.isSpecial(data);
             },
             minus: function (data) {
-                if (payCol.readOnly.isOld(data)) { // 上报或审批后,仅原报,在未开始计量前,可修改
-                    return payCol.readOnly.isSpecial(data) || payCol.readOnly.isStarted(data) || !payCol.readOnly.isYB(data);
-                } else { // 新增时,新增人可修改
-                    return payCol.readOnly.isSpecial(data);
+                if (payBase.isOld(data)) {
+                    return payBase.isSpecial(data) || payBase.isStarted(data) || !payBase.isYB();
+                } else {
+                    return !(payBase.isOwner(data) || payBase.isYB(data));
                 }
             },
             tp: function (data) {
-                return data.ptype === 2 || data.ptype === 4; // 仅本期完成计量、本期应付,不可修改
+                return payBase.isWC(data) || payBase.isYF(data);
             },
             sprice: function (data) {
-                if (payCol.readOnly.isOld(data)) { // 上报或审批后,仅原报,在未开始计量前,可修改
-                    return payCol.readOnly.isYF(data) || payCol.readOnly.isStarted(data) || !payCol.readOnly.isYB(data);
-                } else { // 新增时,新增人可修改
-                    return payCol.readOnly.isSpecial(data);
+                if (payBase.isOld(data)) {
+                    return payBase.isStarted(data) || !payBase.isYB(data);
+                } else {
+                    return payBase.isWC(data) || payBase.isSF(data) || !(payBase.isOwner(data) || payBase.isYB());
                 }
             },
             rprice: function (data) {
-                return payCol.readOnly.sprice(data); // 同起扣金额
+                if (payBase.isOld(data)) {
+                    return !payBase.isYB(data);
+                } else {
+                    return payBase.isWC(data) || payBase.isSF(data) && !(payBase.isOwner(data) || payBase.isYB());
+                }
             },
+        },
+        menuVisible: {
             pause: function (data) {
-                if (payCol.readOnly.isOld(data)) { // 上报或审批后,仅原报,可修改
-                    return payCol.readOnly.isSpecial(data) || !payCol.readOnly.isYB(data);
-                } else { // 新增时,新增人可修改
-                    return payCol.readOnly.isSpecial(data);
+                if (payBase.isOld(data)) {
+                    return payBase.isYB(data);
+                } else {
+                    return payBase.isOwner(data) || payBase.isYB();
+                }
+            },
+            deadline: function (data) {
+                if (payBase.isOld(data)) {
+                    return !payBase.isFinish(data) && payBase.isYB();
+                } else {
+                    return payBase.isOwner(data) || payBase.isYB();
+                }
+            },
+            belongYF: function (data) {
+                if (payBase.isOld(data)) {
+                    return !payBase.isStarted(data) && payBase.isYB(data);
+                } else {
+                    return payBase.isOwner(data) || payBase.isYB(data);
                 }
             }
-        }
+        },
     };
     SpreadJsObj.initSpreadSettingEvents(paySpreadSetting, payCol);
     SpreadJsObj.initSheet(paySpread.getActiveSheet(), paySpreadSetting);
     SpreadJsObj.loadSheetData(paySpread.getActiveSheet(), SpreadJsObj.DataType.Data, dealPay);
 
     const paySpreadObj = {
-        _checkExpr: function (text, data, priceField, exprField) {
+        _checkExprValid(expr, invalidParam) {
+            if (!expr) return [true, null];
+            const param = [];
+            let num = '', base = '';
+            for (let i = 0, iLen = expr.length; i < iLen; i++) {
+                if (/^[\d\.%]+/.test(expr[i])) {
+                    if (base !== '') {
+                        param.push({type: 'base', value: base});
+                        base = '';
+                    }
+                    num = num + expr[i];
+                } else if (/^[a-z]/.test(expr[i])) {
+                    if (num !== '') {
+                        param.push({type: 'num', value: num});
+                        base = '';
+                    }
+                    base = base + expr[i];
+                } else if (expr[i] === '(') {
+                    if (num !== '') {
+                        param.push({type: 'num', value: num});
+                        base = '';
+                    }
+                    if (base !== '') {
+                        param.push({type: 'base', value: base});
+                        base = '';
+                    }
+                    param.push({type: 'left', value: '('});
+                } else if (expr[i] === ')') {
+                    if (num !== '') {
+                        param.push({type: 'num', value: num});
+                        base = '';
+                    }
+                    if (base !== '') {
+                        param.push({type: 'base', value: base});
+                        base = '';
+                    }
+                    param.push({type: 'right', value: ')'});
+                } else if (/^[\+\-*\/]/.test(expr[i])) {
+                    if (num !== '') {
+                        param.push({type: 'num', value: num});
+                        base = '';
+                    }
+                    if (base !== '') {
+                        param.push({type: 'base', value: base});
+                        base = '';
+                    }
+                    param.push({type: 'calc', value: expr[i]});
+                } else {
+                    return [false, '输入的表达式含有非法字符: ' + expr[i]];
+                }
+            }
+            if (num !== '') {
+                param.push({type: 'num', value: num});
+                base = '';
+            }
+            if (base !== '') {
+                param.push({type: 'base', value: base});
+                base = '';
+            }
+            if (param.length === 0) return true;
+            if (param.length > 1) {
+                if (param[0].value === '-') {
+                    param[1].value = '-' + param[1];
+                }
+                param.unshift();
+            }
+            const iLen = param.length;
+            let iLeftCount = 0, iRightCount = 0;
+            for (const [i, p] of param.entries()) {
+                if (p.type === 'calc') {
+                    if (i === 0 || i === iLen - 1)
+                        return [false, '输入的表达式非法:计算符号' + p.value + '前后应有数字或计算基数'];
+                }
+                if (p.type === 'num') {
+                    num = p.value.replace('%', '');
+                    if (p.value.length - num.length > 1)
+                        return [false, '输入的表达式非法:' + p.value + '不是一个有效的数字'];
+                    num = _.toNumber(num);
+                    if (num === undefined || num === null || _.isNaN(num))
+                        return [false, '输入的表达式非法:' + p.value + '不是一个有效的数字'];
+                    if (i > 0) {
+                        if (param[i - 1].type !== 'calc') {
+                            return [false, '输入的表达式非法:' + p.value + '前应有运算符'];
+                        } else if (param[i - 1].value === '/' && num === 0) {
+                            return [false, '输入的表达式非法:请勿除0'];
+                        }
+                    }
+                }
+                if (p.type === 'base') {
+                    const baseParam = _.find(calcBase, {code: p.value});
+                    if (!baseParam)
+                        return [false, '输入的表达式非法:不存在计算基数' + p.value];
+                    if (invalidParam && invalidParam.indexOf(p.value) >= 0)
+                        return [false, '不可使用计算基数' + p.value];
+                    if (i > 0 && param[i - 1].type === 'calc')
+                        return [false, '输入的表达式非法:' + p.value + '前应有运算符'];
+                }
+                if (p.type === 'left') {
+                    iLeftCount += 1;
+                    if (i !== 0 && param[i-1].type !== 'calc')
+                        return [false, '输入的表达式非法:(前应有运算符'];
+                }
+                if (p.type === 'right') {
+                    iRightCount += 1;
+                    if (i !== iLen - 1 && param[i+1].type !== 'calc')
+                        return [false, '输入的表达式非法:)后应有运算符'];
+                    if (iRightCount > iLeftCount)
+                        return [false, '输入的表达式非法:")"前无对应的"("'];
+                }
+            }
+            if (iLeftCount > iRightCount)
+                return [false, '输入的表达式非法:"("后无对应的")"'];
+            return [true, ''];
+        },
+        _checkSExpr: function (payNode, text, data) {
+            if (!payNode) return [false, '数据错误'];
+
+            const num = text ? _.toNumber(text) : null;
+            let expr = text ? (num ? null : text) : null;
+            expr = expr ? expr.replace('=', '').toLowerCase(): null;
+            const [valid, msg] = this._checkExprValid(expr, ['bqwc', 'ybbqwc']);
+            if (!valid) return [valid, msg];
+
+            if (payBase.isStarted(payNode)) {
+                return [false, '已经开始计量,请勿修改起扣金额'];
+            } else {
+                if (stage.order > 1) {
+                    const value = expr ? payCalc.calculateExpr(expr) : num;
+                    if (wcPay.pre_tp && value < wcPay.pre_tp)
+                        return [false, '已进行到第' + stage.order + '期,起扣金额请勿少于本期完成截止上期计量金额' + wcPay.pre_tp];
+                    data.sprice = num;
+                    data.sexpr = expr;
+                    return [true, ''];
+                } else {
+                    data.sprice = num;
+                    data.sexpr = expr;
+                    return [true, ''];
+                }
+            }
+        },
+        _checkRExpr: function (payNode, text, data) {
+            if (!payNode) return [false, '数据错误'];
+
+            const num = text ? _.toNumber(text) : null;
+            let expr = text ? (num ? null : text) : null;
+            expr = expr ? expr.replace('=', '').toLowerCase(): null;
+            const [valid, msg] = this._checkExprValid(expr, ['bqwc', 'ybbqwc']);
+            if (!valid) return [valid, msg];
+
+            if (payBase.isStarted(payNode)) {
+                if (payNode.pre_finish) return [false, '已达扣款限额,请勿修改'];
+                const value = expr ? payCalc.calculateExpr(expr) : num;
+                if (payNode.pre_tp && value < payNode.pre_tp) return [false, '截止上期已计量' + payNode.pre_tp + ',扣款限额请勿少于改值'];
+                data.rprice = num;
+                data.rexpr = expr;
+                return [true, ''];
+            } else {
+                data.rprice = num;
+                data.rexpr = expr;
+                return [true, ''];
+            }
+        },
+        _checkExpr: function (text, data) {
             if (text) {
                 const num = _.toNumber(text);
                 if (num) {
-                    data[priceField] = num;
-                    data[exprField] = null;
+                    data.tp = num;
+                    data.expr = null;
                 } else {
-                    data[exprField] = text.replace('=', '');
-                    data[priceField] = null;
+                    const expr = text.replace('=', '').toLowerCase();
+                    const [valid, msg] = this._checkExprValid(expr);
+                    if (!valid) return [valid, msg];
+                    data.expr = expr;
+                    data.tp = null;
                 }
             } else {
-                data[priceField] = null;
-                data[exprField] = null;
+                data.tp = null;
+                data.expr = null;
             }
+            return [true, ''];
         },
         refreshActn: function () {
             const setObjEnable = function (obj, enable) {
@@ -208,10 +447,12 @@ $(document).ready(() => {
             const sheet = paySpread.getActiveSheet();
             const select = SpreadJsObj.getSelectObject(sheet);
             setObjEnable($('#add'), !readOnly);
-            setObjEnable($('#del'), !readOnly && select && (!payCol.readOnly.isOld(select) ||
-                (!payCol.readOnly.isStarted(select) && payCol.readOnly.isYB(select))));
-            setObjEnable($('#up-move'), !readOnly && select && select.ptype === 1 && dealPay.indexOf(select) > 3);
-            setObjEnable($('#down-move'), !readOnly && select && select.ptype === 1 && dealPay.indexOf(select) < dealPay.length - 1);
+            const delValid = payBase.isOld(select)
+                ? !payBase.isStarted(select) && payBase.isYB()
+                : payBase.isYB() || payBase.isOwner(select);
+            setObjEnable($('#del'), !readOnly && select && delValid);
+            setObjEnable($('#up-move'), !readOnly && select && !payBase.isSpecial(select) && dealPay.indexOf(select) > 3);
+            setObjEnable($('#down-move'), !readOnly && select && !payBase.isSpecial(select) && dealPay.indexOf(select) < dealPay.length - 1);
         },
         add: function () {
             const sheet = paySpread.getActiveSheet();
@@ -228,11 +469,11 @@ $(document).ready(() => {
         del: function () {
             const sheet = paySpread.getActiveSheet();
             const select = SpreadJsObj.getSelectObject(sheet);
-            if (payCol.readOnly.isNonZero(select.tp)) {
+            if (payBase.isNonZero(select.tp)) {
                 toast('该支付(扣款)项存在数据,如需删除请先清除本期金额!');
                 return;
-            } else if (payCol.readOnly.isOld(select)) {
-                if (payCol.readOnly.isStarted(select)) {
+            } else if (payBase.isOld(select)) {
+                if (payBase.isStarted(select)) {
                     toast('该合同支付项往期已进行计算,不允许删除');
                     return;
                 }
@@ -295,13 +536,13 @@ $(document).ready(() => {
             const data = SpreadJsObj.getSelectObject(info.sheet);
             if (col.field === 'tp') {
                 $('#expr').val(data.expr).attr('field', 'expr').attr('org', data.expr)
-                    .attr('readOnly', readOnly|| payCol.readOnly.isSpecial(data));
+                    .attr('readOnly', readOnly|| payBase.isSpecial(data));
             } else if (col.field === 'sprice') {
                 $('#expr').val(data.sexpr).attr('field', 'sexpr').attr('org', data.sexpr)
-                    .attr('readOnly', readOnly|| payCol.readOnly.isSpecial(data) || payCol.readOnly.isOld(data));
+                    .attr('readOnly', readOnly|| payCol.readOnly.sprice(data));
             } else if (col.field === 'rprice') {
                 $('#expr').val(data.rexpr).attr('field', 'rexpr').attr('org', data.rexpr)
-                    .attr('readOnly', readOnly|| payCol.readOnly.isSpecial(data) || payCol.readOnly.isOld(data));
+                    .attr('readOnly', readOnly|| payCol.readOnly.rprice(data));
             } else {
                 $('#expr').val('').attr('readOnly', true);
             }
@@ -317,11 +558,17 @@ $(document).ready(() => {
                 const validText = info.editingText ? info.editingText.replace('\n', '') : null;
                 let orgValue;
                 if (col.field === 'tp') {
-                    orgValue = _.toNumber(validText) ? select.tp : select.expr;
+                    orgValue = validText && validText !== ''
+                        ? _.toNumber(validText) ? select.tp : select.expr
+                        : (select.expr && select.expr !== '') ? select.expr : select.tp;
                 } else if (col.field === 'sprice') {
-                    orgValue = _.toNumber(validText) ? select.sprice : select.sexpr;
+                    orgValue = validText && validText !== ''
+                        ? _.toNumber(validText) ? select.sprice : select.sexpr
+                        : (select.sexpr && select.sexpr !== '') ? select.sexpr : select.sprice;
                 } else if (col.field === 'rprice') {
-                    orgValue = _.toNumber(validText) ? select.rexpr : select.rexpr;
+                    orgValue = validText && validText !== ''
+                        ? _.toNumber(validText) ? select.rprice : select.rexpr
+                        : (select.rexpr && select.rexpr !== '') ? select.rexpr : select.rprice;
                 } else {
                     orgValue = select[col.field];
                 }
@@ -337,7 +584,12 @@ $(document).ready(() => {
                 // 获取更新数据
                 if (col.field === 'tp') {
                     data.updateData.pid = select.pid;
-                    paySpreadObj._checkExpr(validText, data.updateData, 'tp', 'expr');
+                    const [valid, msg] = paySpreadObj._checkExpr(validText, data.updateData);
+                    if (!valid) {
+                        toastr.warning(msg);
+                        SpreadJsObj.reLoadRowData(info.sheet, info.row);
+                        return;
+                    }
                 } else if (col.field === 'name') {
                     data.updateData.pid = select.pid;
                     data.updateData.name = validText;
@@ -345,9 +597,19 @@ $(document).ready(() => {
                     data.updateData.id = select.pid;
                     if (validText) {
                         if (col.field === 'sprice') {
-                            paySpreadObj._checkExpr(validText, data.updateData, 'sprice', 'sexpr');
+                            const [valid, msg] = paySpreadObj._checkSExpr(select, validText, data.updateData);
+                            if (!valid) {
+                                toastr.warning(msg);
+                                SpreadJsObj.reLoadRowData(info.sheet, info.row);
+                                return;
+                            }
                         } else if (col.field === 'rprice') {
-                            paySpreadObj._checkExpr(validText, data.updateData, 'rprice', 'rexpr');
+                            const [valid, msg] = paySpreadObj._checkRExpr(select, validText, data.updateData);
+                            if (!valid) {
+                                toastr.warning(msg);
+                                SpreadJsObj.reLoadRowData(info.sheet, info.row);
+                                return;
+                            }
                         } else {
                             data.updateData[col.field] = validText;
                         }
@@ -359,6 +621,8 @@ $(document).ready(() => {
                 postData(window.location.pathname + '/save', data, function (result) {
                     loadUpdateDealPays(result, col.field === 'name' ? ['name'] : null);
                     SpreadJsObj.reLoadSheetData(paySpread.getActiveSheet());
+                }, function () {
+                    SpreadJsObj.reLoadRowData(info.sheet, info.row);
                 });
             }
         },
@@ -430,11 +694,17 @@ $(document).ready(() => {
                         } else {
                             updateData.id = node.pid;
                             if (col.field === 'sprice') {
-                                updateData.sprice = null;
-                                updateData.sexpr = null;
+                                const [valid, msg] = paySpreadObj._checkSExpr(node, null, updateData);
+                                if (!valid) {
+                                    toastr.warning(msg);
+                                    return;
+                                }
                             } else if (col.field === 'rprice') {
-                                updateData.rprice = null;
-                                updateData.rexpr = null;
+                                const [valid, msg] = paySpreadObj._checkRExpr(node, null, updateData);
+                                if (!valid) {
+                                    toastr.warning(msg);
+                                    return;
+                                }
                             } else {
                                 updateData[col.field] = null;
                             }
@@ -465,7 +735,10 @@ $(document).ready(() => {
                 }
 
                 const sortData = info.sheet.zh_data;
-                const data = {type: col.field === 'tp' ? 'stage' : 'info', updateData: []};
+                const data = {
+                    type: (col.field === 'tp' || col.field === 'name') ? 'stage' : 'info',
+                    updateData: []
+                };
 
                 for (let iRow = 0; iRow < info.cellRange.rowCount; iRow++) {
                     const curRow = info.cellRange.row + iRow;
@@ -475,14 +748,32 @@ $(document).ready(() => {
                         const updateData = {};
                         if (col.field === 'tp') {
                             updateData.pid = node.pid;
-                            paySpreadObj._checkExpr(validText, updateData, 'tp', 'expr');
+                            const [valid, msg] = paySpreadObj._checkExpr(validText, updateData);
+                            if (!valid) {
+                                toastr.warning(msg);
+                                SpreadJsObj.reLoadSheetData(paySpread.getActiveSheet());
+                                return;
+                            }
+                        } else if (col.field === 'name') {
+                            updateData.pid = node.pid;
+                            updateData.name = validText;
                         } else {
                             updateData.id = node.pid;
                             if (validText) {
                                 if (col.field === 'sprice') {
-                                    paySpreadObj._checkExpr(validText, updateData, 'sprice', 'sexpr');
+                                    const [valid, msg] = paySpreadObj._checkSExpr(node, validText, updateData);
+                                    if (!valid) {
+                                        toastr.warning(msg);
+                                        SpreadJsObj.reLoadSheetData(paySpread.getActiveSheet());
+                                        return;
+                                    }
                                 } else if (col.field === 'rprice') {
-                                    paySpreadObj._checkExpr(validText, updateData, 'rprice', 'rexpr');
+                                    const [valid, msg] = paySpreadObj._checkRExpr(node, validText, updateData);
+                                    if (!valid) {
+                                        toastr.warning(msg);
+                                        SpreadJsObj.reLoadSheetData(paySpread.getActiveSheet());
+                                        return;
+                                    }
                                 } else {
                                     updateData[col.field] = validText;
                                 }
@@ -520,7 +811,7 @@ $(document).ready(() => {
             const expr = $(this);
             const select = SpreadJsObj.getSelectObject(paySpread.getActiveSheet());
             const field = expr.attr('field'), orgValue = expr.attr('org'), newValue = expr.val();
-            if (orgValue === newValue) { return; }
+            if (orgValue === newValue || (!orgValue && newValue == '')) { return; }
 
             const data = {};
             if (field === 'expr') {
@@ -528,11 +819,22 @@ $(document).ready(() => {
                 data.updateData = { pid: select.pid, tp: null, expr: newValue }
             } else if (field === 'sexpr') {
                 data.type = 'info';
-                data.updateData = {id: select.pid, sprice: null, sexpr: newValue};
-
+                data.updateData = {id: select.pid};
+                const [valid, msg] = paySpreadObj._checkSExpr(select, newValue, data.updateData);
+                if (!valid) {
+                    toastr.warning(msg);
+                    this.value = select.sexpr;
+                    return;
+                }
             } else if (field === 'rexpr') {
                 data.type = 'info';
-                data.updateData = {id: select.pid, rprice: null, rexpr: newValue};
+                data.updateData = {id: select.pid};
+                const [valid, msg] = paySpreadObj._checkRExpr(select, newValue, data.updateData);
+                if (!valid) {
+                    toastr.warning(msg);
+                    this.value = select.rexpr;
+                    return;
+                }
             } else {
                 expr.val('');
                 return;
@@ -600,7 +902,6 @@ $(document).ready(() => {
             } else {
                 $('#dl-hint').show();
             }
-
         }
     };
     // 右键菜单
@@ -636,7 +937,7 @@ $(document).ready(() => {
                 },
                 visible: function(key, opt){
                     const select = SpreadJsObj.getSelectObject(paySpread.getActiveSheet());
-                    return select.ptype === 4;
+                    return payBase.isWC(select);
                 }
             },
             'start': {
@@ -659,11 +960,7 @@ $(document).ready(() => {
                 },
                 visible: function (key, opt) {
                     const select = SpreadJsObj.getSelectObject(paySpread.getActiveSheet());
-                    return select.ptype === 1 && select.pause;
-                },
-                disabled: function (key, opt) {
-                    const select = SpreadJsObj.getSelectObject(paySpread.getActiveSheet());
-                    return readOnly || payCol.readOnly.pause(select);
+                    return !payBase.isSpecial(select) && !readOnly && select.pause && payCol.menuVisible.pause(select);
                 }
             },
             'stop': {
@@ -686,33 +983,24 @@ $(document).ready(() => {
                 },
                 visible: function (key, opt) {
                     const select = SpreadJsObj.getSelectObject(paySpread.getActiveSheet());
-                    return select.ptype === 1 && !select.pause;
+                    return !payBase.isSpecial(select) && !readOnly && !payBase.pause && payCol.menuVisible.pause(select);
                 },
-                disabled: function (key, opt) {
-                    const select = SpreadJsObj.getSelectObject(paySpread.getActiveSheet());
-                    return readOnly || payCol.readOnly.pause(select);
-                }
             },
             'setDeadline': {
                 name: '设置计提期限',
                 icon: 'fa-clipboard',
-                visible: function (key, opt) {
-                    const select = SpreadJsObj.getSelectObject(paySpread.getActiveSheet());
-                    const stageId = getStageId();
-                    return select.ptype === 1 && (select.csorder == stageId || stageId == 1);
-                },
                 callback: function (key, opt) {
                     const select = SpreadJsObj.getSelectObject(paySpread.getActiveSheet());
                     deadlineObj.initView(select);
                     if (select.rprice) {
                         $('#deadline').modal('show');
                     } else {
-                        toast('计提期限用于达到条件时,即刻计量至付(扣)款限额,应先设置付(扣)款限额', 'warning');
+                        toastr.warning('计提期限用于达到条件时,即刻计量至付(扣)款限额,应先设置付(扣)款限额');
                     }
                 },
-                disabled: function (key, opt) {
+                visible: function (key, opt) {
                     const select = SpreadJsObj.getSelectObject(paySpread.getActiveSheet());
-                    return readOnly || payCol.readOnly.minus(select);
+                    return !payBase.isSpecial(select) && !readOnly && payCol.menuVisible.deadline(select);
                 }
             },
             'dropYF': {
@@ -720,8 +1008,7 @@ $(document).ready(() => {
                 icon: 'fa-chain-broken',
                 visible: function (key, opt) {
                     const select = SpreadJsObj.getSelectObject(paySpread.getActiveSheet());
-                    const stageId = getStageId();
-                    return select.ptype === 1 && select.is_yf && (select.csorder == stageId || stageId == 1);
+                    return !payBase.isSpecial(select) && select.is_yf && payCol.menuVisible.belongYF(select);
                 },
                 callback: function (key, opt) {
                     const select = SpreadJsObj.getSelectObject(paySpread.getActiveSheet());
@@ -737,10 +1024,6 @@ $(document).ready(() => {
                         loadUpdateDealPays(result);
                         SpreadJsObj.reLoadSheetData(paySpread.getActiveSheet());
                     });
-                },
-                disabled: function (key, opt) {
-                    const select = SpreadJsObj.getSelectObject(paySpread.getActiveSheet());
-                    return readOnly || payCol.readOnly.minus(select);
                 }
             },
             'belongYF': {
@@ -748,8 +1031,7 @@ $(document).ready(() => {
                 icon: 'fa-chain',
                 visible: function (key, opt) {
                     const select = SpreadJsObj.getSelectObject(paySpread.getActiveSheet());
-                    const stageId = getStageId();
-                    return select.ptype === 1 && !select.is_yf && (select.csorder == stageId || stageId == 1);
+                    return !payBase.isSpecial(select) && !select.is_yf && payCol.menuVisible.belongYF(select);
                 },
                 callback: function (key, opt) {
                     const select = SpreadJsObj.getSelectObject(paySpread.getActiveSheet());
@@ -760,15 +1042,10 @@ $(document).ready(() => {
                             is_yf: true
                         }
                     };
-                    // 更新至服务器
                     postData(window.location.pathname + '/save', data, function (result) {
                         loadUpdateDealPays(result);
                         SpreadJsObj.reLoadSheetData(paySpread.getActiveSheet());
                     });
-                },
-                disabled: function (key, opt) {
-                    const select = SpreadJsObj.getSelectObject(paySpread.getActiveSheet());
-                    return readOnly || payCol.readOnly.minus(select);
                 }
             }
         }
@@ -779,6 +1056,10 @@ $(document).ready(() => {
     $('#tp').change(deadlineObj.getTotalPriceHint);
     $('#deadline-ok').click(function () {
         const select = SpreadJsObj.getSelectObject(paySpread.getActiveSheet());
+        if (payBase.isFinish(select)) {
+            toastr.warning('已达扣款限额,请勿修改');
+            return;
+        }
         const data = {
             type: 'info',
             updateData: {
@@ -789,6 +1070,22 @@ $(document).ready(() => {
         data.updateData.dl_count = parseInt($('#stage-count').val());
         data.updateData.dl_tp_type = $('[name=tp-type]:checked').val();
         data.updateData.dl_tp = parseFloat($('#tp').val());
+        if (data.updateData.dl_type === 1) {
+            if (data.updateData.dl_count < stage.order) {
+                toastr.warning('已计量至第' + stage.order + '期,不可设置计提期限为第' + data.updateData.dl_count + '期');
+            }
+        } else {
+            if (data.updateData.dl_tp_type === 'contract' && preContractTp && data.updateData.dl_tp < preContractTp) {
+                toastr.warning('截止上期,合同已计量' + preContractTp + ',不可设置计提期限为累计合同计量达' + data.updateData.dl_tp);
+                return;
+            } else if (data.updateData.dl_tp_type === 'gather' && preGatherTp && data.updateData.dl_tp < preGatherTp) {
+                toastr.warning('截止上期,合同+变更已计量' + preGatherTp + ',不可设置计提期限为累计完成计量达' + data.updateData.dl_tp);
+                return;
+            } else if (data.updateData.dl_tp_type === 'qc' && preGatherTp && data.updateData.dl_tp < preQcTp) {
+                toastr.warning('截止上期,变更已计量' + preQcTp + ',不可设置计提期限为累计变更计量达' + data.updateData.dl_tp);
+                return;
+            }
+        }
         postData(window.location.pathname + '/save', data, function (result) {
             loadUpdateDealPays(result);
             SpreadJsObj.reLoadSheetData(paySpread.getActiveSheet());
@@ -829,7 +1126,7 @@ $(document).ready(() => {
             } else {
                 select.attachment = data.concat(select.attachment);
             }
-            makeAttTable(select.id, select.attachment);
+            makeAttTable(select);
             const index = dealPay.indexOf(select);
             dealPay.splice(index, 1, select);
             SpreadJsObj.reLoadSheetData(paySpread.getActiveSheet());
@@ -850,7 +1147,7 @@ $(document).ready(() => {
         const select = SpreadJsObj.getSelectObject(paySpread.getActiveSheet());
         postData('/tender/' + tender.id + '/measure/stage/' + tender.ledger_times + '/pay/delete/file', data, function (result) {
             select.attachment.splice(index, 1);
-            makeAttTable(id, select.attachment);
+            makeAttTable(select);
             const pay_index = dealPay.indexOf(select);
             dealPay.splice(pay_index, 1, select);
             SpreadJsObj.reLoadSheetData(paySpread.getActiveSheet());

+ 52 - 10
app/public/js/tender.js

@@ -162,13 +162,13 @@ $(document).ready(function() {
         spread.bind(spreadNS.Events.EditEnded, function (e, info) {
             const value = _.toNumber(info.editingText);
             if (!_.isInteger(value)) {
-                toast('请输入0-6的整数', 'warning');
+                toastr.warning('请输入0-6的整数');
                 sheet.setText(info.row, info.col, '0');
             } else if (value > 6) {
-                toast('请输入0-6的整数', 'warning');
+                toastr.warning('请输入0-6的整数');
                 sheet.setText(info.row, info.col, '6');
             } else if (value < 0) {
-                toast('请输入0-6的整数', 'warning');
+                toastr.warning('请输入0-6的整数');
                 sheet.setText(info.row, info.col, '0');
             }
         });
@@ -208,7 +208,7 @@ $(document).ready(function() {
                 }
             }
             if (bHint) {
-                toast('请输入0-6的整数', 'warning');
+                toastr.warning('请输入0-6的整数');
             }
         });
 
@@ -248,8 +248,21 @@ $(document).ready(function() {
             precision.other.value = _.toNumber(sheet.getText(13, 2));
             return precision;
         }
+        function checkPrecisionMinLimit(precision, limit) {
+            for (const unit of precision) {
+                if (precision[unit].value < limit[unit].value) {
+                    if (precision[unit].unit) {
+                        toastr.warning('台账已审批通过,清单精度不可减少,单位为' + limit[unit].unit + '的清单的精度不可小于' + limit[unit].value);
+                    } else {
+                        toastr.warning('台账已审批通过,清单精度不可减少,其他清单的精度不可小于' + limit[unit].value);
+                    }
+                    return false;
+                }
+            }
+            return true;
+        };
 
-        return {loadPrecisonProperty, setReadOnly, getNewPrecisionData, };
+        return {loadPrecisonProperty, setReadOnly, getNewPrecisionData, checkPrecisionMinLimit};
     })();
     // 合同参数
     const dealObj = (function () {
@@ -290,10 +303,10 @@ $(document).ready(function() {
         spread.bind(spreadNS.Events.EditEnded, function (e, info) {
             const value = _.toNumber(info.editingText);
             if (_.isNaN(value)) {
-                toast('请输入不超过万亿的数字', 'warning');
+                toastr.warning('请输入不超过万亿的数字');
                 info.sheet.setText(info.row, info.col, '0');
             } else if (value > Math.pow(10, 13)) {
-                toast('请输入不超过万亿的数字', 'warning');
+                toastr.warning('请输入不超过万亿的数字');
                 info.sheet.setText(info.row, info.col, '0');
             }
             if (info.row === 1 || info.row === 2) {
@@ -329,7 +342,7 @@ $(document).ready(function() {
                 }
             }
             if (bHint) {
-                toast('请输入不超过万亿的数字', 'warning');
+                toastr.warning('请输入不超过万亿的数字');
             }
             calcHtjMinusZlj();
         });
@@ -549,13 +562,41 @@ $(document).ready(function() {
                 payTp: _.toNumber($('#decimal-pay-tp').val()),
             }
         };
+        if (ledgerChecked) {
+            if (prop.decimal.up < property.decimal.up) {
+                toastr.warning('台账已审批完成,单价的小数位数,不可小于' + property.decimal.up);
+                return;
+            }
+            if (prop.decimal.tp < property.decimal.tp) {
+                toastr.warning('台账已审批完成,金额的小数位数,不可小于' + property.decimal.tp);
+                return;
+            }
+        }
+        if (firstStageChecked) {
+            if (property.decimal.pay) {
+                if (!prop.decimal.pay) {
+                    toastr.warning('第一期已审批完成,不可取消合同支付单独设置');
+                    return;
+                }
+                if (prop.decimal.payTp < property.decimal.payTp) {
+                    toastr.warning('第一期已审批完成,单独设置的合同支付小数位数,不可小于' + property.decimal.payTp);
+                    return;
+                }
+            } else {
+                if (prop.decimal.pay) {
+                    toastr.warning('第一期已审批完成,合同支付不可单独设置');
+                    return;
+                }
+            }
+        }
+        if (ledgerChecked && !precisionObj.checkPrecisionMinLimit(prop.decimal, property.decimal)) return;
         const tenderId = window.location.pathname.split('/')[2];
         postData('/tender/' + tenderId + '/save', prop, function (data) {
             setReadOnly('#v-pills-2', true);
             property.decimal = data.decimal;
             $('#post-2').parent().hide();
             $('#edit-2').parent().show();
-        });
+        }, null, true);
     });
 
     /**
@@ -577,13 +618,14 @@ $(document).ready(function() {
     // 提交
     $('#post-3').click(() => {
         const prop = { precision: precisionObj.getNewPrecisionData() };
+        if (ledgerChecked && !precisionObj.checkPrecisionMinLimit(prop.precision, property.precision)) return;
         const tenderId = window.location.pathname.split('/')[2];
         postData('/tender/' + tenderId + '/save', prop, function (data) {
             precisionObj.setReadOnly(true);
             property.precision = data.precision;
             $('#post-3').parent().hide();
             $('#edit-3').parent().show();
-        });
+        }, null, true);
     });
 
     /**

+ 17 - 1
app/router.js

@@ -12,6 +12,8 @@ module.exports = app => {
     const tenderCheck = app.middlewares.tenderCheck();
     // 期读取中间件
     const stageCheck = app.middlewares.stageCheck();
+    // 材料调差读取中间件
+    const materialCheck = app.middlewares.materialCheck();
 
     // 登入登出相关
     app.get('/login', 'loginController.index');
@@ -86,11 +88,12 @@ module.exports = app => {
 
     // 台账管理相关
     app.get('/tender/:id/ledger', sessionAuth, tenderCheck, 'ledgerController.explode');
+    app.post('/tender/:id/ledger/load', sessionAuth, tenderCheck, 'ledgerController.loadExplodeData');
     app.post('/tender/:id/ledger/get-children', sessionAuth, tenderCheck, 'ledgerController.getChildren');
     app.post('/tender/:id/ledger/update', sessionAuth, tenderCheck, 'ledgerController.update');
     app.post('/tender/:id/ledger/upload-excel', sessionAuth, tenderCheck, 'ledgerController.uploadExcel');
     app.get('/tender/:id/ledger/download/:file', sessionAuth, tenderCheck, 'ledgerController.download');
-    app.post('/tender/:id/pos', sessionAuth, tenderCheck, 'ledgerController.pos');
+    //app.post('/tender/:id/pos', sessionAuth, tenderCheck, 'ledgerController.pos');
     app.post('/tender/:id/pos/update', sessionAuth, tenderCheck, 'ledgerController.posUpdate');
     app.post('/tender/:id/pos/paste', sessionAuth, tenderCheck, 'ledgerController.posPaste');
     // 台账审批相关
@@ -208,6 +211,19 @@ module.exports = app => {
     // 变更单位管理
     app.post('/change/update/company', sessionAuth, 'changeController.updateCompany');
 
+    // 材料调差
+    app.get('/tender/:id/measure/material', sessionAuth, tenderCheck, 'materialController.index');
+    app.get('/tender/:id/measure/material/:order', sessionAuth, tenderCheck, materialCheck, 'materialController.info');
+    app.post('/tender/:id/measure/material/add', sessionAuth, tenderCheck, 'materialController.add');
+    app.post('/tender/:id/measure/material/delete', sessionAuth, tenderCheck, 'materialController.delete');
+    // 审批
+    app.post('/tender/:id/measure/material/:order/audit/add', sessionAuth, tenderCheck, materialCheck, 'materialController.addAudit');
+    app.post('/tender/:id/measure/material/:order/audit/delete', sessionAuth, tenderCheck, materialCheck, 'materialController.deleteAudit');
+    app.post('/tender/:id/measure/material/:order/audit/start', sessionAuth, tenderCheck, materialCheck, 'materialController.startAudit');
+    app.post('/tender/:id/measure/material/:order/audit/check', sessionAuth, tenderCheck, materialCheck, 'materialController.checkAudit');
+    // 调差工料
+    app.post('/tender/:id/measure/material/:order/save', sessionAuth, tenderCheck, materialCheck, 'materialController.saveBillsData');
+
     // 个人账号相关
     app.get('/profile/info', sessionAuth, 'profileController.info');
     app.get('/profile/sms', sessionAuth, 'profileController.sms');

+ 10 - 0
app/service/ledger.js

@@ -238,6 +238,16 @@ module.exports = app => {
             return await this.db.query(sql, sqlParam);
         }
 
+        async getTenderUsedUnits(tenderId) {
+            const sql = 'SELECT unit ' +
+                '  FROM ' + this.tableName +
+                '  WHERE tender_id = ? and is_leaf = true' +
+                '  GROUP By unit';
+            const sqlParam = [tenderId];
+            const units = this.db.query(sql, sqlParam);
+            return this._.map(units, 'unit');
+        }
+
         /**
          * 统计子节点total_price
          * @param {Number} tenderId - 标段id

+ 172 - 0
app/service/material.js

@@ -0,0 +1,172 @@
+'use strict';
+
+/**
+ * 期计量 数据模型
+ *
+ * @author Mai
+ * @date 2018/8/13
+ * @version
+ */
+
+const auditConst = require('../const/audit').material;
+
+module.exports = app => {
+    class Material extends app.BaseService {
+        /**
+         * 构造函数
+         *
+         * @param {Object} ctx - egg全局变量
+         * @return {void}
+         */
+        constructor(ctx) {
+            super(ctx);
+            this.tableName = 'material';
+        }
+
+        /**
+         * 获取 最新一期 材料调差期计量
+         * @param tenderId
+         * @param includeUnCheck
+         * @returns {Promise<*>}
+         */
+        async getLastestMaterial(tenderId, includeUnCheck = false) {
+            this.initSqlBuilder();
+            this.sqlBuilder.setAndWhere('tid', {
+                value: tenderId,
+                operate: '=',
+            });
+            if (!includeUnCheck) {
+                this.sqlBuilder.setAndWhere('status', {
+                    value: auditConst.status.uncheck,
+                    operate: '!=',
+                });
+            }
+            this.sqlBuilder.orderBy = [['order', 'desc']];
+            const [sql, sqlParam] = this.sqlBuilder.build(this.tableName);
+            const material = await this.db.queryOne(sql, sqlParam);
+            return material;
+        }
+
+        /**
+         * 获取 最新一期 审批完成的 材料调差期计量
+         * @param tenderId
+         * @returns {Promise<*>}
+         */
+        async getLastestCompleteMaterial(tenderId) {
+            this.initSqlBuilder();
+            this.sqlBuilder.setAndWhere('tid', {
+                value: tenderId,
+                operate: '=',
+            });
+            this.sqlBuilder.setAndWhere('status', {
+                value: auditConst.status.checked,
+                operate: '=',
+            });
+            this.sqlBuilder.orderBy = [['order', 'desc']];
+            const [sql, sqlParam] = this.sqlBuilder.build(this.tableName);
+            const material = await this.db.queryOne(sql, sqlParam);
+            return material;
+        }
+
+        /**
+         * 获取标段下的全部计量期,按倒序
+         * @param tenderId
+         * @returns {Promise<void>}
+         */
+        async getValidMaterials(tenderId) {
+            const materials = await this.db.select(this.tableName, {
+                where: { tid: tenderId },
+                orders: [['order', 'desc']],
+            });
+            if (materials.length !== 0) {
+                const lastMaterial = materials[materials.length - 1];
+                if (lastMaterial.status === auditConst.status.uncheck && lastMaterial.user_id !== this.ctx.session.sessionUser.accountId) {
+                    materials.splice(materials.length - 1, 1);
+                }
+            }
+            // 最新一期计量(未审批完成),当前操作人的期详细数据,应实时计算
+            if (materials.length > 0 && materials[0].status !== auditConst.status.checked) {
+                const material = materials[0];
+                const curAuditor = await this.ctx.service.materialAudit.getCurAuditor(material.id, material.times);
+                const isActive = curAuditor ? curAuditor.id === this.ctx.session.sessionUser.accountId : material.user_id === this.ctx.session.sessionUser.accountId;
+                if (isActive) {
+                    material.curTimes = material.times;
+                    material.curOrder = curAuditor ? curAuditor.order : 0;
+                }
+            }
+            return materials;
+        }
+
+        /**
+         * 添加材料调差期
+         * @param tenderId - 标段id
+         * @param data - post的数据
+         * @returns {Promise<void>}
+         */
+        async addMaterial(tenderId, data) {
+            const materials = await this.getAllDataByCondition({
+                where: { tid: tenderId },
+                order: ['order'],
+            });
+            const preMaterial = materials[materials.length - 1];
+            if (materials.length > 0 && materials[materials.length - 1].status !== auditConst.status.checked) {
+                throw '上一期未审批通过,请等待上一期审批通过后,再新增数据';
+            }
+            const order = materials.length + 1;
+            const newMaterial = {
+                tid: tenderId,
+                order,
+                in_time: new Date(),
+                times: 1,
+                status: auditConst.status.uncheck,
+                user_id: this.ctx.session.sessionUser.accountId,
+                stage_id: data.stage_id.join(','),
+                s_order: data.s_order,
+            };
+            const transaction = await this.db.beginTransaction();
+            try {
+                // 新增期记录
+                const result = await transaction.insert(this.tableName, newMaterial);
+                if (result.affectedRows === 1) {
+                    newMaterial.id = result.insertId;
+                } else {
+                    throw '新增期数据失败';
+                }
+                // 存在上一期时,复制上一期审批流程
+                if (preMaterial) {
+                    const auditResult = await this.ctx.service.materialAudit.copyPreMaterialAuditors(transaction, preMaterial, newMaterial);
+                    if (!auditResult) {
+                        throw '复制上一期审批流程失败';
+                    }
+                }
+
+                await transaction.commit();
+                return newMaterial;
+            } catch (err) {
+                await transaction.rollback();
+                throw err;
+            }
+        }
+
+        /**
+         * 删除材料调差期
+         *
+         * @param {Number} id - 期Id
+         * @returns {Promise<void>}
+         */
+        async deleteMaterial(id) {
+            const transaction = await this.db.beginTransaction();
+            try {
+                await transaction.delete(this.tableName, { id });
+                await transaction.delete(this.ctx.service.materialAudit.tableName, { mid: id });
+                await transaction.commit();
+                return true;
+            } catch (err) {
+                await transaction.rollback();
+                throw err;
+            }
+        }
+    }
+
+    return Material;
+};

+ 549 - 0
app/service/material_audit.js

@@ -0,0 +1,549 @@
+'use strict';
+
+/**
+ *
+ *
+ * @author Mai
+ * @date 2019/2/27
+ * @version
+ */
+
+const auditConst = require('../const/audit').material;
+// const smsTypeConst = require('../const/sms_type');
+const SMS = require('../lib/sms');
+
+module.exports = app => {
+    class MaterialAudit extends app.BaseService {
+        /**
+         * 构造函数
+         *
+         * @param {Object} ctx - egg全局变量
+         * @return {void}
+         */
+        constructor(ctx) {
+            super(ctx);
+            this.tableName = 'material_audit';
+        }
+
+        /**
+         * 获取 审核人信息
+         *
+         * @param {Number} materialId - 材料调差期id
+         * @param {Number} auditorId - 审核人id
+         * @param {Number} times - 第几次审批
+         * @returns {Promise<*>}
+         */
+        async getAuditor(materialId, auditorId, times = 1) {
+            const sql = 'SELECT la.`aid`, pa.`name`, pa.`company`, pa.`role`, pa.`mobile`, pa.`telephone`, la.`times`, la.`order`, la.`status`, la.`opinion`, la.`begin_time`, la.`end_time` ' +
+                'FROM ?? AS la, ?? AS pa ' +
+                'WHERE la.`mid` = ? and la.`aid` = ? and la.`times` = ?' +
+                '    and la.`aid` = pa.`id`';
+            const sqlParam = [this.tableName, this.ctx.service.projectAccount.tableName, materialId, auditorId, times];
+            return await this.db.queryOne(sql, sqlParam);
+        }
+
+        /**
+         * 获取 审核列表信息
+         *
+         * @param {Number} materialId - 材料调差期id
+         * @param {Number} times - 第几次审批
+         * @returns {Promise<*>}
+         */
+        async getAuditors(materialId, times = 1) {
+            const sql = 'SELECT la.`aid`, pa.`name`, pa.`company`, pa.`role`, pa.`mobile`, pa.`telephone`, la.`times`, la.`order`, la.`status`, la.`opinion`, la.`begin_time`, la.`end_time` ' +
+                'FROM ?? AS la, ?? AS pa ' +
+                'WHERE la.`mid` = ? and la.`times` = ? and la.`aid` = pa.`id` order by la.`order`';
+            const sqlParam = [this.tableName, this.ctx.service.projectAccount.tableName, materialId, times];
+            return await this.db.query(sql, sqlParam);
+        }
+
+        /**
+         * 获取 当前审核人
+         *
+         * @param {Number} materialId - 材料调差期id
+         * @param {Number} times - 第几次审批
+         * @returns {Promise<*>}
+         */
+        async getCurAuditor(materialId, times = 1) {
+            const sql = 'SELECT la.`aid`, pa.`name`, pa.`company`, pa.`role`, pa.`mobile`, pa.`telephone`, la.`times`, la.`order`, la.`status`, la.`opinion`, la.`begin_time`, la.`end_time` ' +
+                'FROM ?? AS la, ?? AS pa ' +
+                'WHERE la.`mid` = ? and la.`status` = ? and la.`times` = ?' +
+                '    and la.`aid` = pa.`id`';
+            const sqlParam = [this.tableName, this.ctx.service.projectAccount.tableName, materialId, auditConst.status.checking, times];
+            return await this.db.queryOne(sql, sqlParam);
+        }
+
+        /**
+         * 获取 最新审核顺序
+         *
+         * @param {Number} materialId - 材料调差期id
+         * @param {Number} times - 第几次审批
+         * @returns {Promise<number>}
+         */
+        async getNewOrder(materialId, times = 1) {
+            const sql = 'SELECT Max(??) As max_order FROM ?? Where `mid` = ? and `times` = ?';
+            const sqlParam = ['order', this.tableName, materialId, times];
+            const result = await this.db.queryOne(sql, sqlParam);
+            return result && result.max_order ? result.max_order + 1 : 1;
+        }
+
+        /**
+         * 新增审核人
+         *
+         * @param {Number} materialId - 材料调差期id
+         * @param {Number} auditorId - 审核人id
+         * @param {Number} times - 第几次审批
+         * @return {Promise<number>}
+         */
+        async addAuditor(materialId, auditorId, times = 1) {
+            const newOrder = await this.getNewOrder(materialId, times);
+            const data = {
+                tid: this.ctx.tender.id,
+                mid: materialId,
+                aid: auditorId,
+                times,
+                order: newOrder,
+                status: auditConst.status.uncheck,
+            };
+            const result = await this.db.insert(this.tableName, data);
+            return result.effectRows = 1;
+        }
+
+        /**
+         * 移除审核人时,同步其后审核人order
+         * @param transaction - 事务
+         * @param {Number} materialId - 材料调差期id
+         * @param {Number} auditorId - 审核人id
+         * @param {Number} times - 第几次审批
+         * @returns {Promise<*>}
+         * @private
+         */
+        async _syncOrderByDelete(transaction, materialId, order, times) {
+            this.initSqlBuilder();
+            this.sqlBuilder.setAndWhere('mid', {
+                value: materialId,
+                operate: '=',
+            });
+            this.sqlBuilder.setAndWhere('order', {
+                value: order,
+                operate: '>=',
+            });
+            this.sqlBuilder.setAndWhere('times', {
+                value: times,
+                operate: '=',
+            });
+            this.sqlBuilder.setUpdateData('order', {
+                value: 1,
+                selfOperate: '-',
+            });
+            const [sql, sqlParam] = this.sqlBuilder.build(this.tableName, 'update');
+            const data = await transaction.query(sql, sqlParam);
+
+            return data;
+        }
+
+        /**
+         * 移除审核人
+         *
+         * @param {Number} materialId - 材料调差期id
+         * @param {Number} auditorId - 审核人id
+         * @param {Number} times - 第几次审批
+         * @return {Promise<boolean>}
+         */
+        async deleteAuditor(materialId, auditorId, times = 1) {
+            const transaction = await this.db.beginTransaction();
+            try {
+                const condition = { mid: materialId, aid: auditorId, times };
+                const auditor = await this.getDataByCondition(condition);
+                if (!auditor) {
+                    throw '该审核人不存在';
+                }
+                await this._syncOrderByDelete(transaction, materialId, auditor.order, times);
+                await transaction.delete(this.tableName, condition);
+                await transaction.commit();
+            } catch (err) {
+                await transaction.rollback();
+                throw err;
+            }
+            return true;
+        }
+
+        /**
+         * 开始审批
+         *
+         * @param {Number} materialId - 材料调差期id
+         * @param {Number} times - 第几次审批
+         * @return {Promise<boolean>}
+         */
+        async start(materialId, times = 1) {
+            const audit = await this.getDataByCondition({ mid: materialId, times, order: 1 });
+            if (!audit) {
+                throw '请先选择审批人,再上报数据';
+            }
+
+            const transaction = await this.db.beginTransaction();
+            try {
+                await transaction.update(this.tableName, { id: audit.id, status: auditConst.status.checking, begin_time: new Date() });
+                // 计算原报最终数据
+                // await this.ctx.service.stagePay.calcAllStagePays(this.ctx.stage, transaction);
+                // 复制一份下一审核人数据
+                // await this.ctx.service.stagePay.copyAuditStagePays(this.ctx.stage, this.ctx.stage.times, 1, transaction);
+                // 更新期数据
+                // const tpData = await this.ctx.service.stageBills.getSumTotalPrice(this.ctx.stage);
+                await transaction.update(this.ctx.service.material.tableName, {
+                    id: materialId, status: auditConst.status.checking,
+                });
+
+                // 添加短信通知-需要审批提醒功能
+                // const smsUser = await this.ctx.service.projectAccount.getDataById(audit.aid);
+                // if (smsUser.auth_mobile !== '' && smsUser.auth_mobile !== undefined && smsUser.sms_type !== '') {
+                //     const smsType = JSON.parse(smsUser.sms_type);
+                //     if (smsType[smsTypeConst.const.JL] !== undefined && smsType[smsTypeConst.const.JL].indexOf(smsTypeConst.judge.approval.toString()) !== -1) {
+                //         const tenderInfo = await this.ctx.service.tender.getDataById(audit.tid);
+                //         const stageInfo = await this.ctx.service.stage.getDataById(audit.sid);
+                //         const sms = new SMS(this.ctx);
+                //         const tenderName = await sms.contentChange(tenderInfo.name);
+                //         const content = '【纵横计量支付】' + tenderName + '第' + stageInfo.order + '期,需要您审批。';
+                //         sms.send(smsUser.auth_mobile, content);
+                //     }
+                // }
+
+                // todo 更新标段tender状态 ?
+                await transaction.commit();
+            } catch (err) {
+                await transaction.rollback();
+                throw err;
+            }
+            return true;
+        }
+
+        async _checked(materialId, checkData, times) {
+            const time = new Date();
+            // 整理当前流程审核人状态更新
+            const audit = await this.getDataByCondition({ mid: materialId, times, status: auditConst.status.checking });
+            if (!audit) {
+                throw '审核数据错误';
+            }
+            const nextAudit = await this.getDataByCondition({ mid: materialId, times, order: audit.order + 1 });
+
+            const transaction = await this.db.beginTransaction();
+            try {
+                await transaction.update(this.tableName, { id: audit.id, status: checkData.checkType, opinion: checkData.opinion, end_time: time });
+                // 无下一审核人表示,审核结束
+                if (nextAudit) {
+                    // 复制一份下一审核人数据
+                    // await this.ctx.service.stagePay.copyAuditStagePays(this.ctx.stage, this.ctx.stage.times, nextAudit.order, transaction);
+                    // 流程至下一审批人
+                    await transaction.update(this.tableName, { id: nextAudit.id, status: auditConst.status.checking, begin_time: time });
+                    // 同步 期信息
+                    await transaction.update(this.ctx.service.material.tableName, {
+                        id: materialId, status: auditConst.status.checking,
+                    });
+
+                    // 添加短信通知-需要审批提醒功能
+                    // const smsUser = await this.ctx.service.projectAccount.getDataById(nextAudit.aid);
+                    // if (smsUser.auth_mobile !== '' && smsUser.auth_mobile !== undefined && smsUser.sms_type !== '') {
+                    //     const smsType = JSON.parse(smsUser.sms_type);
+                    //     if (smsType[smsTypeConst.const.JL] !== undefined && smsType[smsTypeConst.const.JL].indexOf(smsTypeConst.judge.approval.toString()) !== -1) {
+                    //         const tenderInfo = await this.ctx.service.tender.getDataById(nextAudit.tid);
+                    //         const stageInfo = await this.ctx.service.stage.getDataById(nextAudit.sid);
+                    //         const sms = new SMS(this.ctx);
+                    //         const tenderName = await sms.contentChange(tenderInfo.name);
+                    //         const content = '【纵横计量支付】' + tenderName + '第' + stageInfo.order + '期,需要您审批。';
+                    //         sms.send(smsUser.auth_mobile, content);
+                    //     }
+                    // }
+                } else {
+                    // 本期结束
+                    // 生成截止本期数据 final数据
+                    // console.time('generatePre');
+                    // await this.ctx.service.stageBillsFinal.generateFinalData(transaction, this.ctx.tender, this.ctx.stage);
+                    // await this.ctx.service.stagePosFinal.generateFinalData(transaction, this.ctx.tender, this.ctx.stage);
+                    // console.timeEnd('generatePre');
+                    // 同步 期信息
+                    await transaction.update(this.ctx.service.material.tableName, {
+                        id: materialId, status: checkData.checkType,
+                    });
+
+                    // 添加短信通知-审批通过提醒功能
+                    // const mobile_array = [];
+                    // const stageInfo = await this.ctx.service.stage.getDataById(stageId);
+                    // const auditList = await this.getAuditors(stageId, stageInfo.times);
+                    // const smsUser1 = await this.ctx.service.projectAccount.getDataById(stageInfo.user_id);
+                    // if (smsUser1.auth_mobile !== undefined && smsUser1.sms_type !== '') {
+                    //     const smsType = JSON.parse(smsUser1.sms_type);
+                    //     if (smsType[smsTypeConst.const.JL] !== undefined && smsType[smsTypeConst.const.JL].indexOf(smsTypeConst.judge.result.toString()) !== -1) {
+                    //         mobile_array.push(smsUser1.auth_mobile);
+                    //     }
+                    // }
+                    // for (const user of auditList) {
+                    //     const smsUser = await this.ctx.service.projectAccount.getDataById(user.aid);
+                    //     if (smsUser.auth_mobile !== '' && smsUser.auth_mobile !== undefined && smsUser.sms_type !== '') {
+                    //         const smsType = JSON.parse(smsUser.sms_type);
+                    //         if (mobile_array.indexOf(smsUser.auth_mobile) === -1 && smsType[smsTypeConst.const.JL] !== undefined && smsType[smsTypeConst.const.JL].indexOf(smsTypeConst.judge.result.toString()) !== -1) {
+                    //             mobile_array.push(smsUser.auth_mobile);
+                    //         }
+                    //     }
+                    // }
+                    // if (mobile_array.length > 0) {
+                    //     const tenderInfo = await this.ctx.service.tender.getDataById(stageInfo.tid);
+                    //     const sms = new SMS(this.ctx);
+                    //     const tenderName = await sms.contentChange(tenderInfo.name);
+                    //     const content = '【纵横计量支付】' + tenderName + '第' + stageInfo.order + '期,审批通过。';
+                    //     sms.send(mobile_array, content);
+                    // }
+                }
+                await transaction.commit();
+            } catch (err) {
+                await transaction.rollback();
+                throw err;
+            }
+        }
+
+        async _checkNo(materialId, checkData, times) {
+            const time = new Date();
+            // 整理当前流程审核人状态更新
+            const audit = await this.getDataByCondition({ mid: materialId, times, status: auditConst.status.checking });
+            if (!audit) {
+                throw '审核数据错误';
+            }
+            const sql = 'SELECT `tid`, `mid`, `aid`, `order` FROM ?? WHERE `mid` = ? and `times` = ? GROUP BY `aid`';
+            const sqlParam = [this.tableName, materialId, times];
+            const auditors = await this.db.query(sql, sqlParam);
+            let order = 1;
+            for (const a of auditors) {
+                a.times = times + 1;
+                a.order = order;
+                a.status = auditConst.status.uncheck;
+                order++;
+            }
+
+            const transaction = await this.db.beginTransaction();
+            try {
+                await transaction.update(this.tableName, { id: audit.id, status: checkData.checkType, opinion: checkData.opinion, end_time: time });
+                // 同步 期信息
+                await transaction.update(this.ctx.service.material.tableName, {
+                    id: materialId, status: checkData.checkType,
+                    times: times + 1,
+                });
+                // 拷贝新一次审核流程列表
+                await transaction.insert(this.tableName, auditors);
+                // 计算该审批人最终数据
+                // await this.ctx.service.stagePay.calcAllStagePays(this.ctx.stage, transaction);
+                // 复制一份最新数据给原报
+                // await this.ctx.service.stagePay.copyAuditStagePays(this.ctx.stage, this.ctx.stage.times + 1, 0, transaction);
+
+                // 添加短信通知-审批退回提醒功能
+                // const mobile_array = [];
+                // const stageInfo = await this.ctx.service.stage.getDataById(stageId);
+                // const auditList = await this.getAuditors(stageId, stageInfo.times);
+                // const smsUser1 = await this.ctx.service.projectAccount.getDataById(stageInfo.user_id);
+                // if (smsUser1.auth_mobile !== '' && smsUser1.auth_mobile !== undefined && smsUser1.sms_type !== '') {
+                //     const smsType = JSON.parse(smsUser1.sms_type);
+                //     if (smsType[smsTypeConst.const.JL] !== undefined && smsType[smsTypeConst.const.JL].indexOf(smsTypeConst.judge.result.toString()) !== -1) {
+                //         mobile_array.push(smsUser1.auth_mobile);
+                //     }
+                // }
+                // for (const user of auditList) {
+                //     const smsUser = await this.ctx.service.projectAccount.getDataById(user.aid);
+                //     if (smsUser.auth_mobile !== '' && smsUser.auth_mobile !== undefined && smsUser.sms_type !== '') {
+                //         const smsType = JSON.parse(smsUser.sms_type);
+                //         if (mobile_array.indexOf(smsUser.auth_mobile) === -1 && smsType[smsTypeConst.const.JL] !== undefined && smsType[smsTypeConst.const.JL].indexOf(smsTypeConst.judge.result.toString()) !== -1) {
+                //             mobile_array.push(smsUser.auth_mobile);
+                //         }
+                //     }
+                // }
+                // if (mobile_array.length > 0) {
+                //     const tenderInfo = await this.ctx.service.tender.getDataById(stageInfo.tid);
+                //     const sms = new SMS(this.ctx);
+                //     const tenderName = await sms.contentChange(tenderInfo.name);
+                //     const content = '【纵横计量支付】' + tenderName + '第' + stageInfo.order + '期,审批退回。';
+                //     sms.send(mobile_array, content);
+                // }
+
+                await transaction.commit();
+            } catch (err) {
+                await transaction.rollback();
+                throw err;
+            }
+        }
+
+        /**
+         * 审批
+         * @param {Number} materialId - 材料调差期id
+         * @param {auditConst.status.checked|auditConst.status.checkNo} checkType - 审批结果
+         * @param {Number} times - 第几次审批
+         * @returns {Promise<void>}
+         */
+        async check(materialId, checkData, times = 1) {
+            if (checkData.checkType !== auditConst.status.checked && checkData.checkType !== auditConst.status.checkNo) {
+                throw '提交数据错误';
+            }
+            switch (checkData.checkType) {
+                case auditConst.status.checked:
+                    await this._checked(materialId, checkData, times);
+                    break;
+                case auditConst.status.checkNo:
+                    await this._checkNo(materialId, checkData, times);
+                    break;
+                default:
+                    throw '无效审批操作';
+            }
+        }
+
+        /**
+         * 审批
+         * @param {Number} materialId - 材料调差期id
+         * @param {Number} times - 第几次审批
+         * @returns {Promise<void>}
+         */
+        async checkAgain(materialId, times = 1) {
+            const time = new Date();
+            // 整理当前流程审核人状态更新
+            const audit = (await this.getAllDataByCondition({ where: { mid: materialId, times }, orders: [['order', 'desc']], limit: 1, offset: 0 }))[0];
+            if (!audit || audit.order <= 1) {
+                throw '审核数据错误';
+            }
+            const transaction = await this.db.beginTransaction();
+            try {
+                // 当前审批人2次添加至流程中
+                const newAuditors = [];
+                newAuditors.push({
+                    tid: audit.tid, mid: audit.mid, aid: audit.aid,
+                    times: audit.times, order: audit.order + 1, status: auditConst.status.checkAgain,
+                    begin_time: time, end_time: time, opinion: '',
+                });
+                newAuditors.push({
+                    tid: audit.tid, mid: audit.mid, aid: audit.aid,
+                    times: audit.times, order: audit.order + 2, status: auditConst.status.checking,
+                    begin_time: time,
+                });
+                await transaction.insert(this.tableName, newAuditors);
+
+                // 复制一份最新数据给下一人
+                // await this.ctx.service.stagePay.copyAuditStagePays(this.ctx.stage, this.ctx.stage.times, audit.order + 2, transaction);
+
+                // 本期结束
+                // 生成截止本期数据 final数据
+                // await this.ctx.service.stageBillsFinal.delGenerateFinalData(transaction, this.ctx.tender, this.ctx.stage);
+                // await this.ctx.service.stagePosFinal.delGenerateFinalData(transaction, this.ctx.tender, this.ctx.stage);
+                // 同步 期信息
+                await transaction.update(this.ctx.service.material.tableName, {
+                    id: materialId, status: auditConst.status.checking,
+                });
+
+                // // 添加短信通知-需要审批提醒功能
+                // const smsUser = await this.ctx.service.projectAccount.getDataById(audit.aid);
+                // if (smsUser.auth_mobile !== undefined && smsUser.sms_type !== '') {
+                //     const smsType = JSON.parse(smsUser.sms_type);
+                //     if (smsType[smsTypeConst.const.JL] !== undefined && smsType[smsTypeConst.const.JL].indexOf(smsTypeConst.judge.approval.toString()) !== -1) {
+                //         const tenderInfo = await this.ctx.service.tender.getDataById(audit.tid);
+                //         const stageInfo = await this.ctx.service.stage.getDataById(audit.sid);
+                //         const sms = new SMS(this.ctx);
+                //         const tenderName = await sms.contentChange(tenderInfo.name);
+                //         const content = '【纵横计量支付】' + tenderName + '第' + stageInfo.order + '期,需要您审批。';
+                //         sms.send(smsUser.auth_mobile, content);
+                //     }
+                // }
+                await transaction.commit();
+            } catch (err) {
+                await transaction.rollback();
+                throw err;
+            }
+        }
+
+        /**
+         * 获取审核人需要审核的期列表
+         *
+         * @param auditorId
+         * @returns {Promise<*>}
+         */
+        async getAuditMaterial(auditorId) {
+            const sql = 'SELECT sa.`aid`, sa.`times`, sa.`order`, sa.`begin_time`, sa.`end_time`, sa.`tid`, sa.`mid`,' +
+                        '    s.`order` As `sorder`, s.`status` As `sstatus`,' +
+                        '    t.`name`, t.`project_id`, t.`type`, t.`user_id` ' +
+                        '  FROM ?? AS sa, ?? AS s, ?? As t ' +
+                        '  WHERE ((sa.`aid` = ? and sa.`status` = ?) OR (s.`user_id` = ? and sa.`status` = ? and s.`status` = ? and sa.`times` = (s.`times`-1)))' +
+                        '    and sa.`mid` = s.`id` and sa.`tid` = t.`id`';
+            const sqlParam = [this.tableName, this.ctx.service.material.tableName, this.ctx.service.tender.tableName, auditorId, auditConst.status.checking, auditorId, auditConst.status.checkNo, auditConst.status.checkNo];
+            return await this.db.query(sql, sqlParam);
+        }
+
+        /**
+         * 获取审核人流程列表
+         *
+         * @param auditorId
+         * @returns {Promise<*>}
+         */
+        async getAuditGroupByList(materialId, times) {
+            const sql = 'SELECT la.`aid`, pa.`name`, pa.`company`, pa.`role`, la.`times`, la.`mid`, la.`aid`, la.`order` ' +
+                'FROM ?? AS la, ?? AS pa ' +
+                'WHERE la.`mid` = ? and la.`times` = ? and la.`aid` = pa.`id` GROUP BY la.`aid` ORDER BY la.`order`';
+            const sqlParam = [this.tableName, this.ctx.service.projectAccount.tableName, materialId, times];
+            return await this.db.query(sql, sqlParam);
+        }
+
+        /**
+         * 复制上一期的审批人列表给最新一期
+         *
+         * @param transaction - 新增一期的事务
+         * @param {Object} preMaterial - 上一期
+         * @param {Object} newaMaterial - 最新一期
+         * @returns {Promise<*>}
+         */
+        async copyPreMaterialAuditors(transaction, preMaterial, newMaterial) {
+            const auditors = await this.getAuditGroupByList(preMaterial.id, preMaterial.times);
+            const newAuditors = [];
+            for (const a of auditors) {
+                const na = {
+                    tid: preMaterial.tid,
+                    mid: newMaterial.id,
+                    aid: a.aid,
+                    times: newMaterial.times,
+                    order: newAuditors.length + 1,
+                    status: auditConst.status.uncheck,
+                };
+                newAuditors.push(na);
+            }
+            const result = await transaction.insert(this.tableName, newAuditors);
+            return result.effectRows = auditors.length;
+        }
+
+        /**
+         * 移除审核人
+         *
+         * @param {Number} materialId - 材料调差期id
+         * @param {Number} status - 期状态
+         * @param {Number} status - 期次数
+         * @return {Promise<boolean>}
+         */
+        async getAuditorByStatus(materialId, status, times = 1) {
+            let auditor = null;
+            let sql = '';
+            let sqlParam = '';
+            switch (status) {
+                case auditConst.status.checking :
+                case auditConst.status.checked :
+                case auditConst.status.checkNoPre :
+                    sql = 'SELECT la.`aid`, pa.`name`, pa.`company`, pa.`role`, la.`times`, la.`mid`, la.`aid`, la.`order` ' +
+                        'FROM ?? AS la, ?? AS pa ' +
+                        'WHERE la.`mid` = ? and la.`status` = ? and la.`aid` = pa.`id` order by la.`times` desc, la.`id` desc';
+                    sqlParam = [this.tableName, this.ctx.service.projectAccount.tableName, materialId, status];
+                    auditor = await this.db.queryOne(sql, sqlParam);
+                    break;
+                case auditConst.status.checkNo :
+                    sql = 'SELECT la.`aid`, pa.`name`, pa.`company`, pa.`role`, la.`times`, la.`mid`, la.`aid`, la.`order` ' +
+                        'FROM ?? AS la, ?? AS pa ' +
+                        'WHERE la.`mid` = ? and la.`status` = ? and la.`times` = ? and la.`aid` = pa.`id` order by la.`times` desc, la.`id` desc';
+                    sqlParam = [this.tableName, this.ctx.service.projectAccount.tableName, materialId, auditConst.status.checkNo, parseInt(times) - 1];
+                    auditor = await this.db.queryOne(sql, sqlParam);
+                    break;
+                case auditConst.status.uncheck :
+                default:break;
+            }
+            return auditor;
+        }
+    }
+
+    return MaterialAudit;
+};

+ 77 - 0
app/service/material_bills.js

@@ -0,0 +1,77 @@
+'use strict';
+
+/**
+ * 期计量 数据模型
+ *
+ * @author Mai
+ * @date 2018/8/13
+ * @version
+ */
+
+const auditConst = require('../const/audit').material;
+
+module.exports = app => {
+    class MaterialBills extends app.BaseService {
+        /**
+         * 构造函数
+         *
+         * @param {Object} ctx - egg全局变量
+         * @return {void}
+         */
+        constructor(ctx) {
+            super(ctx);
+            this.tableName = 'material_bills';
+        }
+
+        /**
+         * 添加工料
+         * @return {void}
+         */
+        async add() {
+            if (!this.ctx.tender || !this.ctx.material) {
+                throw '数据错误';
+            }
+            const newBills = {
+                tid: this.ctx.tender.id,
+                mid: this.ctx.material.id,
+                in_time: new Date(),
+            };
+
+            // 新增工料
+            const result = await this.db.insert(this.tableName, newBills);
+            if (result.affectedRows !== 1) {
+                throw '新增工料数据失败';
+            }
+            return await this.getDataById(result.insertId);
+        }
+
+        /**
+         * 删除工料
+         * @param {int} id 工料id
+         * @return {void}
+         */
+        async del(id) {
+            if (!this.ctx.tender || !this.ctx.material) {
+                throw '数据错误';
+            }
+            // 判断是否可删
+            return await this.deleteById(id);
+        }
+
+        /**
+         * 修改工料信息
+         * @param {Object} data 工料内容
+         * @return {void}
+         */
+        async save(data) {
+            if (!this.ctx.tender || !this.ctx.material) {
+                throw '数据错误';
+            }
+            delete data.in_time;
+            // 判断是否可修改
+            return await this.db.update(this.tableName, data);
+        }
+    }
+
+    return MaterialBills;
+};

+ 77 - 0
app/service/material_list.js

@@ -0,0 +1,77 @@
+'use strict';
+
+/**
+ * 期计量 数据模型
+ *
+ * @author Mai
+ * @date 2018/8/13
+ * @version
+ */
+
+const auditConst = require('../const/audit').material;
+
+module.exports = app => {
+    class MaterialList extends app.BaseService {
+        /**
+         * 构造函数
+         *
+         * @param {Object} ctx - egg全局变量
+         * @return {void}
+         */
+        constructor(ctx) {
+            super(ctx);
+            this.tableName = 'material_list';
+        }
+
+        /**
+         * 添加工料
+         * @return {void}
+         */
+        async add() {
+            if (!this.ctx.tender || !this.ctx.material) {
+                throw '数据错误';
+            }
+            const newBills = {
+                tid: this.ctx.tender.id,
+                mid: this.ctx.material.id,
+                in_time: new Date(),
+            };
+
+            // 新增工料
+            const result = await this.db.insert(this.tableName, newBills);
+            if (result.affectedRows !== 1) {
+                throw '新增工料数据失败';
+            }
+            return await this.getDataById(result.insertId);
+        }
+
+        /**
+         * 删除工料
+         * @param {int} id 工料id
+         * @return {void}
+         */
+        async del(id) {
+            if (!this.ctx.tender || !this.ctx.material) {
+                throw '数据错误';
+            }
+            // 判断是否可删
+            return await this.deleteById(id);
+        }
+
+        /**
+         * 修改工料信息
+         * @param {Object} data 工料内容
+         * @return {void}
+         */
+        async save(data) {
+            if (!this.ctx.tender || !this.ctx.material) {
+                throw '数据错误';
+            }
+            delete data.in_time;
+            // 判断是否可修改
+            return await this.db.update(this.tableName, data);
+        }
+    }
+
+    return MaterialList;
+};

+ 11 - 0
app/service/pos.js

@@ -55,6 +55,17 @@ module.exports = app => {
             // return await this.db.query(sql, sqlParam);
         }
 
+        async getPosDataByUnits(tenderId, units) {
+            console.log(units);
+            const sql = 'SELECT p.id, p.lid, p.sgfh_qty, p.sjcl_qty, p.qtcl_qty' +
+                '  FROM ' + this.tableName + ' p' +
+                '  LEFT JOIN ' + this.ctx.service.ledger.tableName + ' b' +
+                '  ON p.lid = b.id ' +
+                '  WHERE p.tid = ? and b.unit IN (?)';
+            const sqlParam = [tenderId, units];
+            return await this.db.query(sql, sqlParam);
+        }
+
         async _insertPosData(transaction, data, tid) {
             data.id = this.uuid.v4();
             data.tid = tid;

+ 4 - 4
app/service/project_account.js

@@ -119,7 +119,7 @@ module.exports = app => {
                 // let cooperation = 0;
                 if (loginType === 2) {
                     // 查找项目数据
-                    const projectData = await this.ctx.service.project.getProjectByCode(data.project.toString());
+                    const projectData = await this.ctx.service.project.getProjectByCode(data.project.toString().trim());
                     if (projectData === null) {
                         throw '不存在项目数据';
                     }
@@ -131,7 +131,7 @@ module.exports = app => {
 
                     // 查找对应数据
                     accountData = await this.db.get(this.tableName, {
-                        account: data.account,
+                        account: data.account.trim(),
                         project_id: projectData.id,
                         enable: 1,
                     });
@@ -140,7 +140,7 @@ module.exports = app => {
                         throw '不存在对应用户数据';
                     }
 
-                    projectList = await this.getProjectInfoByAccount(data.account);
+                    projectList = await this.getProjectInfoByAccount(data.account.trim());
                     // permission = accountData.permission;
                     // cooperation = accountData.cooperation;
 
@@ -151,7 +151,7 @@ module.exports = app => {
                     //      result = await sso.loginValid(data.account, data.project_password.toString());
                     // } else {
                         // 加密密码
-                        const encryptPassword = crypto.createHmac('sha1', data.account).update(data.project_password)
+                        const encryptPassword = crypto.createHmac('sha1', data.account.trim()).update(data.project_password.trim())
                             .digest().toString('base64');
                         result = encryptPassword === accountData.password;
                     //}

+ 35 - 44
app/service/revise_audit.js

@@ -10,7 +10,6 @@
 
 const auditConst = require('../const/audit').revise;
 const smsTypeConst = require('../const/sms_type');
-const SMS = require('../lib/sms');
 
 module.exports = app => {
     class ReviseAudit extends app.BaseService {
@@ -197,16 +196,14 @@ module.exports = app => {
                 await transaction.update(this.ctx.service.ledgerRevise.tableName, reviseData);
 
                 // 添加短信通知-需要审批提醒功能
-                const smsUser = await this.ctx.service.projectAccount.getDataById(audit.audit_id);
-                if (smsUser.auth_mobile !== undefined && smsUser.sms_type !== '') {
-                    const smsType = JSON.parse(smsUser.sms_type);
-                    if (smsType[smsTypeConst.const.TZ] !== undefined && smsType[smsTypeConst.const.TZ].indexOf(smsTypeConst.judge.approval.toString()) !== -1) {
-                        const sms = new SMS(this.ctx);
-                        const tenderName = await sms.contentChange(this.ctx.tender.data.name);
-                        const content = '【纵横计量支付】' + tenderName + '台账修订需要您审批。';
-                        sms.send(smsUser.auth_mobile, content);
-                    }
-                }
+                // 下一人
+                await this.ctx.helper.sendUserSms(audit.audit_id, smsTypeConst.const.XD,
+                    smsTypeConst.judge.approval.toString(), '台帐修订需要您审批,请登录系统处理。');
+                // 其他参与人
+                const auditList = await this.getAuditors(revise.tid, times);
+                const users = this._.pull(this._.map(auditList, 'user_id'), audit.id);
+                await this.ctx.helper.sendUserSms(users, smsTypeConst.const.XD,
+                    smsTypeConst.judge.result.toString(), '台账修订已上报。');
 
                 await transaction.commit();
             } catch (err) {
@@ -268,17 +265,16 @@ module.exports = app => {
                     if (nextAudit) {
                         await transaction.update(this.tableName, {id: nextAudit.id, status: auditConst.status.checking, begin_time: time});
 
-                        // 添加短信通知-需要审批提醒功能
-                        const smsUser = await this.ctx.service.projectAccount.getDataById(nextAudit.audit_id);
-                        if (smsUser.auth_mobile !== undefined && smsUser.sms_type !== '') {
-                            const smsType = JSON.parse(smsUser.sms_type);
-                            if (smsType[smsTypeConst.const.TZ] !== undefined && smsType[smsTypeConst.const.TZ].indexOf(smsTypeConst.judge.approval.toString()) !== -1) {
-                                const sms = new SMS(this.ctx);
-                                const tenderName = await sms.contentChange(this.ctx.tender.data.name);
-                                const content = '【纵横计量支付】' + tenderName + '台帐修订需要您审批。';
-                                sms.send(smsUser.auth_mobile, content);
-                            }
-                        }
+                        // 短信通知-需要审批提醒功能
+                        // 下一人
+                        await this.ctx.helper.sendUserSms(nextAudit.user_id, smsTypeConst.const.XD,
+                            smsTypeConst.judge.approval.toString(), '台帐修订需要您审批,请登录系统处理。');
+                        // 其他参与人
+                        const auditList = await this.getAuditors(revise.tid, times);
+                        const users = this._.pull(this._.map(auditList, 'user_id'), audit.id);
+                        users.push(revise.uid);
+                        await this.ctx.helper.sendUserSms(users, smsTypeConst.const.XD,
+                            smsTypeConst.judge.result.toString(), '台账修订审批通过。');
                     } else {
                         // 同步修订信息
                         await transaction.update(this.ctx.service.ledgerRevise.tableName, {id: revise.id, status: checkType, end_time: time});
@@ -289,17 +285,16 @@ module.exports = app => {
                             id: revise.tid, total_price: sum.total_price, deal_tp: sum.deal_tp
                         });
 
-                        // 添加短信通知-审批通过提醒功能
-                        const smsUser = await this.ctx.service.projectAccount.getDataById(revise.uid);
-                        if (smsUser.auth_mobile !== undefined && smsUser.sms_type !== '') {
-                            const smsType = JSON.parse(smsUser.sms_type);
-                            if (smsType[smsTypeConst.const.TZ] !== undefined && smsType[smsTypeConst.const.TZ].indexOf(smsTypeConst.judge.result.toString()) !== -1) {
-                                const sms = new SMS(this.ctx);
-                                const tenderName = await sms.contentChange(this.ctx.tender.data.name);
-                                const content = '【纵横计量支付】' + tenderName + '台账修订审批通过,请登录系统处理。';
-                                sms.send(smsUser.auth_mobile, content);
-                            }
-                        }
+                        // 短信通知-审批通过提醒功能
+                        // 下一人
+                        const msg = '台账修订审批通过,请登录系统处理。';
+                        await this.ctx.helper.sendUserSms(revise.uid, smsTypeConst.const.XD,
+                            smsTypeConst.judge.result.toString(), msg);
+                        // 其他参与人
+                        const auditList = await this.getAuditors(revise.tid, times);
+                        const users = this._.pull(this._.map(auditList, 'user_id'), audit.id);
+                        await this.ctx.helper.sendUserSms(users, smsTypeConst.const.XD,
+                            smsTypeConst.judge.result.toString(), '台账修订审批通过。');
                     }
                 } else {
                     // 同步修订信息
@@ -316,17 +311,13 @@ module.exports = app => {
                     }
                     await transaction.insert(this.tableName, auditors);
 
-                    // 添加短信通知-审批退回提醒功能
-                    const smsUser = await this.ctx.service.projectAccount.getDataById(revise.uid);
-                    if (smsUser.auth_mobile !== undefined && smsUser.sms_type !== '') {
-                        const smsType = JSON.parse(smsUser.sms_type);
-                        if (smsType[smsTypeConst.const.TZ] !== undefined && smsType[smsTypeConst.const.TZ].indexOf(smsTypeConst.judge.result.toString()) !== -1) {
-                            const sms = new SMS(this.ctx);
-                            const tenderName = await sms.contentChange(this.ctx.tender.data.name);
-                            const content = '【纵横计量支付】' + tenderName + '台账修订审批退回,请登录系统处理。';
-                            sms.send(smsUser.auth_mobile, content);
-                        }
-                    }
+                    // 短信通知-审批退回提醒功能
+                    // 下一人
+                    await this.ctx.helper.sendUserSms(revise.uid, smsTypeConst.const.XD,
+                        smsTypeConst.judge.result.toString(), '台账修订审批退回,请登录系统处理。');
+                    // 其他参与人
+                    await this.ctx.helper.sendUserSms(this._.map(auditors, 'user_id'),
+                        smsTypeConst.const.XD, smsTypeConst.judge.result.toString(), '台账修订审批退回。');
                 }
 
                 await transaction.commit();

+ 1 - 0
app/service/stage.js

@@ -335,6 +335,7 @@ module.exports = app => {
                     }
                 }
                 await transaction.delete(this.ctx.service.stagePay.tableName, { sid: id });
+                await transaction.delete(this.ctx.service.pay.tableName, { csid: id });
                 // 删除计量附件文件
                 const attList = await this.ctx.service.stageAtt.getAllDataByCondition({ where: { sid: id } });
                 if (attList.length !== 0) {

+ 1 - 1
app/service/stage_audit.js

@@ -602,7 +602,7 @@ module.exports = app => {
             const time = new Date();
             // 整理当前流程审核人状态更新
             const audit = (await this.getAllDataByCondition({ where: { sid: stageId, times }, orders: [['order', 'desc']], limit: 1, offset: 0 }))[0];
-            if (!audit || audit.order <= 1) {
+            if (!audit || audit.order < 1) {
                 throw '审核数据错误';
             }
             const transaction = await this.db.beginTransaction();

+ 4 - 10
app/service/stage_bills.js

@@ -114,7 +114,7 @@ module.exports = app => {
             }
         }
 
-        async getUsedStageBills(tid, sid) {
+        async getStageUsedBills(tid, sid) {
             const sql = 'SELECT Bills.lid, (Bills.contract_qty <> 0 and Bills.qc_qty <> 0) As used FROM ' + this.tableName + ' As Bills ' +
                 '  INNER JOIN ( ' +
                 '    SELECT MAX(`times` * ' + timesLen + ' + `order`) As `progress`, `lid`, `sid` From ' + this.tableName +
@@ -124,7 +124,7 @@ module.exports = app => {
                 '  ON (Bills.times * ' + timesLen + ' + `order`) = MaxFilter.progress And Bills.lid = MaxFilter.lid And Bills.`sid` = MaxFilter.`sid`';
             const sqlParam = [tid, sid];
             const stageBills = await this.db.query(sql, sqlParam);
-            return this._.map(this._.filter(stageBills, 'used'), pid);
+            return this._.map(this._.filter(stageBills, 'used'), 'lid');
         }
 
         /**
@@ -197,11 +197,11 @@ module.exports = app => {
             }
 
             const precision = this.ctx.helper.findPrecision(this.ctx.tender.info.precision, ledgerData.unit);
-            if (insertData.contract_qty) {
+            if (insertData.contract_qty !== undefined) {
                 d.contract_qty = this.round(insertData.contract_qty, precision.value);
                 d.contract_tp = this.ctx.helper.mul(d.contract_qty, ledgerData.unit_price, info.decimal.tp);
             }
-            if (insertData.qc_qty) {
+            if (insertData.qc_qty !== undefined) {
                 d.qc_qty = this.round(insertData.qc_qty, precision.value);
                 d.qc_tp = this.ctx.helper.mul(d.qc_qty, ledgerData.unit_price, info.decimal.tp);
             }
@@ -235,12 +235,6 @@ module.exports = app => {
                     }
                     if (!stageBills || stageBills.times !== this.ctx.stage.curTimes || stageBills.order !== this.ctx.stage.curOrder) {
                         await this._insertStageBillsData(transaction, d, stageBills, ledgerBills);
-                        // d.tid = this.ctx.tender.id;
-                        // d.sid = this.ctx.stage.id;
-                        // d.said = this.ctx.session.sessionUser.accountId;
-                        // d.times = this.ctx.stage.curTimes;
-                        // d.order = this.ctx.stage.curOrder;
-                        // await transaction.insert(this.tableName, d);
                     } else {
                         d.id = stageBills.id;
                         await transaction.update(this.tableName, d);

+ 33 - 3
app/service/stage_bills_final.js

@@ -85,8 +85,8 @@ module.exports = app => {
             }
             if (stage.order > 1) {
                 const cur = await this.ctx.service.stageBills.getLastestStageData(tender.id, stage.id);
-                if (!cur || cur.length === 0) return;
                 const pre = await this.getFinalData(tender, stage.order - 1);
+                if ((!cur || cur.length === 0) && (!pre || pre.length === 0)) return;
                 for (const c of cur) {
                     delete c.id;
                     delete c.said;
@@ -109,13 +109,13 @@ module.exports = app => {
                     }
                 }
 
-                //await transaction.insert(this.tableName, cur);
                 for (const p of pre) {
+                    if (p.id !== undefined) delete p.id;
                     p.sid = stage.id;
                     p.sorder = stage.order;
                     p.used = p.used || !this.ctx.helper.checkZero(p.contract_qty) || !this.ctx.helper.checkZero(p.qc_qty);
                 }
-                await transaction.insert(this.tableName, cur.concat(pre));
+                await transaction.insert(this.tableName, cur ? cur.concat(pre) : pre);
             } else {
                 const sql = 'Insert Into ??(tid, sid, lid, sorder, contract_qty, contract_tp, qc_qty, qc_tp, used)' +
                     '  SELECT b.tid, b.sid, b.lid, ? As `sorder`, b.contract_qty, b.contract_tp, b.qc_qty, b.qc_tp,' +
@@ -145,6 +145,36 @@ module.exports = app => {
             }
             await transaction.delete(this.tableName, { tid: tender.id, sid: stage.id });
         }
+
+        async getSumTotalPrice(tenderId, stageOrder) {
+            const sql = 'SELECT Sum(`contract_tp`) As `contract_tp`, Sum(`qc_tp`) As `qc_tp` FROM ' + this.tableName +
+                '  WHERE sorder = ? and tid = ?';
+            const sqlParam = [stageOrder, tenderId];
+            const result = await this.db.queryOne(sql, sqlParam);
+            return result;
+        }
+
+        async getSumTotalPriceFilter(tenderId, stageOrder, operate, filter) {
+            const sql = 'SELECT Sum(`contract_tp`) As `contract_tp`, Sum(`qc_tp`) As `qc_tp`' +
+                '  FROM ' + this.tableName + ' As Bills ' +
+                '  LEFT JOIN ' + this.ctx.service.ledger.tableName + ' As Ledger ' +
+                '  ON Bills.lid = Ledger.id And Bills.sorder = ? And Bills.tid = ? And Ledger.b_code ' + operate + ' ?';
+            const sqlParam = [stageOrder, tenderId, filter];
+            const result = await this.db.queryOne(sql, sqlParam);
+            return result;
+        }
+
+        async getSumTotalPriceGcl(tenderId, stageOrder, regText) {
+            if (regText) {
+                return await this.getSumTotalPriceFilter(tenderId, stageOrder, 'REGEXP', regText);
+            } else {
+                return await this.getSumTotalPriceFilter(tenderId, stageOrder, '<>', this.db.escape(''));
+            }
+        }
+
+        async getSumTotalPriceNotGcl(tenderId, stageOrder) {
+            return await this.getSumTotalPriceFilter(tenderId, stageOrder, '=', this.db.escape(''));
+        }
     }
 
     return StageBillsFinal;

+ 1 - 1
app/service/stage_change.js

@@ -304,7 +304,7 @@ module.exports = app => {
          * @returns {Promise<void>}
          */
         async getStageUsedChangeId(sid) {
-            const sql = 'SELECT c.`cid` FROM ' + this.tableName + ' As c' +
+            const sql = 'SELECT c.`cid`, sum(qty) As qty FROM ' + this.tableName + ' As c' +
                         '  INNER JOIN (' +
                         '    SELECT MAX(`stimes` * ' + timesLen + ' + `sorder`) As `flow`, `lid`, `pid`, `cbid` From ' + this.tableName +
                         '      WHERE sid = ?' +

+ 19 - 14
app/service/stage_pay.js

@@ -86,12 +86,6 @@ module.exports = app => {
                 pays = await transaction.select(this.ctx.service.pay.tableName, {where: { tid: this.ctx.tender.id } });
             }
             const stagePays = [];
-            for (const p of pays) {
-                stagePays.push({
-                    tid: p.tid, sid: stage.id, pid: p.id,
-                    stimes: stage.times, sorder: 0, expr: p.expr, name: p.name,
-                });
-            }
             // 获取截止上期数据
             if (stage.order > 1) {
                 const preStage = await this.ctx.service.stage.getDataByCondition({tid: stage.tid, order: stage.order - 1});
@@ -99,14 +93,25 @@ module.exports = app => {
                     throw '标段数据有误';
                 }
                 const prePays = await this.getStageLastestPays(preStage.id);
+                console.log(prePays.length);
                 for (const pp of prePays) {
-                    const sp = this._.find(stagePays, {pid: pp.pid});
-                    sp.name = pp.name;
-                    sp.expr = pp.expr;
-                    sp.pre_tp = pp.end_tp;
-                    sp.pause = pp.pause;
-                    sp.pre_used = pp.pre_used || !this.ctx.helper.checkZero(pp.tp);
-                    sp.pre_finish = pp.rprice ? pp.end_tp === pp.rprice : false;
+                    const p = this._.find(pays, {id: pp.pid});
+                    stagePays.push({
+                        tid: p.tid, sid: stage.id, pid: p.id,
+                        stimes: stage.times, sorder: 0,
+                        name: pp.name, expr: pp.expr, pause: pp.pause,
+                        pre_tp: pp.end_tp,
+                        pre_used: pp.pre_used || !this.ctx.helper.checkZero(pp.tp),
+                        pre_finish: pp.pre_finish || (pp.rprice ? pp.end_tp === pp.rprice : false),
+                    });
+                }
+                console.log(stagePays.length);
+            } else {
+                for (const p of pays) {
+                    stagePays.push({
+                        tid: p.tid, sid: stage.id, pid: p.id,
+                        stimes: stage.times, sorder: 0, expr: p.expr, name: p.name,
+                    });
                 }
             }
             let result;
@@ -115,7 +120,7 @@ module.exports = app => {
             } else {
                 result = await this.db.insert(this.tableName, stagePays);
             }
-            return result.affectedRows === pays.length;
+            return result.affectedRows === stagePays.length;
         }
 
         async getStagePay(stage, pid) {

+ 27 - 12
app/service/stage_pos.js

@@ -125,15 +125,16 @@ module.exports = app => {
         }
 
         async getStageUsedPos(tid, sid, where) {
+            const self = this;
             const stagePos = await this.getLastestStageData2(tid, sid, where);
             const pids = this._.map(stagePos, function (sp) {
-                if (this.ctx.helper.checkZero(sp.contract_qty) && this.ctx.helper.checkZero(sp.qc_qty)) {
+                if (self.ctx.helper.checkZero(sp.contract_qty) && self.ctx.helper.checkZero(sp.qc_qty)) {
                     return sp.pid;
                 } else {
                     return -1;
                 }
             });
-            return this._.full(pids, -1);
+            return this._.pull(pids, -1);
         }
 
         /**
@@ -254,19 +255,33 @@ module.exports = app => {
                     }
 
                     if (d.contract_qty !== undefined || d.qc_qty !== undefined || d.postil !== undefined) {
-                        const sp = {pid: d.pid, lid: d.lid, contract_qty: d.contract_qty, qc_qty: d.qc_qty, postil: d.postil};
                         const osp = this._.find(orgStagePos, function (p) { return p.pid === d.pid; });
-                        if (precision) {
-                            this.ctx.helper.checkFieldPrecision(sp, this.qtyFields, precision.value);
-                        }
                         if (osp && osp.times === this.ctx.stage.curTimes && osp.order === this.ctx.stage.curOrder) {
-                            await transaction.update(this.tableName, d, {where: {id: osp.id}});
+                            const sp = {};
+                            if (d.contract_qty !== undefined) {
+                                sp.contract_qty = this.ctx.helper.round(d.contract_qty, precision.value);
+                            }
+                            if (d.qc_qty !== undefined) {
+                                sp.qc_qty = this.ctx.helper.round(d.qc_qty, precision.value);
+                            }
+                            if (d.postil !== undefined) {
+                                sp.postil = this.ctx.helper.round(d.postil, precision.value);
+                            }
+                            await transaction.update(this.tableName, sp, {where: {id: osp.id}});
                         } else {
-                            sp.tid = this.ctx.tender.id;
-                            sp.sid = this.ctx.stage.id;
-                            sp.said = this.ctx.session.sessionUser.accountId;
-                            sp.times = this.ctx.stage.curTimes;
-                            sp.order = this.ctx.stage.curOrder;
+                            const sp = {
+                                pid: d.pid, lid: d.lid,
+                                tid: this.ctx.tender.id, sid: this.ctx.stage.id,
+                                said: this.ctx.session.sessionUser.accountId,
+                                times: this.ctx.stage.curTimes, order: this.ctx.stage.curOrder
+                            };
+                            sp.contract_qty = d.contract_qty === undefined && osp
+                                ? osp.contract_qty
+                                : this.ctx.helper.round(d.contract_qty, precision.value);
+                            sp.qc_qty = d.qc_qty === undefined && osp
+                                ? osp.qc_qty
+                                : this.ctx.helper.round(d.qc_qty, precision.value);
+                            sp.postil = d.postil === undefined && osp ? osp.postil : d.postil;
                             await transaction.insert(this.tableName, sp);
                         }
                     }

+ 3 - 3
app/service/stage_pos_final.js

@@ -69,8 +69,8 @@ module.exports = app => {
             }
             if (stage.order > 1) {
                 const cur = await this.ctx.service.stagePos.getLastestStageData2(tender.id, stage.id);
-                if (!cur || cur.length === 0) return;
                 const pre = await this.getFinalData(tender, stage.order - 1);
+                if ((!cur || cur.length === 0) && (!pre || pre.length === 0)) return;
                 for (const c of cur) {
                     delete c.id;
                     delete c.said;
@@ -90,13 +90,13 @@ module.exports = app => {
                         c.used = !this.ctx.helper.checkZero(c.contract_qty) || !this.ctx.helper.checkZero(c.qc_qty);
                     }
                 }
-                //await transaction.insert(this.tableName, cur);
                 for (const p of pre) {
+                    if (p.id !== undefined) delete p.id;
                     p.sid = stage.id;
                     p.sorder = stage.order;
                     p.used = p.used || !this.ctx.helper.checkZero(p.contract_qty) || !this.ctx.helper.checkZero(p.qc_qty);
                 }
-                await transaction.insert(this.tableName, cur.concat(pre));
+                await transaction.insert(this.tableName, cur ? cur.concat(pre) : pre);
             } else {
                 const sql = 'Insert Into ??(tid, sid, lid, pid, sorder, contract_qty, qc_qty, used)' +
                             '  SELECT p.tid, p.sid, p.lid, p.pid, ? As `sorder`, p.contract_qty, p.qc_qty,' +

+ 111 - 0
app/service/tender_info.js

@@ -82,6 +82,117 @@ module.exports = app => {
             }
             await this.db.update(this.tableName, data, {where: {tid: tenderId}});
         }
+
+        async savePrecision(tenderId, newPrecision, oldPrecision, decimal) {
+            const changePrecision = [];
+            const units = await this.ctx.service.ledger.getTenderUsedUnits(tenderId);
+            const defUnits = this._.map(newPrecision, 'units');
+            const otherUnits = units.filter(function (u) {return defUnits.indexOf(u) === -1});
+            let changeUnits = [];
+            for (const prop in newPrecision) {
+                if (oldPrecision[prop]) {
+                    if (newPrecision[prop].value < oldPrecision[prop].value) {
+                        changePrecision.push(newPrecision[prop]);
+                    }
+                } else {
+                    changePrecision.push(newPrecision[prop]);
+                }
+            }
+            for (const cp of changePrecision) {
+                if (cp.unit) {
+                    changeUnits.push(cp.unit);
+                } else {
+                    changeUnits.push(otherUnits);
+                }
+            }
+            changeUnits = this._.flatten(changeUnits);
+
+            if (changeUnits.length > 0) {
+                const bills = await this.ctx.service.ledger.getAllDataByCondition({
+                    columns: ['id', 'unit', 'unit_price', 'sgfh_qty', 'sjcl_qty', 'qtcl_qty', 'deal_qty'],
+                    where: {tender_id: tenderId, unit: changeUnits, is_leaf: true}
+                });
+                const pos = changeUnits.length > 0 ? await this.ctx.service.pos.getPosDataByUnits(tenderId, changeUnits) : [];
+
+                for (const b of bills) {
+                    const precision = this.ctx.helper.findPrecision(newPrecision, b.unit);
+                    const bPos = this._.filter(pos, {lid: b.id});
+                    if (bPos.length > 0) {
+                        let sgfh_qty = 0, sjcl_qty = 0, qtcl_qty = 0, quantity = 0;
+                        for (const p of bPos) {
+                            this.ctx.helper.checkFieldPrecision(p, ['sgfh_qty', 'sjcl_qty', 'qtcl_qty'], precision.value);
+                            p.quantity = this.ctx.helper.add(this.ctx.helper.add(p.sgfh_qty, p.sjcl_qty), p.qtcl_qty);
+                            sgfh_qty = this.ctx.helper.add(sgfh_qty, p.sgfh_qty);
+                            sjcl_qty = this.ctx.helper.add(sjcl_qty, p.sjcl_qty);
+                            qtcl_qty = this.ctx.helper.add(qtcl_qty, p.qtcl_qty);
+                            quantity = this.ctx.helper.add(quantity, p.quantity);
+                        }
+                        b.sgfh_qty = sgfh_qty;
+                        b.sjcl_qty = sjcl_qty;
+                        b.qtcl_qty = qtcl_qty;
+                        b.quantity = quantity;
+                    } else {
+                        this.ctx.helper.checkFieldPrecision(b, ['sgfh_qty', 'sjcl_qty', 'qtcl_qty', 'deal_qty'], precision.value);
+                    }
+                    b.quantity = this.ctx.helper.add(this.ctx.helper.add(b.sgfh_qty, b.sjcl_qty), b.qtcl_qty);
+                    b.sgfh_tp = this.ctx.helper.mul(b.sgfh_qty, b.unit_price, decimal.tp);
+                    b.sjcl_tp = this.ctx.helper.mul(b.sjcl_qty, b.unit_price, decimal.tp);
+                    b.qtcl_tp = this.ctx.helper.mul(b.qtcl_qty, b.unit_price, decimal.tp);
+                    b.deal_tp = this.ctx.helper.mul(b.deal_qty, b.unit_price, decimal.tp);
+                    b.total_price = this.ctx.helper.mul(b.quantity, b.unit_price, decimal.tp);
+                }
+
+                const transaction = await this.db.beginTransaction();
+                try {
+                    await transaction.update(this.tableName,
+                        {precision: JSON.stringify(newPrecision)}, {where: {tid: tenderId}});
+                    if (bills.length > 0) await transaction.updateRows(this.ctx.service.ledger.tableName, bills);
+                    if (pos.length > 0) await transaction.updateRows(this.ctx.service.pos.tableName, pos);
+                    await transaction.commit();
+                } catch (err) {
+                    await transaction.rollback();
+                    throw err;
+                }
+            } else {
+                await this.db.update(this.tableName,
+                    {precision: JSON.stringify(newPrecision)}, {where: {tid: tenderId}});
+            }
+        }
+
+        async saveDecimal(tenderId, newDecimal, oldDecimal) {
+            const changeBills = [], calcUp = newDecimal.up < oldDecimal.up, calcTp = newDecimal.tp !== oldDecimal.tp;
+            if (calcUp || calcTp) {
+                const bills = await this.ctx.service.ledger.getAllDataByCondition({
+                    columns: ['id', 'unit_price', 'sgfh_qty', 'sjcl_qty', 'qtcl_qty', 'deal_qty', 'quantity'],
+                    where: {tender_id: tenderId, is_leaf: true}
+                });
+                for (const b of bills) {
+                    const cb = { id: b.id };
+                    cb.unit_price = calcUp ? this.ctx.helper.round(b.unit_price, newDecimal.up) : b.unit_price;
+                    cb.sgfh_tp = this.ctx.helper.mul(b.sgfh_qty, cb.unit_price, newDecimal.tp);
+                    cb.sjcl_tp = this.ctx.helper.mul(b.sjcl_qty, cb.unit_price, newDecimal.tp);
+                    cb.qtcl_tp = this.ctx.helper.mul(b.qtcl_qty, cb.unit_price, newDecimal.tp);
+                    cb.deal_tp = this.ctx.helper.mul(b.deal_qty, cb.unit_price, newDecimal.tp);
+                    cb.total_price = this.ctx.helper.mul(b.quantity, cb.unit_price, newDecimal.tp);
+                    changeBills.push(cb);
+                }
+            }
+            if (changeBills.length > 0) {
+                const transaction = await this.db.beginTransaction();
+                try {
+                    await transaction.update(this.tableName,
+                        {decimal: JSON.stringify(newDecimal)}, { where: { tid: tenderId }});
+                    await transaction.updateRows(this.ctx.service.ledger.tableName, changeBills);
+                    await transaction.commit();
+                } catch(error) {
+                    await transaction.rollback();
+                    throw error;
+                }
+            } else {
+                await this.db.update(this.tableName,
+                    {decimal: JSON.stringify(newDecimal)}, { where: { tid: tenderId }});
+            }
+        }
     }
 
     return TenderInfo;

+ 1 - 1
app/view/change/index.ejs

@@ -15,7 +15,7 @@
                 </div>
             </div>
             <% if (tender.user_id === uid) { %>
-            <div>
+            <div class="ml-auto">
                 <a href="#add-bj" data-toggle="modal" data-target="#add-bj" class="btn btn-sm btn-primary pull-right">新建变更令</a>
                 <a href="#setting" data-toggle="modal" data-target="#setting" class="btn btn-sm btn-outline-primary pull-right"><i class="fa fa-cog"></i></a>
             </div>

+ 0 - 21
app/view/change/info.ejs

@@ -715,28 +715,7 @@
         </div>
     </div>
 </div>
-<link href="/public/css/toastr.css" rel="stylesheet">
-<script src="/public/js/toastr.min.js"></script>
 <script>
-    $(document).ready(() => {
-        toastr.options = {
-            "closeButton": false,
-            "debug": false,
-            "newestOnTop": false,
-            "progressBar": false,
-            "positionClass": "toast-top-right",
-            "preventDuplicates": false,
-            "onclick": null,
-            "showDuration": "300",
-            "hideDuration": "1000",
-            "timeOut": "5000",
-            "extendedTimeOut": "1000",
-            "showEasing": "swing",
-            "hideEasing": "linear",
-            "showMethod": "fadeIn",
-            "hideMethod": "fadeOut"
-        };
-    })
     let table = '';
     const totalPriceUnit = '<%- tpUnit %>';
     const unitPriceUnit = '<%- upUnit %>';

+ 21 - 4
app/view/layout/layout.ejs

@@ -14,8 +14,8 @@
     <link rel="stylesheet" href="/public/css/spreadjs/sheets/gc.spread.sheets.excelsmartcost.css">
     <link rel="stylesheet" href="/public/css/jquery-contextmenu/jquery.contextMenu.min.css">
     <link rel="stylesheet" href="/public/css/ztree/zTreeStyle.css" type="text/css">
-    <link rel="stylesheet" href="/public/css/datepicker/datepicker.min.css" rel="stylesheet" type="text/css">
-    <link href="/public/css/toastr.css" rel="stylesheet">
+    <link rel="stylesheet" href="/public/css/datepicker/datepicker.min.css" type="text/css">
+    <link rel="stylesheet" href="/public/css/toastr.css">
     <!-- JS. -->
     <% for (const file of jsFiles) { %>
     <script type="text/javascript" src="<%- file %>"></script>
@@ -33,12 +33,29 @@
         <%- content %>
     </div>
 </div>
-<div class="toast" style="position: absolute; z-index: 10000;text-align: center">
+<div class="toast" style="text-align: center">
     <i class="icon fa"></i>
     <span class="message"></span>
 </div>
 <%- modal %>
 <script type="text/javascript">
+    toastr.options = {
+        "closeButton": false,
+        "debug": false,
+        "newestOnTop": false,
+        "progressBar": false,
+        "positionClass": "toast-top-center",
+        "preventDuplicates": false,
+        "onclick": null,
+        "showDuration": "300",
+        "hideDuration": "1000",
+        "timeOut": "5000",
+        "extendedTimeOut": "1000",
+        "showEasing": "swing",
+        "hideEasing": "linear",
+        "showMethod": "fadeIn",
+        "hideMethod": "fadeOut"
+    };
     let toastInfo = '<%- message %>';
     try {
         toastInfo = toastInfo !== '' && toastInfo !== 'null' ? JSON.parse(toastInfo) : '';
@@ -47,7 +64,7 @@
     }
 
     if (toastInfo !== '') {
-        toast(toastInfo.message, toastInfo.type, toastInfo.icon);
+        toastr[toastInfo.type](toastInfo.message);
     }
     let user = '<%= ctx.session.sessionUser.name %>';
 </script>

+ 0 - 2
app/view/ledger/explode.ejs

@@ -150,8 +150,6 @@
     const tender = JSON.parse('<%- JSON.stringify(tender) %>');
     const tenderInfo = JSON.parse('<%- JSON.stringify(tenderInfo) %>');
     const measureType = JSON.parse('<%- JSON.stringify(measureType) %>');
-    let ledger = '<%- ledger %>';
-    ledger = JSON.parse(ledger);
     let ledgerSpreadSetting = '<%- ledgerSpreadSetting %>';
     ledgerSpreadSetting = JSON.parse(ledgerSpreadSetting);
     let posSpreadSetting = JSON.parse('<%- posSpreadSetting %>');

+ 21 - 0
app/view/material/audit_btn.ejs

@@ -0,0 +1,21 @@
+<div class="contarl-box">
+    <% if (ctx.material.status === auditConst.status.uncheck) { %>
+        <% if (ctx.session.sessionUser.accountId === ctx.material.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 if (ctx.material.status === auditConst.status.checking) { %>
+        <% if (ctx.material.curAuditor && ctx.material.curAuditor.aid === ctx.session.sessionUser.accountId) { %>
+            <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>
+        <% } %>
+    <% } else if (ctx.material.status === auditConst.status.checked) { %>
+        <a href="#sp-list" data-toggle="modal" data-target="#sp-list" class="btn btn-outline-secondary btn-sm btn-block">审批完成</a>
+    <% } else if (ctx.material.status === auditConst.status.checkNo) { %>
+        <a href="#sp-list" data-toggle="modal" data-target="#sp-list" class="btn btn-outline-warning btn-sm btn-block text-muted">审批退回</a>
+        <% if (ctx.session.sessionUser.accountId === ctx.material.user_id) { %>
+            <a id="sp-list2-btn" href="javascript: void(0);" data-toggle="modal" data-target="#sp-list2" class="btn btn-primary btn-sm btn-block">重新上报</a>
+        <% } %>
+    <% } %>
+</div>

+ 628 - 0
app/view/material/audit_modal.ejs

@@ -0,0 +1,628 @@
+<% if ((ctx.material.status === auditConst.status.uncheck || ctx.material.status === auditConst.status.checkNo) && ctx.session.sessionUser.accountId === ctx.material.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="form-group">
+                    <label>选择审批人</label>
+                    <div class="input-group">
+                        <div class="input-group-prepend">
+                            <select class="form-control" id="account_group">
+                                <option value="0">所有分组</option>
+                                <% for (const dw in accountGroup) { %>
+                                    <option value="<%= dw %>"><%= accountGroup[dw] %></option>
+                                <% } %>
+                            </select>
+                        </div>
+                        <select class="form-control" id="account_list">
+                            <option value="0">选择审批人</option>
+                            <% for (const account of accountList) { %>
+                                <option value="<%= account.id %>"><%= account.name %><% if (account.role !== '') { %>(<%= account.role %>)<% } %><% if (account.company !== '') { %> -<%= account.company %><% } %></option>
+                            <% } %>
+                        </select>
+                    </div>
+                </div>
+                <div class="card mt-3">
+                    <div class="card-header">
+                        审批流程
+                    </div>
+                    <ul class="list-group list-group-flush" id="auditors">
+                        <% for (let i = 0, iLen = ctx.material.auditorList.length; i < iLen; i++) { %>
+                        <li class="list-group-item" auditorId="<%- ctx.material.auditorList[i].aid %>">
+                            <a href="javascript: void(0)" class="text-danger pull-right">移除</a>
+                            <%- ctx.material.auditorList[i].order %> <%- ctx.material.auditorList[i].name %>
+                            <small class="text-muted"><%- ctx.material.auditorList[i].role %></small>
+                        </li>
+                        <% } %>
+                    </ul>
+                </div>
+            </div>
+            <form class="modal-footer" method="post" action="<%- preUrl %>/audit/start" onsubmit="return checkAuditorFrom()">
+                <button type="button" class="btn btn-secondary" data-dismiss="modal">关闭</button>
+                <input type="hidden" name="_csrf" value="<%= ctx.csrf %>">
+                <button class="btn btn-primary" type="submit">确认上报</button>
+            </form>
+        </div>
+    </div>
+</div>
+<% } %>
+<% if (ctx.material.status === auditConst.status.checking) { %>
+    <% if (ctx.material.curAuditor && ctx.material.curAuditor.aid === ctx.session.sessionUser.accountId) { %>
+        <!--审批通过-->
+        <div class="modal fade" id="sp-done" data-backdrop="static">
+    <div class="modal-dialog modal-lg" role="document">
+        <form class="modal-content" action="<%- preUrl %>/audit/check" method="post">
+            <div class="modal-header">
+                <h5 class="modal-title">审批通过</h5>
+            </div>
+            <div class="modal-body">
+                <div class="row">
+                    <div class="col-4">
+                        <div class="card mt-3">
+                            <ul class="list-group list-group-flush">
+                                <li class="list-group-item">
+                                    <i class="fa fa fa-play-circle fa-rotate-90"></i> <%- ctx.material.user.name %>  <small class="text-muted"><%- ctx.material.user.role %></small>
+                                </li>
+                                <% for (let i = 0; i < ctx.material.auditors2.length; i++) { %>
+                                <li class="list-group-item">
+                                    <% if (i < ctx.material.auditors2.length - 1) { %>
+                                    <i class="fa fa-chevron-circle-down"></i> <%- ctx.material.auditors2[i].name %>  <small class="text-muted"><%- ctx.material.auditors2[i].role %></small>
+                                    <% } else { %>
+                                    <i class="fa fa fa-stop-circle"></i> <%- ctx.material.auditors2[i].name %>  <small class="text-muted"><%- ctx.material.auditors2[i].role %></small>
+                                    <% } %>
+                                </li>
+                                <% } %>
+                            </ul>
+                        </div>
+                    </div>
+                    <div class="col-8 modal-height-500" style="overflow: auto">
+                        <div class="card mt-3">
+                            <ul class="list-group list-group-flush">
+                                <li class="list-group-item">
+                                    <span class="text-success pull-right"><small><%- ctx.material.auditors[0].begin_time.toLocaleDateString() %></small> 上报</span>
+                                    <h5 class="card-title"><i class="fa fa-play-circle fa-rotate-90 text-success"></i> <%- ctx.material.user.name %> <small class="text-muted"><%- ctx.material.user.role %></small></h5>
+                                </li>
+                                <% for (let iA = 0; iA < ctx.material.auditors.length; iA++) { %>
+                                <% const auditors = ctx.material.auditors; %>
+                                <li class="list-group-item">
+                                    <% if (auditors[iA].status === auditConst.status.checked) { %>
+                                    <span class="text-success pull-right"><small><%- auditors[iA].end_time.toLocaleString() %></small> 审批通过</span>
+                                    <h5 class="card-title">
+                                        <i class="<%- (iA < auditors.length - 1 ? 'fa fa-chevron-circle-down text-success' : 'fa fa-stop-circle text-success') %>"></i> <%- auditors[iA].name %> <small class="text-muted"><%- auditors[iA].role %></small>
+                                    </h5>
+                                    <% } else if (auditors[iA].stauts == auditConst.status.checking) { %>
+                                    <span class="pull-right">审批中</span>
+                                    <h5 class="card-title">
+                                        <i class="<%- (iA < auditors.length - 1 ? 'fa fa-chevron-circle-down' : 'fa fa-stop-circle') %>"></i> <%- auditors[iA].name %> <small class="text-muted"><%- auditors[iA].role %></small>
+                                    </h5>
+                                    <% } else { %>
+                                    <h5 class="card-title">
+                                        <i class="<%- (iA < auditors.length - 1 ? 'fa fa-chevron-circle-down' : 'fa fa-stop-circle') %>"></i> <%- auditors[iA].name %> <small class="text-muted"><%- auditors[iA].role %></small>
+                                    </h5>
+                                    <% } %>
+                                    <% if (auditors[iA].status === auditConst.status.checked) { %>
+                                    <p class="card-text"><%- auditors[iA].opinion %></p>
+                                    <% } else if (auditors[iA].status === auditConst.status.checking) { %>
+                                    <div class="form-group">
+                                        <label>审批意见<b class="text-danger">*</b></label>
+                                        <textarea class="form-control" name="opinion">同意</textarea>
+                                    </div>
+                                    <% } %>
+                                </li>
+                                <% } %>
+                            </ul>
+                        </div>
+                    </div>
+                </div>
+            </div>
+            <div class="modal-footer">
+                <button type="button" class="btn btn-secondary" data-dismiss="modal">关闭</button>
+                <input type="hidden" name="_csrf" value="<%= ctx.csrf %>" />
+                <input type="hidden" name="checkType" value="<%= auditConst.status.checked %>" />
+                <button type="submit" class="btn btn-success" >确认通过</button>
+            </div>
+        </form>
+    </div>
+</div>
+        <!--审批退回-->
+        <div class="modal fade" id="sp-back" data-backdrop="static">
+    <div class="modal-dialog modal-lg" role="document">
+        <form class="modal-content modal-lg" action="<%- preUrl %>/audit/check" method="post">
+            <div class="modal-header">
+                <h5 class="modal-title">审批退回</h5>
+            </div>
+            <div class="modal-body">
+                <div class="row">
+                    <div class="col-4">
+                        <div class="card mt-3">
+                            <ul class="list-group list-group-flush">
+                                <li class="list-group-item">
+                                    <i class="fa fa fa-play-circle fa-rotate-90"></i> <%- ctx.material.user.name %>  <small class="text-muted"><%- ctx.material.user.role %></small>
+                                </li>
+                                <% for (let i = 0; i < ctx.material.auditors2.length; i++) { %>
+                                <li class="list-group-item">
+                                    <% if (i < ctx.material.auditors2.length - 1) { %>
+                                    <i class="fa fa-chevron-circle-down"></i> <%- ctx.material.auditors2[i].name %>  <small class="text-muted"><%- ctx.material.auditors2[i].role %></small>
+                                    <% } else { %>
+                                    <i class="fa fa fa-stop-circle"></i> <%- ctx.material.auditors2[i].name %>  <small class="text-muted"><%- ctx.material.auditors2[i].role %></small>
+                                    <% } %>
+                                </li>
+                                <% } %>
+                            </ul>
+                        </div>
+                    </div>
+                    <div class="col-8 modal-height-500" style="overflow: auto">
+                        <div class="card mt-3">
+                            <ul class="list-group list-group-flush">
+                                <li class="list-group-item">
+                                    <span class="text-success pull-right"><small><%- ctx.material.auditors[0].begin_time.toLocaleDateString() %></small> 上报</span>
+                                    <h5 class="card-title"><i class="fa fa-play-circle fa-rotate-90 text-success"></i> <%- ctx.material.user.name %> <small class="text-muted"><%- ctx.material.user.role %></small></h5>
+                                </li>
+                                <% for (let iA = 0; iA < ctx.material.auditors.length; iA++) { %>
+                                <% const auditors = ctx.material.auditors; %>
+                                <li class="list-group-item">
+                                    <% if (auditors[iA].status === auditConst.status.checked) { %>
+                                    <span class="text-success pull-right"><small><%- auditors[iA].end_time.toLocaleString() %></small> 审批通过</span>
+                                    <h5 class="card-title">
+                                        <i class="<%- (iA < auditors.length - 1 ? 'fa fa-chevron-circle-down text-success' : 'fa fa-stop-circle text-success') %>"></i> <%- auditors[iA].name %> <small class="text-muted"><%- auditors[iA].role %></small>
+                                    </h5>
+                                    <% } else if (auditors[iA].stauts == auditConst.status.checking) { %>
+                                    <span class="pull-right">审批中</span>
+                                    <h5 class="card-title">
+                                        <i class="<%- (iA < auditors.length - 1 ? 'fa fa-chevron-circle-down' : 'fa fa-stop-circle') %>"></i> <%- auditors[iA].name %> <small class="text-muted"><%- auditors[iA].role %></small>
+                                    </h5>
+                                    <% } else { %>
+                                    <h5 class="card-title">
+                                        <i class="<%- (iA < auditors.length - 1 ? 'fa fa-chevron-circle-down' : 'fa fa-stop-circle') %>"></i> <%- auditors[iA].name %> <small class="text-muted"><%- auditors[iA].role %></small>
+                                    </h5>
+                                    <% } %>
+                                    <% if (auditors[iA].status === auditConst.status.checked) { %>
+                                    <p class="card-text"><%- auditors[iA].opinion %></p>
+                                    <% } else if (auditors[iA].status === auditConst.status.checking) { %>
+                                    <div class="form-group">
+                                        <label>审批意见<b class="text-danger">*</b></label>
+                                        <textarea class="form-control" name="opinion">不同意</textarea>
+                                    </div>
+                                    <% } %>
+                                </li>
+                                <% } %>
+                            </ul>
+                        </div>
+                    </div>
+                </div>
+            </div>
+            <div class="modal-footer">
+                <button type="button" class="btn btn-secondary" data-dismiss="modal">关闭</button>
+                <input type="hidden" name="_csrf" value="<%= ctx.csrf %>" />
+                <input type="hidden" name="checkType" value="<%= auditConst.status.checked %>" />
+                <button type="submit" class="btn btn-warning" >确认退回</button>
+            </div>
+        </form>
+    </div>
+</div>
+    <% } else { %>
+        <!--审批流程/结果-->
+        <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">
+                        <div class="card mt-3">
+                            <ul class="list-group list-group-flush">
+                                <li class="list-group-item">
+                                    <i class="fa fa fa-play-circle fa-rotate-90"></i> <%- ctx.material.user.name %>  <small class="text-muted"><%- ctx.material.user.role %></small>
+                                </li>
+                                <% for (let i = 0; i < ctx.material.auditors2.length; i++) { %>
+                                <li class="list-group-item">
+                                    <% if (i < ctx.material.auditors2.length - 1) { %>
+                                    <i class="fa fa-chevron-circle-down"></i> <%- ctx.material.auditors2[i].name %>  <small class="text-muted"><%- ctx.material.auditors2[i].role %></small>
+                                    <% } else { %>
+                                    <i class="fa fa fa-stop-circle"></i> <%- ctx.material.auditors2[i].name %>  <small class="text-muted"><%- ctx.material.auditors2[i].role %></small>
+                                    <% } %>
+                                </li>
+                                <% } %>
+                            </ul>
+                        </div>
+                    </div>
+                    <div class="col-8 modal-height-500" style="overflow: auto">
+                        <% for (const ah of ctx.material.auditHistory) { %>
+                        <div class="card mt-3">
+                            <ul class="list-group list-group-flush">
+                                <% for (let iA = 0; iA < ah.length; iA++) { %>
+                                <% if (iA === 0) { %>
+                                <li class="list-group-item">
+                                    <span class="text-success pull-right"><% if (ctx.material.auditHistory.indexOf(ah) > 0) { %>重新<% } %>上报</span>
+                                    <h5 class="card-title"><i class="fa fa-play-circle fa-rotate-90 text-success"></i> <%- ctx.material.user.name %> <small class="text-muted"><%- ctx.material.user.role %></small></h5>
+                                    <p class="card-text"><small class="text-muted"><%- ah[iA].begin_time.toLocaleDateString() %></small></p>
+                                </li>
+                                <li class="list-group-item">
+                                    <% if (ah[iA].status !== auditConst.status.uncheck) { %>
+                                    <span class="<%- auditConst.statusClass[ah[iA].status] %> pull-right"><%- auditConst.statusString[ah[iA].status]%><% if (ah[iA].status === auditConst.status.checkNo) { %> <%- ctx.material.user.name %><% } %></span>
+                                    <% } %>
+                                    <h5 class="card-title"><i class="fa <%if (iA === ah.length - 1) { %>fa-stop-circle<% } else { %>fa-chevron-circle-down<% } %> <%- auditConst.statusClass[ah[iA].status] %>"></i> <%- ah[iA].name %> <small class="text-muted"><%- ah[iA].role %></small></h5>
+                                    <% if (ah[iA].status === auditConst.status.checked || ah[iA].status === auditConst.status.checkNo) { %>
+                                    <p class="card-text mb-1"><%- ah[iA].opinion %></p>
+                                    <p class="card-text"><small class="text-muted"><%- ah[iA].end_time.toLocaleDateString() %></small></p>
+                                    <% } %>
+                                </li>
+                                <% } else if (iA === ah.length - 1) { %>
+                                <li class="list-group-item">
+                                    <% if (ah[iA].status !== auditConst.status.uncheck) { %>
+                                    <span class="<%- auditConst.statusClass[ah[iA].status] %> pull-right"><%- auditConst.statusString[ah[iA].status]%><% if (ah[iA].status === auditConst.status.checkNo) { %> <%- ctx.material.user.name %><% } %></span>
+                                    <% } %>
+                                    <h5 class="card-title"><i class="fa fa-stop-circle <%- auditConst.statusClass[ah[iA].status] %>"></i> <%- ah[iA].name %> <small class="text-muted"><%- ah[iA].role %></small></h5>
+                                    <% if (ah[iA].status === auditConst.status.checked || ah[iA].status === auditConst.status.checkNo) { %>
+                                    <p class="card-text mb-1"><%- ah[iA].opinion %></p>
+                                    <p class="card-text"><small class="text-muted"><%- ah[iA].end_time.toLocaleDateString() %></small></p>
+                                    <% } %>
+                                </li>
+                                <% } else { %>
+                                <li class="list-group-item">
+                                    <% if (ah[iA].status !== auditConst.status.uncheck) { %>
+                                    <span class="<%- auditConst.statusClass[ah[iA].status] %> pull-right"><%- auditConst.statusString[ah[iA].status]%><% if (ah[iA].status === auditConst.status.checkNo) { %> <%- ctx.material.user.name %><% } %></span>
+                                    <% } %>
+                                    <h5 class="card-title"><i class="fa fa-chevron-circle-down <%- auditConst.statusClass[ah[iA].status] %>"></i> <%- ah[iA].name %> <small class="text-muted"><%- ah[iA].role %></small></h5>
+                                    <% if (ah[iA].status === auditConst.status.checked || ah[iA].status === auditConst.status.checkNo) { %>
+                                    <p class="card-text mb-1"><%- ah[iA].opinion %></p>
+                                    <p class="card-text"><small class="text-muted"><%- ah[iA].end_time.toLocaleDateString() %></small></p>
+                                    <% } %>
+                                </li>
+                                <% } %>
+                                <% } %>
+                            </ul>
+                        </div>
+                        <% } %>
+                        <% if (ctx.material.status === auditConst.status.checking) {%>
+                        <div class="card mt-3">
+                            <ul class="list-group list-group-flush">
+                                <% const auditors = ctx.material.auditors; %>
+                                <% for (let iA = 0; iA < auditors.length; iA++) { %>
+                                <% if (iA === 0) { %>
+                                <li class="list-group-item">
+                                    <span class="text-success pull-right"><% if (ctx.material.times > 1) { %>重新<% } %>上报</span>
+                                    <h5 class="card-title"><i class="fa fa-play-circle fa-rotate-90 text-success"></i> <%- ctx.material.user.name %> <small class="text-muted"><%- ctx.material.user.role %></small></h5>
+                                    <p class="card-text"><small class="text-muted"><%- auditors[iA].begin_time.toLocaleDateString() %></small></p>
+                                </li>
+                                <li class="list-group-item">
+                                    <% if (auditors[iA].status !== auditConst.status.uncheck) { %>
+                                    <span class="<%- auditConst.statusClass[auditors[iA].status] %> pull-right"><%- auditConst.statusString[auditors[iA].status]%><% if (auditors[iA].status === auditConst.status.checkNo) { %> <%- ctx.material.user.name %><% } %></span>
+                                    <% } %>
+                                    <h5 class="card-title"><i class="fa <%if (iA === auditors.length - 1) { %>fa-stop-circle<% } else { %>fa-chevron-circle-down<% } %> <%- auditConst.statusClass[auditors[iA].status] %>"></i> <%- auditors[iA].name %> <small class="text-muted"><%- auditors[iA].role %></small></h5>
+                                    <% if (auditors[iA].status === auditConst.status.checked || auditors[iA].status === auditConst.status.checkNo) { %>
+                                    <p class="card-text mb-1"><%- auditors[iA].opinion %></p>
+                                    <p class="card-text"><small class="text-muted"><%- auditors[iA].end_time.toLocaleDateString() %></small></p>
+                                    <% } %>
+                                </li>
+                                <% } else if (iA === auditors.length - 1) { %>
+                                <li class="list-group-item">
+                                    <% if (auditors[iA].status !== auditConst.status.uncheck) { %>
+                                    <span class="<%- auditConst.statusClass[auditors[iA].status] %> pull-right"><%- auditConst.statusString[auditors[iA].status]%><% if (auditors[iA].status === auditConst.status.checkNo) { %> <%- ctx.material.user.name %><% } %></span>
+                                    <% } %>
+                                    <h5 class="card-title"><i class="fa fa-stop-circle <%- auditConst.statusClass[auditors[iA].status] %>"></i> <%- auditors[iA].name %> <small class="text-muted"><%- auditors[iA].role %></small></h5>
+                                    <% if (auditors[iA].status === auditConst.status.checked || auditors[iA].status === auditConst.status.checkNo) { %>
+                                    <p class="card-text mb-1"><%- auditors[iA].opinion %></p>
+                                    <p class="card-text"><small class="text-muted"><%- auditors[iA].end_time.toLocaleDateString() %></small></p>
+                                    <% } %>
+                                </li>
+                                <% } else { %>
+                                <li class="list-group-item">
+                                    <% if (auditors[iA].status !== auditConst.status.uncheck) { %>
+                                    <span class="<%- auditConst.statusClass[auditors[iA].status] %> pull-right"><%- auditConst.statusString[auditors[iA].status]%><% if (auditors[iA].status === auditConst.status.checkNo) { %> <%- ctx.material.user.name %><% } %></span>
+                                    <% } %>
+                                    <h5 class="card-title"><i class="fa fa-chevron-circle-down <%- auditConst.statusClass[auditors[iA].status] %>"></i> <%- auditors[iA].name %> <small class="text-muted"><%- auditors[iA].role %></small></h5>
+                                    <% if (auditors[iA].status === auditConst.status.checked || auditors[iA].status === auditConst.status.checkNo) { %>
+                                    <p class="card-text mb-1"><%- auditors[iA].opinion %></p>
+                                    <p class="card-text"><small class="text-muted"><%- auditors[iA].end_time.toLocaleDateString() %></small></p>
+                                    <% } %>
+                                </li>
+                                <% } %>
+                                <% } %>
+                            </ul>
+                        </div>
+                        <% } %>
+                    </div>
+                </div>
+            </div>
+            <div class="modal-footer">
+                <button type="button" class="btn btn-secondary" data-dismiss="modal">关闭</button>
+            </div>
+        </div>
+    </div>
+</div>
+    <% } %>
+<% } else if (ctx.material.status === auditConst.status.checked) { %>
+    <!--审批流程/结果-->
+    <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">
+                        <div class="card mt-3">
+                            <ul class="list-group list-group-flush">
+                                <li class="list-group-item">
+                                    <i class="fa fa fa-play-circle fa-rotate-90"></i> <%- ctx.material.user.name %>  <small class="text-muted"><%- ctx.material.user.role %></small>
+                                </li>
+                                <% for (let i = 0; i < ctx.material.auditors2.length; i++) { %>
+                                <li class="list-group-item">
+                                    <% if (i < ctx.material.auditors2.length - 1) { %>
+                                    <i class="fa fa-chevron-circle-down"></i> <%- ctx.material.auditors2[i].name %>  <small class="text-muted"><%- ctx.material.auditors2[i].role %></small>
+                                    <% } else { %>
+                                    <i class="fa fa fa-stop-circle"></i> <%- ctx.material.auditors2[i].name %>  <small class="text-muted"><%- ctx.material.auditors2[i].role %></small>
+                                    <% } %>
+                                </li>
+                                <% } %>
+                            </ul>
+                        </div>
+                    </div>
+                    <div class="col-8 modal-height-500" style="overflow: auto">
+                        <% for (const ah of ctx.material.auditHistory) { %>
+                        <div class="card mt-3">
+                            <ul class="list-group list-group-flush">
+                                <% for (let iA = 0; iA < ah.length; iA++) { %>
+                                <% if (iA === 0) { %>
+                                <li class="list-group-item">
+                                    <span class="text-success pull-right"><% if (ctx.material.auditHistory.indexOf(ah) > 0) { %>重新<% } %>上报</span>
+                                    <h5 class="card-title"><i class="fa fa-play-circle fa-rotate-90 text-success"></i> <%- ctx.material.user.name %> <small class="text-muted"><%- ctx.material.user.role %></small></h5>
+                                    <p class="card-text"><small class="text-muted"><%- ah[iA].begin_time.toLocaleDateString() %></small></p>
+                                </li>
+                                <li class="list-group-item">
+                                    <% if (ah[iA].status !== auditConst.status.uncheck) { %>
+                                    <span class="<%- auditConst.statusClass[ah[iA].status] %> pull-right"><%- auditConst.statusString[ah[iA].status]%><% if (ah[iA].status === auditConst.status.checkNo) { %> <%- ctx.material.user.name %><% } %></span>
+                                    <% } %>
+                                    <h5 class="card-title"><i class="fa <%if (iA === ah.length - 1) { %>fa-stop-circle<% } else { %>fa-chevron-circle-down<% } %> <%- auditConst.statusClass[ah[iA].status] %>"></i> <%- ah[iA].name %> <small class="text-muted"><%- ah[iA].role %></small></h5>
+                                    <% if (ah[iA].status === auditConst.status.checked || ah[iA].status === auditConst.status.checkNo) { %>
+                                    <p class="card-text mb-1"><%- ah[iA].opinion %></p>
+                                    <p class="card-text"><small class="text-muted"><%- ah[iA].end_time.toLocaleDateString() %></small></p>
+                                    <% } %>
+                                </li>
+                                <% } else if (iA === ah.length - 1) { %>
+                                <li class="list-group-item">
+                                    <% if (ah[iA].status !== auditConst.status.uncheck) { %>
+                                    <span class="<%- auditConst.statusClass[ah[iA].status] %> pull-right"><%- auditConst.statusString[ah[iA].status]%><% if (ah[iA].status === auditConst.status.checkNo) { %> <%- ctx.material.user.name %><% } %></span>
+                                    <% } %>
+                                    <h5 class="card-title"><i class="fa fa-stop-circle <%- auditConst.statusClass[ah[iA].status] %>"></i> <%- ah[iA].name %> <small class="text-muted"><%- ah[iA].role %></small></h5>
+                                    <% if (ah[iA].status === auditConst.status.checked || ah[iA].status === auditConst.status.checkNo) { %>
+                                    <p class="card-text mb-1"><%- ah[iA].opinion %></p>
+                                    <p class="card-text"><small class="text-muted"><%- ah[iA].end_time.toLocaleDateString() %></small></p>
+                                    <% } %>
+                                </li>
+                                <% } else { %>
+                                <li class="list-group-item">
+                                    <% if (ah[iA].status !== auditConst.status.uncheck) { %>
+                                    <span class="<%- auditConst.statusClass[ah[iA].status] %> pull-right"><%- auditConst.statusString[ah[iA].status]%><% if (ah[iA].status === auditConst.status.checkNo) { %> <%- ctx.material.user.name %><% } %></span>
+                                    <% } %>
+                                    <h5 class="card-title"><i class="fa fa-chevron-circle-down <%- auditConst.statusClass[ah[iA].status] %>"></i> <%- ah[iA].name %> <small class="text-muted"><%- ah[iA].role %></small></h5>
+                                    <% if (ah[iA].status === auditConst.status.checked || ah[iA].status === auditConst.status.checkNo) { %>
+                                    <p class="card-text mb-1"><%- ah[iA].opinion %></p>
+                                    <p class="card-text"><small class="text-muted"><%- ah[iA].end_time.toLocaleDateString() %></small></p>
+                                    <% } %>
+                                </li>
+                                <% } %>
+                                <% } %>
+                            </ul>
+                        </div>
+                        <% } %>
+                    </div>
+                </div>
+            </div>
+            <div class="modal-footer">
+                <button type="button" class="btn btn-secondary" data-dismiss="modal">关闭</button>
+            </div>
+        </div>
+    </div>
+</div>
+<% } else if (ctx.material.status === auditConst.status.checkNo) { %>
+    <!--审批流程/结果-->
+    <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">
+                        <div class="card mt-3">
+                            <ul class="list-group list-group-flush">
+                                <li class="list-group-item">
+                                    <i class="fa fa fa-play-circle fa-rotate-90"></i> <%- ctx.material.user.name %>  <small class="text-muted"><%- ctx.material.user.role %></small>
+                                </li>
+                                <% for (let i = 0; i < ctx.material.auditors2.length; i++) { %>
+                                <li class="list-group-item">
+                                    <% if (i < ctx.material.auditors2.length - 1) { %>
+                                    <i class="fa fa-chevron-circle-down"></i> <%- ctx.material.auditors2[i].name %>  <small class="text-muted"><%- ctx.material.auditors2[i].role %></small>
+                                    <% } else { %>
+                                    <i class="fa fa fa-stop-circle"></i> <%- ctx.material.auditors2[i].name %>  <small class="text-muted"><%- ctx.material.auditors2[i].role %></small>
+                                    <% } %>
+                                </li>
+                                <% } %>
+                            </ul>
+                        </div>
+                    </div>
+                    <div class="col-8 modal-height-500" style="overflow: auto">
+                        <% for (const ah of ctx.material.auditHistory) { %>
+                        <div class="card mt-3">
+                            <ul class="list-group list-group-flush">
+                                <% for (let iA = 0; iA < ah.length; iA++) { %>
+                                <% if (iA === 0) { %>
+                                <li class="list-group-item">
+                                    <span class="text-success pull-right"><% if (ctx.material.auditHistory.indexOf(ah) > 0) { %>重新<% } %>上报</span>
+                                    <h5 class="card-title"><i class="fa fa-play-circle fa-rotate-90 text-success"></i> <%- ctx.material.user.name %> <small class="text-muted"><%- ctx.material.user.role %></small></h5>
+                                    <p class="card-text"><small class="text-muted"><%- ah[iA].begin_time.toLocaleDateString() %></small></p>
+                                </li>
+                                <li class="list-group-item">
+                                    <% if (ah[iA].status !== auditConst.status.uncheck) { %>
+                                    <span class="<%- auditConst.statusClass[ah[iA].status] %> pull-right"><%- auditConst.statusString[ah[iA].status]%><% if (ah[iA].status === auditConst.status.checkNo) { %> <%- ctx.material.user.name %><% } %></span>
+                                    <% } %>
+                                    <h5 class="card-title"><i class="fa <%if (iA === ah.length - 1) { %>fa-stop-circle<% } else { %>fa-chevron-circle-down<% } %> <%- auditConst.statusClass[ah[iA].status] %>"></i> <%- ah[iA].name %> <small class="text-muted"><%- ah[iA].role %></small></h5>
+                                    <% if (ah[iA].status === auditConst.status.checked || ah[iA].status === auditConst.status.checkNo) { %>
+                                    <p class="card-text mb-1"><%- ah[iA].opinion %></p>
+                                    <p class="card-text"><small class="text-muted"><%- ah[iA].end_time.toLocaleDateString() %></small></p>
+                                    <% } %>
+                                </li>
+                                <% } else if (iA === ah.length - 1) { %>
+                                <li class="list-group-item">
+                                    <% if (ah[iA].status !== auditConst.status.uncheck) { %>
+                                    <span class="<%- auditConst.statusClass[ah[iA].status] %> pull-right"><%- auditConst.statusString[ah[iA].status]%><% if (ah[iA].status === auditConst.status.checkNo) { %> <%- ctx.material.user.name %><% } %></span>
+                                    <% } %>
+                                    <h5 class="card-title"><i class="fa fa-stop-circle <%- auditConst.statusClass[ah[iA].status] %>"></i> <%- ah[iA].name %> <small class="text-muted"><%- ah[iA].role %></small></h5>
+                                    <% if (ah[iA].status === auditConst.status.checked || ah[iA].status === auditConst.status.checkNo) { %>
+                                    <p class="card-text mb-1"><%- ah[iA].opinion %></p>
+                                    <p class="card-text"><small class="text-muted"><%- ah[iA].end_time.toLocaleDateString() %></small></p>
+                                    <% } %>
+                                </li>
+                                <% } else { %>
+                                <li class="list-group-item">
+                                    <% if (ah[iA].status !== auditConst.status.uncheck) { %>
+                                    <span class="<%- auditConst.statusClass[ah[iA].status] %> pull-right"><%- auditConst.statusString[ah[iA].status]%><% if (ah[iA].status === auditConst.status.checkNo) { %> <%- ctx.material.user.name %><% } %></span>
+                                    <% } %>
+                                    <h5 class="card-title"><i class="fa fa-chevron-circle-down <%- auditConst.statusClass[ah[iA].status] %>"></i> <%- ah[iA].name %> <small class="text-muted"><%- ah[iA].role %></small></h5>
+                                    <% if (ah[iA].status === auditConst.status.checked || ah[iA].status === auditConst.status.checkNo) { %>
+                                    <p class="card-text mb-1"><%- ah[iA].opinion %></p>
+                                    <p class="card-text"><small class="text-muted"><%- ah[iA].end_time.toLocaleDateString() %></small></p>
+                                    <% } %>
+                                </li>
+                                <% } %>
+                                <% } %>
+                            </ul>
+                        </div>
+                        <% } %>
+                    </div>
+                </div>
+            </div>
+            <div class="modal-footer">
+                <button type="button" class="btn btn-secondary" data-dismiss="modal">关闭</button>
+            </div>
+        </div>
+    </div>
+    </div>
+    <% if (ctx.session.sessionUser.accountId === ctx.material.user_id) { %>
+    <!--重新上报-->
+    <div class="modal fade" id="sp-list2" 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">
+                            <a href="#sub-sp" data-toggle="modal" data-target="#sub-sp" id="hideSp">修改审批流程</a>
+                            <div class="card mt-3">
+                                <ul class="list-group list-group-flush">
+                                    <li class="list-group-item">
+                                        <i class="fa fa fa-play-circle fa-rotate-90"></i> <%- ctx.material.user.name %>  <small class="text-muted"><%- ctx.material.user.role %></small>
+                                    </li>
+                                </ul>
+                                <ul class="list-group list-group-flush" id="auditors-list">
+                                    <% const auditorList = ctx.material.auditorList; %>
+                                    <% for (let i = 0; i < auditorList.length; i++) { %>
+                                        <li class="list-group-item" data-auditid="<%- auditorList[i].aid %>">
+                                            <% if (i < auditorList.length - 1) { %>
+                                                <i class="fa fa-chevron-circle-down"></i> <%- auditorList[i].name %>  <small class="text-muted"><%- auditorList[i].role %></small>
+                                            <% } else { %>
+                                                <i class="fa fa fa-stop-circle"></i> <%- auditorList[i].name %>  <small class="text-muted"><%- auditorList[i].role %></small>
+                                            <% } %>
+                                        </li>
+                                    <% } %>
+                                </ul>
+                            </div>
+                        </div>
+                        <div class="col-8 modal-height-500" style="overflow: auto">
+                            <% for (const ah of ctx.material.auditHistory) { %>
+                                <div class="card mt-3">
+                                    <ul class="list-group list-group-flush">
+                                        <% for (let iA = 0; iA < ah.length; iA++) { %>
+                                            <% if (iA === 0) { %>
+                                                <li class="list-group-item">
+                                                    <span class="text-success pull-right"><% if (ctx.material.auditHistory.indexOf(ah) > 0) { %>重新<% } %>上报</span>
+                                                    <h5 class="card-title"><i class="fa fa-play-circle fa-rotate-90 text-success"></i> <%- ctx.material.user.name %> <small class="text-muted"><%- ctx.material.user.role %></small></h5>
+                                                    <p class="card-text"><small class="text-muted"><%- ah[iA].begin_time.toLocaleDateString() %></small></p>
+                                                </li>
+                                                <li class="list-group-item">
+                                                    <% if (ah[iA].status !== auditConst.status.uncheck) { %>
+                                                        <span class="<%- auditConst.statusClass[ah[iA].status] %> pull-right"><%- auditConst.statusString[ah[iA].status]%><% if (ah[iA].status === auditConst.status.checkNo) { %> <%- ctx.material.user.name %><% } %></span>
+                                                    <% } %>
+                                                    <h5 class="card-title"><i class="fa <%if (iA === ah.length - 1) { %>fa-stop-circle<% } else { %>fa-chevron-circle-down<% } %> <%- auditConst.statusClass[ah[iA].status] %>"></i> <%- ah[iA].name %> <small class="text-muted"><%- ah[iA].role %></small></h5>
+                                                    <% if (ah[iA].status === auditConst.status.checked || ah[iA].status === auditConst.status.checkNo) { %>
+                                                        <p class="card-text mb-1"><%- ah[iA].opinion %></p>
+                                                        <p class="card-text"><small class="text-muted"><%- ah[iA].end_time.toLocaleDateString() %></small></p>
+                                                    <% } %>
+                                                </li>
+                                            <% } else if (iA === ah.length - 1) { %>
+                                                <li class="list-group-item">
+                                                    <% if (ah[iA].status !== auditConst.status.uncheck) { %>
+                                                        <span class="<%- auditConst.statusClass[ah[iA].status] %> pull-right"><%- auditConst.statusString[ah[iA].status]%><% if (ah[iA].status === auditConst.status.checkNo) { %> <%- ctx.material.user.name %><% } %></span>
+                                                    <% } %>
+                                                    <h5 class="card-title"><i class="fa fa-stop-circle <%- auditConst.statusClass[ah[iA].status] %>"></i> <%- ah[iA].name %> <small class="text-muted"><%- ah[iA].role %></small></h5>
+                                                    <% if (ah[iA].status === auditConst.status.checked || ah[iA].status === auditConst.status.checkNo) { %>
+                                                        <p class="card-text mb-1"><%- ah[iA].opinion %></p>
+                                                        <p class="card-text"><small class="text-muted"><%- ah[iA].end_time.toLocaleDateString() %></small></p>
+                                                    <% } %>
+                                                </li>
+                                            <% } else { %>
+                                                <li class="list-group-item">
+                                                    <% if (ah[iA].status !== auditConst.status.uncheck) { %>
+                                                        <span class="<%- auditConst.statusClass[ah[iA].status] %> pull-right"><%- auditConst.statusString[ah[iA].status]%><% if (ah[iA].status === auditConst.status.checkNo) { %> <%- ctx.material.user.name %><% } %></span>
+                                                    <% } %>
+                                                    <h5 class="card-title"><i class="fa fa-chevron-circle-down <%- auditConst.statusClass[ah[iA].status] %>"></i> <%- ah[iA].name %> <small class="text-muted"><%- ah[iA].role %></small></h5>
+                                                    <% if (ah[iA].status === auditConst.status.checked || ah[iA].status === auditConst.status.checkNo) { %>
+                                                        <p class="card-text mb-1"><%- ah[iA].opinion %></p>
+                                                        <p class="card-text"><small class="text-muted"><%- ah[iA].end_time.toLocaleDateString() %></small></p>
+                                                    <% } %>
+                                                </li>
+                                            <% } %>
+                                        <% } %>
+                                    </ul>
+                                </div>
+                            <% } %>
+                            <% if (ctx.material.status === auditConst.status.checkNo) {%>
+                                <div class="card mt-3">
+                                    <ul class="list-group list-group-flush">
+                                        <li class="list-group-item">
+                                            <span class="pull-right">重新上报中</span>
+                                            <h5 class="card-title"><i class="fa fa-play-circle fa-rotate-90"></i> <%- ctx.material.user.name %> <small class="text-muted"><%- ctx.material.user.role %></small></h5>
+                                            <p class="card-text"><small class="text-muted"></small></p>
+                                        </li>
+                                    </ul>
+                                    <ul class="list-group list-group-flush" id="auditors-list2">
+                                        <% const auditorList = ctx.material.auditorList; %>
+                                        <% for (let iA = 0; iA < auditorList.length; iA++) { %>
+                                            <% if (iA === auditorList.length - 1) { %>
+                                                <li class="list-group-item" data-auditid="<%- auditorList[iA].aid %>">
+                                                    <h5 class="card-title"><i class="fa fa-stop-circle"></i> <%- auditorList[iA].name %> <small class="text-muted"><%- auditorList[iA].role %></small></h5>
+                                                </li>
+                                            <% } else { %>
+                                                <li class="list-group-item" data-auditid="<%- auditorList[iA].aid %>">
+                                                    <h5 class="card-title"><i class="fa fa-chevron-circle-down"></i> <%- auditorList[iA].name %> <small class="text-muted"><%- auditorList[iA].role %></small></h5>
+                                                </li>
+                                            <% } %>
+                                        <% } %>
+                                    </ul>
+                                </div>
+                            <% } %>
+                        </div>
+                    </div>
+                </div>
+                <form class="modal-footer" method="post" action="<%- preUrl %>/audit/start" onsubmit="return checkAuditorFrom()">
+                    <input type="hidden" name="_csrf" value="<%= ctx.csrf %>">
+                    <button type="button" class="btn btn-secondary" data-dismiss="modal">关闭</button>
+                    <button class="btn btn-primary" type="submit">确认上报</button>
+                </form>
+            </div>
+        </div>
+    </div>
+    <% } %>
+<% } %>

+ 87 - 0
app/view/material/index.ejs

@@ -0,0 +1,87 @@
+<% include ../tender/tender_sub_menu.ejs %>
+<div class="panel-content">
+    <div class="panel-title">
+        <div class="title-main d-flex">
+            <% include ../tender/tender_sub_mini_menu.ejs %>
+            <h2>
+                材料调差期列表
+            </h2>
+            <div class="ml-auto">
+                <% if (ctx.session.sessionUser.accountId === ctx.tender.data.user_id && stages.length > 0 &&
+                        (materials.length === 0 || materials[0].status === auditConst.status.checked)) { %>
+                <a href="#add-qi" data-toggle="modal" data-target="#add-qi" class="btn btn-primary btn-sm pull-right">开始新一期</a>
+                <% } %>
+            </div>
+        </div>
+    </div>
+    <div class="content-wrap">
+        <div class="c-body">
+            <table class="table table-bordered">
+                <thead>
+                <tr>
+                    <th>期数</th>
+                    <th class="text-center">添加时间</th>
+                    <th class="text-center">计量期</th>
+                    <th class="text-center">价差费用(含税)</th>
+                    <th class="text-center">价差费用</th>
+                    <th class="text-center">审批进度</th>
+                    <th class="text-center">操作</th>
+                </tr>
+                </thead>
+                <tbody>
+                <% for (const m of materials) { %>
+                <tr>
+                    <td>
+                        <a href="<%- '/tender/' + ctx.tender.id + '/measure/material/' + m.order %>">第 <%- m.order %> 期</a>
+                    </td>
+                    <td class="text-center"><%= moment(m.in_time).format('YYYY-MM-DD') %></td>
+                    <td class="text-center">第 <%= m.s_order %> 期</td>
+                    <td class="text-right"></td>
+                    <td class="text-right"></td>
+                    <td class="<%- auditConst.auditProgressClass[m.status] %>">
+                        <% if (m.curAuditor) { %>
+                            <a href="#sp-list" data-toggle="modal" data-target="#sp-list" m-order="<%- m.order %>"><%- m.curAuditor.name %><%if (m.curAuditor.role !== '' && m.curAuditor.role !== null) { %>-<%- m.curAuditor.role %><% } %></a>
+                        <% } %>
+                        <%- auditConst.auditProgress[m.status] %>
+                    </td>
+                    <td class="text-center">
+                    <% if (m.status === auditConst.status.uncheck && m.user_id === ctx.session.sessionUser.accountId) { %>
+                        <a href="<%- '/tender/' + ctx.tender.id + '/measure/material/' + m.order %>" class="btn <%- auditConst.statusButtonClass[m.status] %> btn-sm"><%- auditConst.statusButton[m.status] %></a>
+                    <% } else if (m.status === auditConst.status.checkNo && m.curAuditor && m.user_id === ctx.session.sessionUser.accountId) { %>
+                        <a href="<%- '/tender/' + ctx.tender.id + '/measure/material/' + m.order %>" class="btn <%- auditConst.statusButtonClass[m.status] %> btn-sm"><%- auditConst.statusButton[m.status] %></a>
+                    <% } else if (m.status === auditConst.status.checking && m.curAuditor && m.curAuditor.aid === ctx.session.sessionUser.accountId) { %>
+                        <a href="<%- '/tender/' + ctx.tender.id + '/measure/material/' + m.order %>" class="btn <%- auditConst.statusButtonClass[m.status] %> btn-sm"><%- auditConst.statusButton[m.status] %></a>
+                    <% } else { %>
+                        <span class="<%- auditConst.auditStringClass[m.status] %>"><%- auditConst.auditString[m.status] %></span>
+                    <% } %>
+                    <% if (m.user_id === ctx.session.sessionUser.accountId && m.order === materials.length) { %>
+                        <a href="#del-qi" class="btn btn-outline-danger btn-sm ml-1" data-toggle="modal" data-target="#del-qi">删除</a>
+                    <% } %>
+                    </td>
+                </tr>
+                <% } %>
+                </tbody>
+            </table>
+        </div>
+    </div>
+</div>
+<script src="/public/js/sub_menu.js"></script>
+<script>
+    const tenderId = '<%- ctx.tender.id %>';
+    const auditConst = JSON.parse('<%- auditConst2 %>');
+    $.subMenu({
+        menu: '#sub-menu', miniMenu: '#sub-mini-menu', miniMenuList: '#mini-menu-list',
+        toMenu: '#to-menu', toMiniMenu: '#to-mini-menu',
+        //key: 'measure.memu.1.0.0',
+        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();
+        }
+    });
+</script>

+ 63 - 0
app/view/material/info.ejs

@@ -0,0 +1,63 @@
+<% include ./material_sub_menu.ejs %>
+<div class="panel-content">
+    <div class="panel-title">
+        <div class="title-main d-flex justify-content-between">
+            <% include ./material_sub_mini_menu.ejs %>
+            <div>
+                <% if ((ctx.material.status === auditConst.status.uncheck || ctx.material.status === auditConst.status.checkNo) && ctx.session.sessionUser.accountId === ctx.material.user_id) { %>
+                <div class="d-inline-block">
+                    <a href="javascript: void(0);" id="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);" id="del" 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>
+                </div>
+                <% } %>
+                <div class="d-inline-block">
+                    本期调差计量期:第<span class="mx-2"><%= ctx.material.s_order.split(',').join(',') %></span>期
+                </div>
+            </div>
+            <div class="ml-auto">
+            </div>
+        </div>
+    </div>
+    <div class="content-wrap">
+        <div class="c-header p-0">
+        </div>
+        <div class="row w-100 sub-content">
+            <div id="main-view" class="c-body" style="width: 100%">
+                <!--上部分-->
+                <div class="sjs-height-1" id="material-spread">
+                </div>
+                <!--下部分-->
+                <div class="bcontent-wrap">
+                    <div class="bc-bar mb-1">
+                        <div class="input-group input-group-sm ">
+                            <div class="input-group-prepend">
+                                <span class="input-group-text" id="basic-addon1">增税税率</span>
+                            </div>
+                            <select class="form-control col-1"><option>9%</option><option>10%</option><option>11%</option></select>
+                        </div>
+                    </div>
+                    <div class="sp-wrap">
+                        <div class="col-4 p-0">
+                            <table class="table table-sm table-bordered">
+                                <tr><th></th><th>本期金额</th><th>截止本期金额</th></tr>
+                                <tr><td>材料价差费用</td><td></td><td></td></tr>
+                                <tr><td>材料价差费用(含税)</td><td></td><td></td></tr>
+                            </table>
+                        </div>
+                    </div>
+                </div>
+            </div>
+        </div>
+    </div>
+</div>
+<% if ((ctx.material.status === auditConst.status.uncheck || ctx.material.status === auditConst.status.checkNo) && ctx.session.sessionUser.accountId === ctx.material.user_id) {%>
+<script>
+    const accountList = JSON.parse('<%- JSON.stringify(accountList) %>');
+</script>
+<% } %>
+<script>
+    const materialType = JSON.parse('<%- materialType %>');
+    const materialBillsData = JSON.parse('<%- JSON.stringify(materialBillsData) %>');
+    const materialListData = JSON.parse('<%- JSON.stringify(materialListData) %>');
+    const readOnly = <%- material.readOnly %>;
+</script>

+ 2 - 0
app/view/material/info_modal.ejs

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

+ 30 - 0
app/view/material/material_sub_menu.ejs

@@ -0,0 +1,30 @@
+<div class="panel-sidebar" id="sub-menu">
+    <div class="panel-title">
+        <div class="title-bar">
+            <h2 class="text-truncate" style="white-space:nowrap; overflow:hidden; text-overflow:ellipsis;" data-toggle="tooltip" data-placement="right" title=""  data-original-title="第<%- ctx.material.order%>期 - <%- tender.name %>">第<%- ctx.material.order%>期 - <%- tender.name %></h2>
+        </div>
+    </div>
+    <div class="scrollbar-auto">
+        <div class="nav-box">
+                <ul class="nav-list list-unstyled">
+                    <li class=""><a href="/tender/<%- ctx.tender.id %>/measure/material"><i class="fa fa-chevron-left "></i> <span>返回</span></a></li>
+                </ul>
+            </div>
+        <div class="nav-box">
+                <ul class="nav-list list-unstyled">
+                    <li class="<% if (ctx.url === '/tender/' + ctx.tender.id + '/measure/material/' + ctx.material.order) { %>active<% } %>">
+                        <a href="/tender/<%- ctx.tender.id %>/measure/material/<%- ctx.material.order %>"><span class="ml-3">调差工料</span></a>
+                    </li>
+                </ul>
+            </div>
+        <div class="nav-box">
+                <ul class="nav-list list-unstyled">
+                    <li class="<% if (ctx.url === '/tender/' + ctx.tender.id + '/measure/material/' + ctx.material.order + '/list') { %>active<% } %>">
+                        <a href="/tender/<%- ctx.tender.id %>/measure/material/<%- material.order %>/list"><span class="ml-3">调差清单</span></a>
+                    </li>
+                </ul>
+            </div>
+        <% include ./audit_btn.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-sign-out fa-flip-horizontal"></i></a></div>
+    </div>
+</div>

+ 29 - 0
app/view/material/material_sub_mini_menu.ejs

@@ -0,0 +1,29 @@
+<!--折起的菜单-->
+<div class="min-side" id="sub-mini-menu" style="display: none;">
+    <div class="side-switch">
+        <i class="fa fa-bars"></i>
+    </div>
+    <div class="side-menu" id="mini-menu-list" style="display: none">
+        <div class="nav-box">
+            <ul class="nav-list list-unstyled">
+                <li class=""><a href="/tender/<%- ctx.tender.id %>/measure/material"><i class="fa fa-chevron-left "></i> <span>返回</span></a></li>
+            </ul>
+        </div>
+        <div class="nav-box">
+            <ul class="nav-list list-unstyled">
+                <li class="<% if (ctx.url === '/tender/' + ctx.tender.id + '/measure/material/' + ctx.material.order) { %>active<% } %>">
+                    <a href="/tender/<%- ctx.tender.id %>/measure/material/<%- ctx.material.order %>"><span class="ml-3">调差工料</span></a>
+                </li>
+            </ul>
+        </div>
+        <div class="nav-box">
+            <ul class="nav-list list-unstyled">
+                <li class="<% if (ctx.url === '/tender/' + ctx.tender.id + '/measure/material/' + ctx.material.order + '/list') { %>active<% } %>">
+                    <a href="/tender/<%- ctx.tender.id %>/measure/material/<%- material.order %>/list"><span class="ml-3">调差清单</span></a>
+                </li>
+            </ul>
+        </div>
+        <% include ./audit_btn.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-thumb-tack"></i></a></div>
+    </div>
+</div>

+ 144 - 0
app/view/material/modal.ejs

@@ -0,0 +1,144 @@
+<% if (ctx.session.sessionUser.accountId === ctx.tender.data.user_id && stages.length > 0 &&
+        (materials.length === 0 || materials[0].status === auditConst.status.checked)) { %>
+<!--弹出添加期-->
+<div class="modal fade" id="add-qi" data-backdrop="static">
+    <div class="modal-dialog" role="document">
+        <form class="modal-content" action="<%- preUrl + '/measure/material/add' %>" method="post">
+            <div class="modal-header">
+                <h5 class="modal-title">添加新一期</h5>
+            </div>
+            <div class="modal-body">
+                <div class="form-group">
+                    <label>调差期</label>
+                    <input class="form-control" value="第 <%- materials.length + 1 %> 期" type="text" readonly="">
+                </div>
+                <div class="form-group">
+                    <label>本期调差,包含计量期<span class="ml-2 text-danger" id="show_order" style="display: none">第<b class="mx-2"></b>期</span></label>
+                    <div class="row">
+                        <% for (const stage of stages) { %>
+                        <div class="col-4">
+                            <div class="custom-control custom-checkbox">
+                                <input type="checkbox" class="custom-control-input select-stage-order" id="stage_<%= stage.id %>" name="stage_id[]" value="<%= stage.id %>" data-order="<%= stage.order %>">
+                                <label class="custom-control-label" for="stage_<%= stage.id %>">第<%= stage.order %>期</label>
+                            </div>
+                        </div>
+                        <% } %>
+                    </div>
+                </div>
+            </div>
+            <div class="modal-footer">
+                <input type="hidden" name="_csrf" value="<%= ctx.csrf %>" />
+                <input type="hidden" name="s_order" value="" id="s_order" />
+                <button type="button" class="btn btn-secondary" data-dismiss="modal">关闭</button>
+                <button type="submit" id="addMaterial" class="btn btn-primary">确定添加</button>
+            </div>
+        </form>
+    </div>
+</div>
+<% } %>
+<% if (materials && materials.length >= 1) { %>
+<!--删除期-->
+<div class="modal fade" id="del-qi" data-backdrop="static">
+    <div class="modal-dialog" role="document">
+        <form class="modal-content" action="<%- preUrl + '/measure/material/delete' %>" method="post">
+            <div class="modal-header">
+                <h5 class="modal-title">删除期</h5>
+            </div>
+            <div class="modal-body">
+                <h5>确认删除「第<%= materials.length %>期」?</h5>
+                <h5>删除后,数据无法恢复,请谨慎操作。</h5>
+            </div>
+            <div class="modal-footer">
+                <input type="hidden" name="stage_id" value="<%= materials[0].id %>">
+                <input type="hidden" name="_csrf" value="<%= ctx.csrf %>" />
+                <button type="button" class="btn btn-secondary" data-dismiss="modal">取消</button>
+                <button type="submit" class="btn btn-danger">确定删除</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">
+                        <div class="card mt-3">
+                            <ul class="list-group list-group-flush" id="auditor-list">
+                                <li class="list-group-item"><i class="fa fa fa-play-circle fa-rotate-90"></i> 布尔  <small class="text-muted">施工</small></li>
+                                <li class="list-group-item"><i class="fa fa-chevron-circle-down"></i> 张三  <small class="text-muted">监理</small></li>
+                                <li class="list-group-item"><i class="fa fa-chevron-circle-down"></i> 王五 <small class="text-muted">监理</small></li>
+                                <li class="list-group-item"><i class="fa fa fa-stop-circle"></i> 李四 <small class="text-muted">监理</small></li>
+                            </ul>
+                        </div>
+                    </div>
+                    <div class="col-8 modal-height-500" style="overflow: auto" id="auditor-list2">
+                        <div class="card mt-3">
+                            <ul class="list-group list-group-flush">
+                                <li class="list-group-item">
+                                    <span class="text-success pull-right">上报</span>
+                                    <h5 class="card-title"><i class="fa fa-play-circle fa-rotate-90 text-success"></i> 布尔 <small class="text-muted">施工</small></h5>
+                                    <p class="card-text">2017-11-25</p>
+                                </li>
+                                <li class="list-group-item">
+                                    <span class="text-success pull-right">审批通过</span>
+                                    <h5 class="card-title"><i class="fa fa-chevron-circle-down text-success"></i> 张三 <small class="text-muted">监理</small></h5>
+                                    <p class="card-text">审批意见。2017-11-25</p>
+                                </li>
+                                <li class="list-group-item">
+                                    <span class="text-success pull-right">审批通过</span>
+                                    <h5 class="card-title"><i class="fa fa-chevron-circle-down text-success"></i> 王五 <small class="text-muted">监理</small></h5>
+                                    <p class="card-text">审批通过。2017-11-26</p>
+                                </li>
+                                <li class="list-group-item">
+                                    <span class="text-warning pull-right">审批退回 布尔</span>
+                                    <h5 class="card-title"><i class="fa fa-stop-circle text-warning"></i> 李四 <small class="text-muted">监理</small></h5>
+                                    <p class="card-text">审批退回,审批意见文本。2017-11-27</p>
+                                </li>
+                            </ul>
+                        </div>
+                        <!--退回原报重新上报-->
+                        <div class="card mt-3">
+                            <ul class="list-group list-group-flush">
+                                <li class="list-group-item">
+                                    <span class="text-success pull-right">重新上报</span>
+                                    <h5 class="card-title"><i class="fa fa-play-circle fa-rotate-90 text-success"></i> 布尔 <small class="text-muted">施工</small></h5>
+                                    <p class="card-text">2017-12-01</p>
+                                </li>
+                                <li class="list-group-item">
+                                    <span class="text-success pull-right">审批通过</span>
+                                    <h5 class="card-title"><i class="fa fa-chevron-circle-down text-success"></i> 张三 <small class="text-muted">监理</small></h5>
+                                    <p class="card-text">审批通过 2017-12-02</p>
+                                </li>
+                                <li class="list-group-item">
+                                    <span class="text-warning pull-right">审批退回 张三</span>
+                                    <h5 class="card-title"><i class="fa fa-chevron-circle-down text-warning"></i> 王五 <small class="text-muted">监理</small></h5>
+                                    <p class="card-text">审批退回 2017-12-02</p>
+                                </li>
+                                <!--王五退回上一审批人 张三,张三重新审批-->
+                                <li class="list-group-item">
+                                    <span class="pull-right">审批中</span>
+                                    <h5 class="card-title"><i class="fa fa-chevron-circle-down"></i> 张三 <small class="text-muted">监理</small></h5>
+                                    <p class="card-text"></p>
+                                </li>
+                                <li class="list-group-item">
+                                    <h5 class="card-title"><i class="fa fa-stop-circle"></i> 李四 <small class="text-muted">监理</small></h5>
+                                </li>
+                            </ul>
+                        </div>
+                    </div>
+                </div>
+            </div>
+            <div class="modal-footer">
+                <button type="button" class="btn btn-secondary" data-dismiss="modal">关闭</button>
+            </div>
+        </div>
+    </div>
+</div>
+<script src="/public/js/moment/moment.min.js"></script>
+<script src="/public/js/measure_material.js"></script>

+ 2 - 2
app/view/measure/stage.ejs

@@ -1,12 +1,12 @@
 <% include ../tender/tender_sub_menu.ejs %>
 <div class="panel-content">
     <div class="panel-title">
-        <div class="title-main d-flex justify-content-between">
+        <div class="title-main d-flex">
             <% include ../tender/tender_sub_mini_menu.ejs %>
             <h2>
                 期列表
             </h2>
-            <div>
+            <div class="ml-auto">
                 <% if (ctx.session.sessionUser.accountId === ctx.tender.data.user_id && ctx.tender.data.ledger_status === auditConst.status.checked &&
                         (stages.length === 0 || stages[0].status === auditConst.status.checked)) { %>
                 <a href="#add-qi" data-toggle="modal" data-target="#add-qi" class="btn btn-primary btn-sm pull-right">开始新一期</a>

+ 2 - 2
app/view/measure/stage_modal.ejs

@@ -3,7 +3,7 @@
 <!--弹出添加期-->
 <div class="modal fade" id="add-qi" data-backdrop="static">
     <div class="modal-dialog" role="document">
-        <form class="modal-content" action="<%- preUrl + '/measure/add' %>" method="post">
+        <form class="modal-content" action="<%- preUrl + '/measure/add' %>" method="post" onsubmit="return checkValidForm();">
             <div class="modal-header">
                 <h5 class="modal-title">添加新一期</h5>
             </div>
@@ -24,7 +24,7 @@
             <div class="modal-footer">
                 <input type="hidden" name="_csrf" value="<%= ctx.csrf %>" />
                 <button type="button" class="btn btn-secondary" data-dismiss="modal">关闭</button>
-                <button type="submit" class="btn btn-primary">确定添加</button>
+                <button type="submit" class="btn btn-primary" id="add-stage-btn">确定添加</button>
             </div>
         </form>
     </div>

+ 1 - 1
app/view/revise/info_modal.ejs

@@ -478,7 +478,7 @@
     // 检查上报情况
     function checkAuditorFrom () {
         if ($('#auditors li').length === 0) {
-            toast('请先选择审批人,再上报数据', 'error', 'exclamation-circle');
+            toastr.error('请先选择审批人,再上报数据');
             return false;
         }
     }

+ 2 - 0
app/view/stage/audit_modal.ejs

@@ -31,8 +31,10 @@
                         <select class="form-control" id="account_list">
                             <option value="0">选择审批人</option>
                             <% for (const account of accountList) { %>
+                            <% if (account.id !== ctx.session.sessionUser.accountId) { %>
                                 <option value="<%= account.id %>"><%= account.name %><% if (account.role !== '') { %>(<%= account.role %>)<% } %><% if (account.company !== '') { %> -<%= account.company %><% } %></option>
                             <% } %>
+                            <% } %>
                         </select>
                     </div>
                 </div>

+ 8 - 3
app/view/stage/compare_modal.ejs

@@ -8,10 +8,15 @@
             </div>
             <div class="modal-body">
                 <table class="table table-sm">
-                    <tr><th>审批人</th><th width="90">选择</th></tr>
+                    <tr><th>审批人</th><th width="90">选择</th><th></th></tr>
                     <% for (const [i, a] of ctx.stage.auditors.entries()) { %>
-                    <% if (a.status === auditConst.status.checked || a.aid === ctx.session.sessionUser.accountId) { %>
-                    <tr auditorId="<%- a.aid %>"><td><%- a.name %></td><td><input type="checkbox" audit-order="<%- i + 1 %>"></td></tr>
+                    <% if (a.status === auditConst.status.checked || a.status === auditConst.status.checkNoPre ||
+                            (a.status === auditConst.status.checking && a.aid === ctx.session.sessionUser.accountId)) { %>
+                    <tr auditorId="<%- a.aid %>">
+                        <td><%- a.name %></td>
+                        <td><input type="checkbox" audit-order="<%- i + 1 %>"></td>
+                        <td class="text-center <%- auditConst.statusClass[a.status] %>" style="width: 80px"><%- auditConst.statusString[a.status] %></td>
+                    </tr>
                     <% } %>
                     <% } %>
                 </table>

+ 1 - 1
app/view/stage/detail.ejs

@@ -130,7 +130,7 @@
     </div>
 </div>
 <script>
-    const readOnly = <%- (stage.readOnly) %>;
+    const readOnly = <%- (stage.readOnly || stage.revising) %>;
     const stage = JSON.parse('<%- JSON.stringify(ctx.stage) %>');
     const imType = JSON.parse('<%- JSON.stringify(imType) %>');
 </script>

+ 2 - 2
app/view/stage/gather.ejs

@@ -14,7 +14,7 @@
                             <div class="form-group">
                                 <div class="custom-control custom-radio">
                                     <input type="radio" id="customRadio1" name="customRadio" class="custom-control-input" checked="">
-                                    <label class="custom-control-label" for="customRadio1">施工图复核数量</label>
+                                    <label class="custom-control-label" for="customRadio1">台账数量</label>
                                 </div>
                             </div>
                             <div class="form-group">
@@ -26,7 +26,7 @@
                             <div class="form-group mb-0">
                                 <div class="custom-control custom-radio">
                                     <input type="radio" id="customRadio3" name="customRadio" class="custom-control-input">
-                                    <label class="custom-control-label" for="customRadio3">施工图复核数量 或 签约清单数量</label>
+                                    <label class="custom-control-label" for="customRadio3">台账数量 或 签约清单数量</label>
                                 </div>
                             </div>
                         </form>

+ 2 - 1
app/view/stage/index.ejs

@@ -247,7 +247,7 @@
         regExp = new RegExp(FindText, 'g');
         return this.replace(regExp, RepText);
     }
-    const readOnly = <%- stage.readOnly %>;
+    const readOnly = <%- stage.readOnly || stage.revising %>;
     const ledgerSpreadSetting = JSON.parse('<%- JSON.stringify(ledgerSpread) %>');
     const posSpreadSetting = JSON.parse('<%- JSON.stringify(posSpread) %>');
     const tender = JSON.parse('<%- JSON.stringify(tender) %>');
@@ -258,6 +258,7 @@
     const whiteList = JSON.parse('<%- JSON.stringify(whiteList) %>');
     let attData = JSON.parse('<%- JSON.stringify(attData) %>');
     const userID = '<%- ctx.session.sessionUser.accountId %>';
+    const ckColSetting = 'stage-col-visible-1.0.2-<%- tender.id %>';
 </script>
 <% if (ctx.stage.status === auditConst.status.uncheck && ctx.session.sessionUser.accountId === ctx.stage.user_id) {%>
 <script>

+ 5 - 2
app/view/stage/pay.ejs

@@ -54,11 +54,14 @@
 <script>
     const tender = JSON.parse('<%- JSON.stringify(tender) %>');
     const stage = JSON.parse('<%- JSON.stringify(ctx.stage) %>');
-    const readOnly = <%- stage.readOnly %>;
+    const readOnly = <%- stage.readOnly || stage.revising %>;
     const dealPay = JSON.parse('<%- JSON.stringify(dealPay) %>');
     const calcBase = JSON.parse('<%- JSON.stringify(calcBase) %>');
     const decimal = JSON.parse('<%- JSON.stringify(ctx.tender.info.decimal) %>');
     const whiteList = JSON.parse('<%- JSON.stringify(whiteList) %>');
     const uploadPermission = <%- uploadPermission %>;
-    const userID = '<%- ctx.session.sessionUser.accountId %>';
+    const userID = <%- ctx.session.sessionUser.accountId %>;
+    const preContractTp = <%- (pre.contract_tp || 0) %>;
+    const preQcTp = <%- (pre.qc_tp || 0) %>;
+    const preGatherTp = <%- (pre.gather_tp || 0) %>;
 </script>

+ 11 - 1
app/view/tender/detail.ejs

@@ -386,6 +386,8 @@
                                     </div>
                                 </div>
                                 <div class="tab-pane fade" id="v-pills-2" role="tabpanel" >
+                                    <% if (((tender.ledger_status === audit.ledger.status.uncheck || tender.ledger_status === audit.ledger.status.checkNo) && tender.user_id === ctx.session.sessionUser.accountId)
+                                            || (lastStage && lastStage.user_id === ctx.session.sessionUser.accountId && (lastStage.status === audit.stage.status.checkNo || lastStage.status === audit.stage.status.uncheck))) { %>
                                     <!--操作-->
                                     <div class="d-flex justify-content-end mt-3">
                                         <span>
@@ -396,6 +398,7 @@
                                             <button type="button" class="btn btn-sm btn-outline-danger" id="cancel-2"><i class="fa fa-close"></i>  取消</button>
                                         </span>
                                     </div>
+                                    <% } %>
                                     <!--小数位数-->
                                     <legend class="mt-3">小数位数</legend>
                                     <div class="form-group">
@@ -441,6 +444,8 @@
                                     </div>
                                 </div>
                                 <div class="tab-pane fade" id="v-pills-3" role="tabpanel" >
+                                    <% if (((tender.ledger_status === audit.ledger.status.uncheck || tender.ledger_status === audit.ledger.status.checkNo) && tender.user_id === ctx.session.sessionUser.accountId)
+                                            || (lastStage && lastStage.user_id === ctx.session.sessionUser.accountId && (lastStage.status === audit.stage.status.checkNo || lastStage.status === audit.stage.status.uncheck))) { %>
                                     <!--操作-->
                                     <div class="d-flex justify-content-end mt-3">
                                         <div class="alert alert-danger m-0 p-1 px-2" id="hint-3" style="display: none"><i class="fa fa-smile-o mr-2"></i>A simple danger alert—check it out!</div>
@@ -452,13 +457,14 @@
                                             <button type="button" class="btn btn-sm btn-outline-danger" id="cancel-3"><i class="fa fa-close"></i> 取消</button>
                                         </div>
                                     </div>
+                                    <% } %>
                                     <legend class="mt-3">清单精度</legend>
                                     <!--默认显示-->
                                     <div class="form-group" id="precision-spread" style="height: 365px; width: 600px;">
                                     </div>
                                 </div>
                                 <div class="tab-pane fade" id="v-pills-4" role="tabpanel" >
-                                    <!--操作-->
+                                    <% if (!lastStage || (lastStage.order === 1 && lastStage.user_id === ctx.session.sessionUser.accountId && (lastStage.status === audit.stage.status.checkNo || lastStage.status === audit.stage.status.uncheck))) { %>
                                     <!--操作-->
                                     <div class="d-flex justify-content-end mt-3">
                                         <div class="alert alert-danger m-0 p-1 px-2" id="hint-4" style="display: none"><i class="fa fa-smile-o mr-2"></i>A simple danger alert—check it out!</div>
@@ -470,6 +476,7 @@
                                             <button type="button" class="btn btn-sm btn-outline-danger" id="cancel-4"><i class="fa fa-close"></i> 取消</button>
                                         </div>
                                     </div>
+                                    <% } %>
                                     <legend class="mt-3">合同参数</legend>
                                     <!--默认显示-->
                                     <div class="form-group" id="param-spread" style="height: 230px; width: 600px;">
@@ -756,4 +763,7 @@
 </script>
 <script>
     let property = JSON.parse('<%- JSON.stringify(tenderInfo) %>');
+    let ledgerChecked = <%- tender.ldeger_status === audit.ledger.status.checked %>;
+    <% console.log(lastStage) %>
+    let firstStageChecked = <%- lastStage !== undefined && lastStage !== null && (lastStage.order > 1 || (lastStage.order === 1 && lastStage.status !== audit.stage.status.checked)) %>;
 </script>

+ 5 - 0
app/view/tender/tender_sub_menu.ejs

@@ -34,6 +34,11 @@
         </div>
         <div class="nav-box">
             <ul class="nav-list list-unstyled">
+                <li <% if (ctx.url === '/tender/' + ctx.tender.id + '/measure/material') { %>class="active"<% } %>><a href="/tender/<%- ctx.tender.id %>/measure/material"><i class="fa fa-line-chart"></i> <span>材料调差</span></a></li>
+            </ul>
+        </div>
+        <div class="nav-box">
+            <ul class="nav-list list-unstyled">
                 <li <% if (ctx.url === '/tender/' + ctx.tender.id + '/report') { %>class="active"<% } %>><a href="/tender/<%- ctx.tender.id %>/report"><i class="fa fa-file-text-o"></i> <span>报表</span></a></li>
             </ul>
         </div>

+ 6 - 1
app/view/tender/tender_sub_mini_menu.ejs

@@ -31,9 +31,14 @@
         </div>
         <div class="nav-box">
             <ul class="nav-list list-unstyled">
+                <li <% if (ctx.url === '/tender/' + ctx.tender.id + '/measure/material') { %>class="active"<% } %>><a href="/tender/<%- ctx.tender.id %>/measure/material"><i class="fa fa-line-chart"></i> <span>材料调差</span></a></li>
+            </ul>
+        </div>
+        <div class="nav-box">
+            <ul class="nav-list list-unstyled">
                 <li <% if (ctx.url === '/tender/' + ctx.tender.id + '/report') { %>class="active"<% } %>><a href="/tender/<%- ctx.tender.id %>/report"><i class="fa fa-file-text-o"></i> <span>报表</span></a></li>
             </ul>
         </div>
         <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-thumb-tack"></i></a></div>
     </div>
-</div>
+</div>

+ 20 - 4
config/web.js

@@ -48,6 +48,7 @@ const JsFiles = {
         "/public/js/lodash.js",
         "/public/js/lz-string/lz-string.js",
         "/public/js/number-precision.js",
+        "/public/js/toastr.min.js",
         "/public/js/global.js",
     ],
     controller: {
@@ -142,7 +143,6 @@ const JsFiles = {
                 files: [
                     "/public/js/spreadjs/sheets/gc.spread.sheets.all.10.0.1.min.js",
                     "/public/js/decimal.min.js",
-                    "/public/js/toastr.min.js",
                 ],
                 mergeFiles: [
                     "/public/js/sub_menu.js",
@@ -161,7 +161,6 @@ const JsFiles = {
                 files: [
                     "/public/js/spreadjs/sheets/gc.spread.sheets.all.10.0.1.min.js",
                     "/public/js/decimal.min.js",
-                    "/public/js/toastr.min.js",
                 ],
                 mergeFiles: [
                     "/public/js/sub_menu.js",
@@ -178,7 +177,6 @@ const JsFiles = {
                 files: [
                     "/public/js/spreadjs/sheets/gc.spread.sheets.all.10.0.1.min.js",
                     "/public/js/decimal.min.js",
-                    "/public/js/toastr.min.js",
                 ],
                 mergeFiles: [
                     "/public/js/sub_menu.js",
@@ -198,7 +196,6 @@ const JsFiles = {
                 files: [
                     "/public/js/spreadjs/sheets/gc.spread.sheets.all.10.0.1.min.js",
                     "/public/js/decimal.min.js",
-                    "/public/js/toastr.min.js",
                 ],
                 mergeFiles: [
                     "/public/js/sub_menu.js",
@@ -308,6 +305,25 @@ const JsFiles = {
                 mergeFile: 'measure_compare',
             }
         },
+        material: {
+            info: {
+                files: [
+                    "/public/js/spreadjs/sheets/gc.spread.sheets.all.10.0.1.min.js",
+                    "/public/js/decimal.min.js",
+                    "/public/js/toastr.min.js",
+                ],
+                mergeFiles: [
+                    "/public/js/sub_menu.js",
+                    "/public/js/div_resizer.js",
+                    "/public/js/spreadjs_rela/spreadjs_zh.js",
+                    "/public/js/zh_calc.js",
+                    "/public/js/path_tree.js",
+                    "/public/js/material.js",
+                    "/public/js/material_audit.js",
+                ],
+                mergeFile: 'material',
+            },
+        },
     }
 
 };