Browse Source

Merge branch 'dev' of http://192.168.1.41:3000/maixinrong/Calculation into dev

TonyKang 4 years ago
parent
commit
46d5d92750

+ 1 - 0
app/const/account_permission.js

@@ -28,6 +28,7 @@ const permission = {
         children: [
             { title: '创建标段', value: 1 },
             { title: '查阅所有标段', value: 2 },
+            { title: '维护签约清单', value: 3, hint: '开启该选项,台帐审批通过后,可上传签约清单', hintIcon: 'fa-question-circle' },
         ],
     },
     // cooperation: {

+ 51 - 0
app/const/shenpi.js

@@ -0,0 +1,51 @@
+'use strict';
+
+/**
+ * 审批流程设置
+ *
+ * @author ELlisran
+ * @date 2020/10/20
+ * @version
+ */
+// 审批类型
+const sp_type = {
+    advance: 1,
+    ledger: 2,
+    revise: 3,
+    stage: 4,
+    change: 5,
+    material: 6,
+};
+// const sp_name = [];
+// sp_name[sp_type.advance] = '预付款审批';
+// sp_name[sp_type.ledger] = '台账审批';
+// sp_name[sp_type.revise] = '台账修订';
+// sp_name[sp_type.stage] = '计量期审批';
+// sp_name[sp_type.change] = '工程变更审批';
+// sp_name[sp_type.material] = '材料调差审批';
+
+const sp_lc = [
+    { code: 'advance', type: sp_type.advance, name: '预付款审批' },
+    { code: 'ledger', type: sp_type.ledger, name: '台账审批' },
+    { code: 'revise', type: sp_type.revise, name: '台账修订' },
+    { code: 'stage', type: sp_type.stage, name: '计量期审批' },
+    { code: 'change', type: sp_type.change, name: '工程变更审批' },
+    { code: 'material', type: sp_type.material, name: '材料调差审批' },
+];
+
+const sp_status = {
+    sqspr: 1, // 授权审批人
+    gdspl: 2, // 固定审批流
+    gdzs: 3, // 固定终审
+};
+const sp_status_list = [];
+sp_status_list[sp_status.sqspr] = { status: sp_status.sqspr, name: '授权审批人', msg: '由上报人设置审批流程' };
+sp_status_list[sp_status.gdspl] = { status: sp_status.gdspl, name: '固定审批流', msg: '审批流程固定,上报人只能按照设置好的审批流程进行' };
+sp_status_list[sp_status.gdzs] = { status: sp_status.gdzs, name: '固定终审', msg: '结束审批流为固定人,终审前的审批流程由上报人设置,即授权审批人' };
+
+module.exports = {
+    sp_type,
+    sp_lc,
+    sp_status,
+    sp_status_list,
+};

+ 46 - 3
app/controller/change_controller.js

@@ -500,9 +500,8 @@ module.exports = app => {
                 renderData.auditList = auditList;
 
                 // 获取是否已存在调用变更令
-                const stageChangeNum = await ctx.service.stageChange.count({ cid: change.cid });
-                const stageChangeNum2 = await ctx.service.stageChange.count({ cid: change.cid, qty: null });
-                renderData.stageChangeNum = stageChangeNum - stageChangeNum2;
+                const changeUsedData = await ctx.service.stageChange.getFinalUsedData(ctx.tender.id, change.cid);
+                renderData.stageChangeNum = this.ctx.helper.sum(changeUsedData.map(x => { return Math.abs(x.used_qty); }));
                 await this.layout('change/info.ejs', renderData, 'change/info_modal.ejs');
             } catch (err) {
                 this.log(err);
@@ -722,6 +721,50 @@ module.exports = app => {
         }
 
         /**
+         * 批量下载 - 压缩成zip文件返回
+         * @param {Oject} ctx - 全局上下文
+         */
+        async downloadZip(ctx) {
+            try {
+                const fileIds = JSON.parse(ctx.request.query.fileIds);
+                // const { fileIds } = JSON.parse(ctx.request.body.data);
+                // console.log('fileIds', fileIds);
+                const { name: changeName } = await ctx.service.changeAtt.getChangeName(ctx.params.cid);
+                const zipFilename = `${ctx.tender.data.name}-工程变更-${changeName}-附件.zip`;
+                const time = Date.now();
+                const zipPath = `app/public/upload/change/fu_jian_zip${time}.zip`;
+                const size = await ctx.service.changeAtt.compressedFile(fileIds, zipPath);
+
+                // 解决中文无法下载问题
+                const userAgent = (ctx.request.header['user-agent'] || '').toLowerCase();
+                let disposition = '';
+                if (userAgent.indexOf('msie') >= 0 || userAgent.indexOf('chrome') >= 0) {
+                    disposition = 'attachment; filename=' + encodeURIComponent(zipFilename);
+                } else if (userAgent.indexOf('firefox') >= 0) {
+                    disposition = 'attachment; filename*="utf8\'\'' + encodeURIComponent(zipFilename) + '"';
+                } else {
+                    /* safari等其他非主流浏览器只能自求多福了 */
+                    disposition = 'attachment; filename=' + new Buffer(zipFilename).toString('binary');
+                }
+                ctx.response.set({
+                    'Content-Type': 'application/octet-stream',
+                    'Content-Disposition': disposition,
+                    'Content-Length': size,
+                });
+                const readStream = fs.createReadStream(path.join(this.app.baseDir, zipPath));
+
+                ctx.body = readStream;
+                // ctx.body = fs.readFileSync(path.resolve(this.app.baseDir, zipPath));
+                readStream.on('close', () => {
+                    if (fs.existsSync(path.resolve(this.app.baseDir, zipPath))) {
+                        fs.unlinkSync(path.resolve(this.app.baseDir, zipPath));
+                    }
+                });
+            } catch (error) {
+                this.log(error);
+            }
+        }
+        /**
          * 删除附件
          * @param {Object} ctx - egg全局变量
          * @return {void}

+ 21 - 4
app/controller/deal_bills_controller.js

@@ -19,6 +19,7 @@ const loadExcelType = {
 };
 const loadType = loadExcelType.display;
 const auditConst = require('../const/audit').ledger;
+const permissionConst = require('../const/account_permission');
 
 module.exports = app => {
     class DealBillsController extends app.BaseController {
@@ -48,6 +49,24 @@ module.exports = app => {
             ctx.body = responseData;
         }
 
+        async checkPermisision(ctx) {
+            if (!ctx.tender.data) throw '标段数据错误';
+
+            const tender = ctx.tender.data;
+            const isUser = tender.user_id === this.ctx.session.sessionUser.accountId;
+            const auditors = await this.service.ledgerAudit.getAuditors(tender.id, tender.ledger_times);
+            const auditorsId = this.ctx.helper._.map(auditors, 'audit_id');
+            const isAuditor  = auditorsId.indexOf(this.ctx.session.sessionUser.accountId) >= 0;
+            const upPermision = this.ctx.session.sessionUser.permission
+                ? this.ctx.session.sessionUser.permission.tender.indexOf('3') >= 0
+                : false;
+            if (!(((tender.ledger_status === auditConst.status.uncheck || tender.ledger_status === auditConst.status.checkNo) && isUser) ||
+                (tender.ledger_status === auditConst.status.checking && isAuditor) ||
+                (tender.ledger_status === auditConst.status.checked && isAuditor && upPermision))) {
+                throw '您无权进行该操作';
+            }
+        }
+
         /**
          * 导入Excel数据
          * @param ctx
@@ -56,6 +75,7 @@ module.exports = app => {
         async loadExcel(ctx) {
             let stream;
             try {
+                await this.checkPermisision(ctx);
                 stream = await ctx.getFileStream();
                 const create_time = Date.parse(new Date()) / 1000;
                 const fileInfo = path.parse(stream.filename);
@@ -165,10 +185,7 @@ module.exports = app => {
          */
         async update(ctx) {
             try {
-                if (!ctx.tender.data) throw '标段数据错误';
-                if (ctx.tender.data.user_id !== ctx.session.sessionUser.accountId ||
-                    (ctx.tender.ledger_status === auditConst.status.checking || ctx.tender.ledger_status === auditConst.status.checked))
-                    throw '您无权进行该操作';
+                await this.checkPermisision(ctx);
                 const data = JSON.parse(ctx.request.body.data);
                 const result = await ctx.service.dealBills.updateDatas(data);
                 ctx.body = { err: 0, msg: '', data: result };

+ 27 - 3
app/controller/ledger_controller.js

@@ -56,6 +56,18 @@ module.exports = app => {
                 (tender.ledger_status === auditConst.status.checking || tender.ledger_status === auditConst.status.checked);
         }
 
+        _canUpdateDealBills(tender, auditors) {
+            const isUser = tender.user_id === this.ctx.session.sessionUser.accountId;
+            const auditorsId = this.ctx.helper._.map(auditors, 'audit_id');
+            const isAuditor  = auditorsId.indexOf(this.ctx.session.sessionUser.accountId) >= 0;
+            const upPermission = this.ctx.session.sessionUser.permission
+                ? this.ctx.session.sessionUser.permission.tender.indexOf('3') >= 0
+                : false;
+            return ((tender.ledger_status === auditConst.status.uncheck || tender.ledger_status === auditConst.status.checkNo) && isUser) ||
+                (tender.ledger_status === auditConst.status.checking && isAuditor) ||
+                (tender.ledger_status === auditConst.status.checked && isAuditor && upPermission);
+        }
+
         /**
          * 获取SpreadSetting
          * @private
@@ -145,6 +157,7 @@ module.exports = app => {
                     jsFiles: this.app.jsFiles.common.concat(this.app.jsFiles.ledger.explode),
                     stdBills,
                     stdChapters,
+                    dealBillsPermission: this._canUpdateDealBills(tender.data, auditors.filter(x => {return x.audit_order > 0}))
                 };
                 if ((tender.data.ledger_status === auditConst.status.uncheck || tender.data.ledger_status === auditConst.status.checkNo) && tender.data.user_id === ctx.session.sessionUser.accountId) {
                     // renderData.accountGroup = accountGroup;
@@ -449,9 +462,21 @@ module.exports = app => {
                 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 }) : [];
-                const data = ctx.helper.checkBillsWithPos(ledgerData, posData,
+                const qtyData = ctx.helper.checkBillsWithPos(ledgerData, posData,
                     ['sgfh_qty', 'qtcl_qty', 'sjcl_qty', 'quantity']);
-                ctx.body = { err: 0, msg: '', data };
+                qtyData.error.forEach(x => { x.errorType = 'qty'; });
+                const tpData = ctx.helper.checkBillsTp(ledgerData, [
+                    {qty: 'sgfh_qty', tp: 'sgfh_tp'}, {qty: 'qtcl_qty', tp: 'qtcl_tp'},
+                    {qty: 'sjcl_qty', tp: 'sjcl_tp'}, {qty: 'quantity', tp: 'total_price'}
+                ], this.ctx.tender.info.decimal);
+                tpData.error.forEach(x => { x.errorType = 'tp'; });
+                ctx.body = { err: 0, msg: '', data: {
+                    error: [...qtyData.error, ...tpData.error],
+                    source: {
+                        bills: [...qtyData.source.bills, ...tpData.source.bills],
+                        pos: [...qtyData.source.pos, ...tpData.source.pos],
+                    },
+                }};
             } catch (err) {
                 this.log(err);
                 ctx.body = this.ajaxErrorBody(err, '检查数据错误');
@@ -603,7 +628,6 @@ module.exports = app => {
                         fileName = path.join(this.app.baseDir, 'app', 'public', 'files', 'template', 'ledger', '导入分项清单EXCEL格式.xls');
                         ctx.body = await fs.readFileSync(fileName);
                     } else if (file === '台账分解.xlsx') {
-                        console.log(file);
                         const create_time = Date.parse(new Date()) / 1000;
                         fileName = this.app.baseDir + '/app/public/files/ledger' + ctx.tender.id + '-' + create_time + '.xlsx';
                         const exportor = new exportExcel.exportLedger2Excel(ctx);

+ 1 - 1
app/controller/material_controller.js

@@ -1003,7 +1003,7 @@ module.exports = app => {
                 const zipFilename = `${ctx.tender.data.name}-材料调差-${ctx.params.order}-附件.zip`;
                 const time = Date.now();
                 const zipPath = `app/public/upload/${ctx.tender.id}/tc/fu_jian_zip${time}.zip`;
-                const size = await ctx.service.stageAtt.compressedFile(fileIds, zipPath);
+                const size = await ctx.service.materialFile.compressedFile(fileIds, zipPath);
 
                 // 解决中文无法下载问题
                 const userAgent = (ctx.request.header['user-agent'] || '').toLowerCase();

+ 15 - 3
app/controller/revise_controller.js

@@ -459,9 +459,21 @@ module.exports = app => {
                     ? JSON.parse(await fs.readFileSync(posFile, 'utf8'))
                     : await ctx.service.revisePos.getData(ctx.tender.id);
 
-                const data = ctx.helper.checkBillsWithPos(reviseBills, revisePos,
+                const qtyData = ctx.helper.checkBillsWithPos(reviseBills, revisePos,
                     ['sgfh_qty', 'qtcl_qty', 'sjcl_qty', 'quantity']);
-                ctx.body = { err: 0, msg: '', data };
+                qtyData.error.forEach(x => { x.errorType = 'qty'; });
+                const tpData = ctx.helper.checkBillsTp(reviseBills, [
+                    {qty: 'sgfh_qty', tp: 'sgfh_tp'}, {qty: 'qtcl_qty', tp: 'qtcl_tp'},
+                    {qty: 'sjcl_qty', tp: 'sjcl_tp'}, {qty: 'quantity', tp: 'total_price'}
+                ], this.ctx.tender.info.decimal);
+                tpData.error.forEach(x => { x.errorType = 'tp'; });
+                ctx.body = { err: 0, msg: '', data: {
+                        error: [...qtyData.error, ...tpData.error],
+                        source: {
+                            bills: [...qtyData.source.bills, ...tpData.source.bills],
+                            pos: [...qtyData.source.pos, ...tpData.source.pos],
+                        },
+                    }};
             } catch (err) {
                 this.log(err);
                 ctx.body = this.ajaxErrorBody(err, '检查数据错误');
@@ -514,7 +526,7 @@ module.exports = app => {
                 const data = JSON.parse(ctx.request.body.data);
                 if (!data || !data.rid || data.rid === '') throw '查询的台账修订有误';
                 const reviseInfo = await ctx.service.ledgerRevise.getRevise(ctx.tender.id, data.rid);
-                reviseInfo.end_time_str = reviseInfo.end_time ? reviseInfo.end_time.toLocaleString() : '';
+                reviseInfo.end_time_str = reviseInfo.end_time ? ctx.moment(reviseInfo.end_time).format('YYYY-MM-DD hh:mm:ss') : '';
                 ctx.body = { err: 0, msg: '', data: reviseInfo };
             } catch (err) {
                 this.log(err);

+ 20 - 19
app/controller/stage_controller.js

@@ -334,8 +334,20 @@ module.exports = app => {
             try {
                 const ledgerData = await this._getStageLedgerData(ctx);
                 const posData = await this._getStagePosData(ctx);
-                const data = ctx.helper.checkBillsWithPos(ledgerData, posData, ['contract_qty', 'qc_qty']);
-                ctx.body = { err: 0, msg: '', data };
+
+                const qtyData = ctx.helper.checkBillsWithPos(ledgerData, posData, ['contract_qty', 'qc_qty']);
+                qtyData.error.forEach(x => { x.errorType = 'qty'; });
+                const tpData = ctx.helper.checkBillsTp(ledgerData, [
+                    {qty: 'contract_qty', tp: 'contract_tp'}, {qty: 'qc_qty', tp: 'qc_tp'}
+                ], this.ctx.tender.info.decimal);
+                tpData.error.forEach(x => { x.errorType = 'tp'; });
+                ctx.body = { err: 0, msg: '', data: {
+                        error: [...qtyData.error, ...tpData.error],
+                        source: {
+                            bills: [...qtyData.source.bills, ...tpData.source.bills],
+                            pos: [...qtyData.source.pos, ...tpData.source.pos],
+                        },
+                    }};
             } catch (err) {
                 this.log(err);
                 ctx.body = this.ajaxErrorBody(err, '检查数据错误');
@@ -1017,13 +1029,11 @@ module.exports = app => {
                 }
 
                 await ctx.service.stageAudit.start(ctx.stage.id, ctx.stage.times);
-                // const auditor = await ctx.service.stageAudit.getCurAuditor(ctx.stage.id, ctx.stage.times);
-                // ctx.body = { err: 0, msg: '', data: [] };
-                ctx.redirect(ctx.request.header.referer);
+                ctx.body = { err: 0, msg: '', data: [] };
             } catch (err) {
                 this.log(err);
                 ctx.redirect(ctx.request.header.referer);
-                // ctx.body = this.ajaxErrorBody(err, '上报失败');
+                ctx.body = this.ajaxErrorBody(err, '上报失败');
             }
         }
         /**
@@ -1040,7 +1050,7 @@ module.exports = app => {
                 if (!this.ctx.stage.curAuditor || this.ctx.stage.curAuditor.aid !== ctx.session.sessionUser.accountId) {
                     throw '您无权进行该操作';
                 }
-                const data = ctx.request.body;
+                const data = JSON.parse(ctx.request.body.data);
                 data.checkType = parseInt(data.checkType);
                 // const data = {
                 //     checkType: parseInt(ctx.request.body.checkType),
@@ -1056,20 +1066,11 @@ module.exports = app => {
                 }
 
                 await ctx.service.stageAudit.check(ctx.stage.id, data, ctx.stage.times);
-                // const stageOrder = parseInt(ctx.params.order);
-                // const stage = await ctx.service.stage.getDataByCondition({
-                //     tid: ctx.tender.id,
-                //     order: stageOrder,
-                // });
-                // const auditor = await ctx.service.stageAudit.getCurAuditor(ctx.stage.id, ctx.stage.times);
-                // ctx.body = { err: 0, msg: '', data: [] };
-                ctx.redirect(ctx.request.header.referer);
+                ctx.body = { err: 0, msg: '', data: [] };
             } catch (err) {
-                // console.log(err);
                 this.log(err);
-                // ctx.body = this.ajaxErrorBody(err, '提交失败');
-                // ctx.session.postError = err.toString();
-                ctx.redirect(ctx.request.header.referer);
+                ctx.body = this.ajaxErrorBody(err, '提交失败');
+                ctx.session.postError = err.toString();
             }
         }
         /**

+ 97 - 0
app/controller/tender_controller.js

@@ -13,6 +13,8 @@ const codeRuleConst = require('../const/code_rule');
 const settingConst = require('../const/setting.js');
 const tenderMenu = require('../../config/menu').tenderMenu;
 const auditConst = require('../const/audit');
+const shenpiConst = require('../const/shenpi');
+const accountGroup = require('../const/account_group').group;
 const accountPermission = require('../const/account_permission');
 
 module.exports = app => {
@@ -423,8 +425,10 @@ module.exports = app => {
                     p.end_tp = sum;
                     p.end_ratio = ctx.helper.mul(ctx.helper.div(p.end_tp, tender.sum, 4), 100);
                 }
+                const revise = await ctx.service.ledgerRevise.getLastestRevise(tender.id);
                 const renderData = {
                     tender,
+                    revise,
                     tenderInfo: ctx.tender.info,
                     tenderMenu: this.menu.tenderMenu,
                     preUrl: '/tender/' + ctx.tender.id,
@@ -736,6 +740,99 @@ module.exports = app => {
 
             ctx.body = responseData;
         }
+
+        async shenpiSet(ctx) {
+            // 获取所有项目参与者
+            const accountList = await ctx.service.projectAccount.getAllDataByCondition({
+                where: { project_id: ctx.session.sessionProject.id, enable: 1 },
+                columns: ['id', 'name', 'company', 'role', 'enable', 'is_admin', 'account_group', 'mobile'],
+            });
+            const accountGroupList = accountGroup.map((item, idx) => {
+                const groupList = accountList.filter(item => item.account_group === idx);
+                return { groupName: item, groupList };
+            });
+            for (const sp of shenpiConst.sp_lc) {
+                sp.status = ctx.tender.info.shenpi ? JSON.parse(ctx.tender.info.shenpi)[sp.code] : shenpiConst.sp_status.sqspr;
+                if (sp.status === shenpiConst.sp_status.gdspl) {
+                    sp.auditList = await ctx.service.shenpiAudit.getAuditList(ctx.tender.id, sp.type, sp.status);
+                } else if (sp.status === shenpiConst.sp_status.gdzs) {
+                    sp.audit = await ctx.service.shenpiAudit.getAudit(ctx.tender.id, sp.type, sp.status);
+                }
+            }
+            // 获取固定审批流 or 固定终审
+            const renderData = {
+                shenpi: shenpiConst,
+                accountList,
+                accountGroup: accountGroupList,
+            };
+            await this._list('tender/shenpi.ejs', renderData);
+        }
+
+        async saveTenderInfoShenpi(ctx) {
+            try {
+                const data = JSON.parse(ctx.request.body.data);
+                if (!data) {
+                    throw '提交数据错误';
+                }
+                // 判断修改权限
+                if (ctx.session.sessionUser.is_admin === 0) {
+                    throw '你没有权限修改审批流程';
+                }
+                let postData = {};
+                if (!ctx.tender.info.shenpi) {
+                    for (const sp of shenpiConst.sp_lc) {
+                        if (sp.code === data.code) {
+                            postData[sp.code] = data.status;
+                        } else {
+                            postData[sp.code] = shenpiConst.sp_status.sqspr;
+                        }
+                    }
+                } else {
+                    postData = JSON.parse(ctx.tender.info.shenpi);
+                    postData[data.code] = data.status;
+                }
+                await ctx.service.tenderInfo.saveTenderInfo(ctx.tender.id, { shenpi: JSON.stringify(postData) });
+                let auditList = [];
+                if (data.status === shenpiConst.sp_status.gdspl) {
+                    auditList = await ctx.service.shenpiAudit.getAuditList(ctx.tender.id, shenpiConst.sp_type[data.code], data.status);
+                } else if (data.status === shenpiConst.sp_status.gdzs) {
+                    auditList = await ctx.service.shenpiAudit.getAudit(ctx.tender.id, shenpiConst.sp_type[data.code], data.status);
+                }
+                ctx.body = { err: 0, msg: '', data: auditList };
+            } catch (err) {
+                this.log(err);
+                ctx.body = this.ajaxErrorBody(err, '保存审批流程设置失败');
+            }
+        }
+
+        async saveShenpiAudit(ctx) {
+            try {
+                const data = JSON.parse(ctx.request.body.data);
+                if (!data) {
+                    throw '提交数据错误';
+                }
+                // 判断修改权限
+                if (ctx.session.sessionUser.is_admin === 0) {
+                    throw '你没有权限修改审批流程';
+                }
+                switch (data.type) {
+                    case 'add':
+                        const result = await ctx.service.shenpiAudit.addAudit(data);
+                        if (result) {
+                            throw '添加审批人失败';
+                        }
+                        break;
+                    case 'del':
+                        await ctx.service.shenpiAudit.removeAudit(data);
+                        break;
+                    default:break;
+                }
+                ctx.body = { err: 0, msg: '' };
+            } catch (err) {
+                this.log(err);
+                ctx.body = this.ajaxErrorBody(err, '保存审批流程设置失败');
+            }
+        }
     }
 
     return TenderController;

+ 15 - 0
app/extend/context.js

@@ -0,0 +1,15 @@
+'use strict';
+
+/**
+ *
+ *
+ * @author Mai
+ * @date
+ * @version
+ */
+
+const moment = require('moment');
+
+module.exports = {
+    moment: moment,
+};

+ 27 - 0
app/extend/helper.js

@@ -1176,6 +1176,33 @@ module.exports = {
         return result;
     },
 
+    checkBillsTp(bills, field, decimal) {
+        const result = {
+            error: [],
+            source: {
+                bills: [],
+                pos: [],
+            },
+        };
+        for (const b of bills) {
+            const checkData = {}, calcData = {};
+            for (const f of field) {
+                checkData[f.tp] = b[f.tp] || 0;
+                calcData[f.tp] = this.mul(b.unit_price, b[f.qty], decimal.tp) || 0;
+            }
+            if (!_.isMatch(checkData, calcData)) {
+                result.error.push({
+                    ledger_id: b.ledger_id,
+                    b_code: b.b_code,
+                    name: b.name,
+                    error: { checkData, calcData },
+                });
+                result.source.bills.push(b);
+            }
+        }
+        return result;
+    },
+
     check18MainCode(code) {
         return /^([0-9]([0-9][0-9])*)?(GD[0-9]{3}([0-9][0-9])*)?$/.test(code);
     },

+ 235 - 197
app/public/js/change_detail.js

@@ -1,197 +1,235 @@
-'use strict';
-
-/**
- * 变更令详细页js
- *
- * @author EllisRan.
- * @date 2018/11/22
- * @version
- */
-
-$.event.special.valuechange = {
-    teardown: function (namespaces) {
-        $(this).unbind('.valuechange');
-    },
-
-    handler: function (e) {
-        $.event.special.valuechange.triggerChanged($(this));
-    },
-
-    add: function (obj) {
-        $(this).on('keyup.valuechange cut.valuechange paste.valuechange input.valuechange', obj.selector, $.event.special.valuechange.handler)
-    },
-
-    triggerChanged: function (element) {
-        var current = element[0].contentEditable === 'true' ? element.html() : element.val()
-            , previous = typeof element.data('previous') === 'undefined' ? element[0].defaultValue : element.data('previous');
-        if (current !== previous) {
-            element.trigger('valuechange', [element.data('previous')]);
-            element.data('previous', current);
-        }
-    }
-};
-
-$(document).ready(() => {
-    // tab切换
-    $('a[data-toggle="tab"]').on('shown.bs.tab', function (e) {
-        const tab = $(this).attr('aria-controls');
-        $('.show_title').hide();
-        $('#'+ tab +'_title').show();
-        if (tab === 'bills' && $('#bills').hasClass('first-bill-pane')) {
-            if (table) {
-                table.destroy();
-            }
-            table = $('.table-list').removeAttr('width').DataTable(billsTable);
-            if (!$('.change-detail-checkbox').is(':checked')) {
-                const column = table.column(3);
-                column.visible(!column.visible());
-            }
-            $('#bills').removeClass('first-bill-pane');
-        }
-    });
-
-    // 上传附件
-    $('#upload-file-btn').click(function () {
-        const files = $('#upload-file')[0].files;
-        const formData = new FormData();
-        formData.append('cid', $('#changeId').val());
-        formData.append('tid', $('#tenderId').val());
-        for (const file of files) {
-            if (file === undefined) {
-                toastr.error('未选择上传文件!');
-                return false;
-            }
-            const filesize = file.size;
-            if (filesize > 30 * 1024 * 1024) {
-                toastr.error('文件大小过大!');
-                return false;
-            }
-            const fileext = '.' + file.name.toLowerCase().split('.').splice(-1)[0];
-            if (whiteList.indexOf(fileext) === -1) {
-                toastr.error('只能上传指定格式的附件!');
-                return false;
-            }
-            formData.append('size', filesize);
-            formData.append('file[]', file);
-        }
-        postDataWithFile('/change/upload/file', formData, function (data) {
-            $('#addfujian').modal('hide');
-            let html = '';
-            let index = $('#attList tr').length + 1;
-            for (const fileInfo of data) {
-                html += '<tr> ' +
-                    '<td>' + index + '</td> ' +
-                    `<td><a href="javascript: void(0);" class="file-atn" f-id="${fileInfo.id}">${fileInfo.filename}${fileInfo.fileext}</a></td>`+
-                    '<td>' + fileInfo.filesize + '</td> ' +
-                    '<td>' + fileInfo.in_time + '</td> ' +
-                    '<td> <a class="btn btn-light btn-sm delete-file" data-attid="' + fileInfo.id + '"  title="删除附件"><span class="fa fa-trash text-danger"></span></a> </td> ' +
-                    '</tr>';
-                ++index;
-            }
-            $('#attList').append(html);
-        }, function () {
-
-        });
-        $('#upload-file').val('');
-
-    });
-
-    // 删除附件
-    $('body').on('click', '.delete-file', function () {
-        let attid = $(this).data('attid');
-        console.log(attid);
-        let self = $(this);
-        const data = {id: attid};
-        postData('/change/delete/file', data, function (result) {
-            self.parents('tr').remove();
-            // 重新排序
-            let newsort = 1;
-            $('#attList tr').each(function(){
-                $(this).children('td').eq(0).text(newsort);
-                newsort++;
-            });
-        });
-    });
-    // /change/download/file/
-    $('#attList').on('click', '.file-atn', function() {
-        const id = $(this).attr('f-id')
-        postData(`/change/download/file/${id}`, {}, (data) => {
-            const { filepath } = data
-            $('#file-upload').attr('href', filepath)
-            $('#file-upload')[0].click()
-        })
-    })
-    //
-    const cca = getLocalCache('change-checkbox-account-' + accountId);
-    if (cca !== null && cca !== undefined) {
-        $('#customCheck1').prop('checked', cca !== 'false');
-    }
-    // 变更详情展示和隐藏
-    $('.change-detail-checkbox').on('click', function (e) {
-        if($(e.target).is('label')){
-            return;
-        }
-        let column = table.column(3);
-        // 设置用户项目本地记录展示和隐藏情况
-        setLocalCache('change-checkbox-account-'+ accountId, $(this).is(':checked'));
-        column.visible(!column.visible());
-    })
-
-    // 重新审批获取手机验证码
-    // 获取验证码
-    let isPosting = false;
-    $("#get-code").click(function() {
-        if (isPosting) {
-            return false;
-        }
-        const btn = $(this);
-
-        $.ajax({
-            url: '/profile/code?_csrf=' + csrf,
-            type: 'post',
-            data: { mobile: authMobile, type: 'shenpi' },
-            dataTye: 'json',
-            error: function() {
-                isPosting = false;
-            },
-            beforeSend: function() {
-                isPosting = true;
-            },
-            success: function(response) {
-                isPosting = false;
-                if (response.err === 0) {
-                    codeSuccess(btn);
-                    $("input[name='code']").removeAttr('readonly');
-                    $("#re-shenpi-btn").removeAttr('disabled');
-                } else {
-                    toast(response.msg, 'error');
-                }
-            }
-        });
-    });
-});
-/**
- * 获取成功后的操作
- *
- * @param {Object} btn - 点击的按钮
- * @return {void}
- */
-function codeSuccess(btn) {
-    let counter = 60;
-    btn.addClass('disabled').text('重新获取 ' + counter + 'S');
-    btn.parent().siblings('input').removeAttr('readonly').attr('placeholder', '输入短信中的6位验证码');
-    const bindBtn = $("#bind-btn");
-    bindBtn.removeClass('btn-secondary disabled').addClass('btn-primary');
-
-    const countDown = setInterval(function() {
-        const countString = counter - 1 <= 0 ? '' : ' ' + (counter - 1) + 'S';
-        // 倒数结束后
-        if (countString === '') {
-            clearInterval(countDown);
-            btn.removeClass('disabled');
-        }
-        const text = '重新获取' + countString;
-        btn.text(text);
-        counter -= 1;
-    }, 1000);
-}
+'use strict';
+
+/**
+ * 变更令详细页js
+ *
+ * @author EllisRan.
+ * @date 2018/11/22
+ * @version
+ */
+
+$.event.special.valuechange = {
+    teardown: function (namespaces) {
+        $(this).unbind('.valuechange');
+    },
+
+    handler: function (e) {
+        $.event.special.valuechange.triggerChanged($(this));
+    },
+
+    add: function (obj) {
+        $(this).on('keyup.valuechange cut.valuechange paste.valuechange input.valuechange', obj.selector, $.event.special.valuechange.handler)
+    },
+
+    triggerChanged: function (element) {
+        var current = element[0].contentEditable === 'true' ? element.html() : element.val()
+            , previous = typeof element.data('previous') === 'undefined' ? element[0].defaultValue : element.data('previous');
+        if (current !== previous) {
+            element.trigger('valuechange', [element.data('previous')]);
+            element.data('previous', current);
+        }
+    }
+};
+
+$(document).ready(() => {
+    // tab切换
+    $('a[data-toggle="tab"]').on('shown.bs.tab', function (e) {
+        const tab = $(this).attr('aria-controls');
+        $('.show_title').hide();
+        $('#'+ tab +'_title').show();
+        if (tab === 'bills' && $('#bills').hasClass('first-bill-pane')) {
+            if (table) {
+                table.destroy();
+            }
+            table = $('.table-list').removeAttr('width').DataTable(billsTable);
+            if (!$('.change-detail-checkbox').is(':checked')) {
+                const column = table.column(3);
+                column.visible(!column.visible());
+            }
+            $('#bills').removeClass('first-bill-pane');
+        }
+    });
+
+    // 上传附件
+    $('#upload-file-btn').click(function () {
+        const files = $('#upload-file')[0].files;
+        const formData = new FormData();
+        formData.append('cid', $('#changeId').val());
+        formData.append('tid', $('#tenderId').val());
+        for (const file of files) {
+            if (file === undefined) {
+                toastr.error('未选择上传文件!');
+                return false;
+            }
+            const filesize = file.size;
+            if (filesize > 30 * 1024 * 1024) {
+                toastr.error('文件大小过大!');
+                return false;
+            }
+            const fileext = '.' + file.name.toLowerCase().split('.').splice(-1)[0];
+            if (whiteList.indexOf(fileext) === -1) {
+                toastr.error('只能上传指定格式的附件!');
+                return false;
+            }
+            formData.append('size', filesize);
+            formData.append('file[]', file);
+        }
+        postDataWithFile('/change/upload/file', formData, function (data) {
+            $('#addfujian').modal('hide');
+            let html = '';
+            let index = $('#attList tr').length + 1;
+            for (const fileInfo of data) {
+                html += '<tr> ' +
+                    `<td width="25"><input type="checkbox" class="check-file" file-id=${fileInfo.id}></td>` +
+                    '<td>' + index + '</td> ' +
+                    `<td><a href="javascript: void(0);" class="file-atn" f-id="${fileInfo.id}">${fileInfo.filename}${fileInfo.fileext}</a></td>`+
+                    '<td>' + fileInfo.filesize + '</td> ' +
+                    '<td>' + fileInfo.in_time + '</td> ' +
+                    `<td>
+                        <a href="/change/download/file/${fileInfo.id}" class="btn btn-light btn-sm" title="下载"><span class="fa fa-download text-primary"></span></a>
+                        <a class="btn btn-light btn-sm delete-file" data-attid="${fileInfo.id}"  title="删除附件"><span class="fa fa-trash text-danger"></span></a>
+                    </td>`+
+                    // '<td> <a class="btn btn-light btn-sm delete-file" data-attid="' + fileInfo.id + '"  title="删除附件"><span class="fa fa-trash text-danger"></span></a> </td> ' +
+                    '</tr>';
+                ++index;
+            }
+            $('#attList').append(html);
+        }, function () {
+
+        });
+        $('#upload-file').val('');
+
+    });
+
+    // 删除附件
+    $('body').on('click', '.delete-file', function () {
+        let attid = $(this).data('attid');
+        let self = $(this);
+        const data = {id: attid};
+        postData('/change/delete/file', data, function (result) {
+            self.parents('tr').remove();
+            // 重新排序
+            let newsort = 1;
+            $('#attList tr').each(function(){
+                $(this).children('td').eq(1).text(newsort);
+                newsort++;
+            });
+        });
+    });
+    // /change/download/file/
+    $('#attList').on('click', '.file-atn', function() {
+        const id = $(this).attr('f-id')
+        postData(`/change/download/file/${id}`, {}, (data) => {
+            const { filepath } = data
+            $('#file-upload').attr('href', filepath)
+            $('#file-upload')[0].click()
+        })
+    })
+
+    $('#attList').on('click', '.check-file', function() {
+
+        const checkedList = $('#attList').find('input:checked')
+        const childs = $('#attList').children().length
+        const checkBox = $('#check-all-file')
+        if (checkedList.length === childs) {
+            checkBox.prop("checked", true)
+        } else {
+            checkBox.prop("checked", false)
+        }
+    })
+    $('#check-all-file').click(function() {
+        const isCheck = $(this).is(':checked')
+        $('#attList').children().each(function() {
+            $(this).find('input:checkbox').prop("checked", isCheck)
+        })
+    });
+
+    $('#bach-download').click(function() {
+        const fileIds = []
+        $( '#attList .check-file:checked').each(function() {
+            const fileId = $(this).attr('file-id')
+            fileId && fileIds.push(fileId)
+        })
+
+        if (fileIds.length) {
+            const tid = $('#tenderId').val()
+            const cid = $('#changeId').val()
+            $('#downloadZip').attr('href', `/tender/${tid}/change/${cid}/download/compresse-file?fileIds=${JSON.stringify(fileIds)}`);
+            $('#downloadZip')[0].click();
+        }
+    });
+
+    //
+    const cca = getLocalCache('change-checkbox-account-' + accountId);
+    if (cca !== null && cca !== undefined) {
+        $('#customCheck1').prop('checked', cca !== 'false');
+    }
+    // 变更详情展示和隐藏
+    $('.change-detail-checkbox').on('click', function (e) {
+        if($(e.target).is('label')){
+            return;
+        }
+        let column = table.column(3);
+        // 设置用户项目本地记录展示和隐藏情况
+        setLocalCache('change-checkbox-account-'+ accountId, $(this).is(':checked'));
+        column.visible(!column.visible());
+    })
+
+    // 重新审批获取手机验证码
+    // 获取验证码
+    let isPosting = false;
+    $("#get-code").click(function() {
+        if (isPosting) {
+            return false;
+        }
+        const btn = $(this);
+
+        $.ajax({
+            url: '/profile/code?_csrf=' + csrf,
+            type: 'post',
+            data: { mobile: authMobile, type: 'shenpi' },
+            dataTye: 'json',
+            error: function() {
+                isPosting = false;
+            },
+            beforeSend: function() {
+                isPosting = true;
+            },
+            success: function(response) {
+                isPosting = false;
+                if (response.err === 0) {
+                    codeSuccess(btn);
+                    $("input[name='code']").removeAttr('readonly');
+                    $("#re-shenpi-btn").removeAttr('disabled');
+                } else {
+                    toast(response.msg, 'error');
+                }
+            }
+        });
+    });
+});
+/**
+ * 获取成功后的操作
+ *
+ * @param {Object} btn - 点击的按钮
+ * @return {void}
+ */
+function codeSuccess(btn) {
+    let counter = 60;
+    btn.addClass('disabled').text('重新获取 ' + counter + 'S');
+    btn.parent().siblings('input').removeAttr('readonly').attr('placeholder', '输入短信中的6位验证码');
+    const bindBtn = $("#bind-btn");
+    bindBtn.removeClass('btn-secondary disabled').addClass('btn-primary');
+
+    const countDown = setInterval(function() {
+        const countString = counter - 1 <= 0 ? '' : ' ' + (counter - 1) + 'S';
+        // 倒数结束后
+        if (countString === '') {
+            clearInterval(countDown);
+            btn.removeClass('disabled');
+        }
+        const text = '重新获取' + countString;
+        btn.text(text);
+        counter -= 1;
+    }, 1000);
+}

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

@@ -706,8 +706,8 @@ $(document).ready(function() {
                                 const exprInfo = getExprInfo(colSetting.field);
                                 if (exprInfo) {
                                     data[exprInfo.expr] = value;
+                                    bPaste = true;
                                 }
-                                bPaste = true;
                             } catch(err) {
                                 toastMessageUniq(hint.invalidExpr);
                             }

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

@@ -250,7 +250,7 @@ $(document).ready(function () {
             const fileId = $(this).attr('file-id')
             fileId && fileIds.push(fileId)
         })
-        console.log('fileIds', fileIds)
+        // console.log('fileIds', fileIds)
 
         if (fileIds.length) {
             // postData( `/tender/${tid}/measure/material/${order}/file/download/compresse-file`, { fileIds })

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

@@ -57,9 +57,19 @@ const showSideTools = function (show) {
         if (!setting.spreadSetting) {
             setting.spreadSetting = {
                 cols: [
-                    {title: '行号', field: 'serialNo', width: 80, formatter: '@'},
-                    {title: '清单编号', field: 'b_code', width: 150, formatter: '@'},
-                    {title: '清单名称', field: 'name', width: 230, formatter: '@'},
+                    {title: '行号', field: 'serialNo', width: 50, formatter: '@'},
+                    {
+                        title: '错误类型', field: 'errorType', width: 60, formatter: '@',
+                        getValue: function (x) {
+                            switch (x.errorType) {
+                                case 'qty': return '数量';
+                                case 'tp': return '金额';
+                                default: return '';
+                            }
+                        }
+                    },
+                    {title: '清单编号', field: 'b_code', width: 135, formatter: '@'},
+                    {title: '清单名称', field: 'name', width: 215, formatter: '@'},
                 ],
                 emptyRows: 0,
                 headRows: 1,

+ 258 - 0
app/public/js/shenpi.js

@@ -0,0 +1,258 @@
+'use strict';
+
+/**
+ *
+ *
+ * @author Ellisran
+ * @date 2020/10/09
+ * @version
+ */
+
+$(document).ready(function () {
+    let timer = null;
+    let oldSearchVal = null;
+    $('body').bind('input propertychange', '.gr-search', function(e) {
+        oldSearchVal = e.target.value;
+        timer && clearTimeout(timer);
+        timer = setTimeout(() => {
+            const newVal = $(this).val();
+            let html = '';
+            if (newVal && newVal === oldSearchVal) {
+                accountList.filter(item => item && cur_uid !== item.id && (item.name.indexOf(newVal) !== -1 || (item.mobile && item.mobile.indexOf(newVal) !== -1))).forEach(item => {
+                    html += `<dd class="border-bottom p-2 mb-0 " data-id="${item.id}" >
+                        <p class="mb-0 d-flex"><span class="text-primary">${item.name}</span><span
+                                class="ml-auto">${item.mobile || ''}</span></p>
+                        <span class="text-muted">${item.role || ''}</span>
+                    </dd>`
+                })
+                $('.book-list').empty();
+                $('.book-list').append(html);
+            } else {
+                if (!$('.acc-btn').length) {
+                    accountGroup.forEach((group, idx) => {
+                        if (!group) return;
+                        html += `<dt><a href="javascript: void(0);" class="acc-btn" data-groupid="${idx}" data-type="hide"><i class="fa fa-plus-square"></i>
+                        </a> ${group.groupName}</dt>
+                        <div class="dd-content" data-toggleid="${idx}">`
+                        group.groupList.forEach(item => {
+                            if (item.id !== cur_uid) {
+                                html += `<dd class="border-bottom p-2 mb-0 " data-id="${item.id}" >
+                                    <p class="mb-0 d-flex"><span class="text-primary">${item.name}</span><span
+                                            class="ml-auto">${item.mobile || ''}</span></p>
+                                    <span class="text-muted">${item.role || ''}</span>
+                                </dd>`;
+                            }
+                        });
+                        html += '</div>';
+                    });
+                    $('.book-list').empty();
+                    $('.book-list').append(html);
+                }
+            }
+        }, 400);
+    });
+
+    // 添加审批流程按钮逻辑
+    $('body').on('click', '.book-list dt', function () {
+        const idx = $(this).find('.acc-btn').attr('data-groupid');
+        const type = $(this).find('.acc-btn').attr('data-type');
+        if (type === 'hide') {
+            $(this).parent().find(`div[data-toggleid="${idx}"]`).show(() => {
+                $(this).children().find('i').removeClass('fa-plus-square').addClass('fa-minus-square-o');
+                $(this).find('.acc-btn').attr('data-type', 'show');
+
+            })
+        } else {
+            $(this).parent().find(`div[data-toggleid="${idx}"]`).hide(() => {
+                $(this).children().find('i').removeClass('fa-minus-square-o').addClass('fa-plus-square');
+                $(this).find('.acc-btn').attr('data-type', 'hide');
+            })
+        }
+        return false;
+    });
+
+    // 更改审批流程状态
+    $('.form-check input').on('change', function () {
+        // 获取所有审批的checked值并更新
+        const this_status = parseInt($(this).val());
+        const this_code = $(this).data('code');
+        const spt = sp_status_list[this_status];
+        $(this).parents('.form-group').siblings('.alert-warning').text(spt.name + ':' + spt.msg);
+        // 拼接post json
+        const prop = {
+            code: this_code,
+            status: this_status
+        };
+        const _self = $(this);
+        const tenderId = window.location.pathname.split('/')[2];
+        postData('/tender/' + tenderId + '/shenpi/save', prop, function (data) {
+            if (this_status === sp_status.sqspr) {
+                _self.parents('.form-group').siblings('.lc-show').html('');
+            } else if (this_status === sp_status.gdspl) {
+                let addhtml = '<ul class="list-unstyled">\n';
+                if (data.length !== 0) {
+                    for(const [i, audit] of data.entries()) {
+                        addhtml += makeAudit(audit, transFormToChinese(i+1));
+                    }
+                    addhtml += '<li class="pl-3"><a href="javascript:void(0);" class="add-audit"><i class="fa fa-plus"></i> 添加流程</a></li>';
+                } else {
+                    addhtml += makeSelectAudit(this_code, '一');
+                }
+                addhtml += '</ul>\n';
+                _self.parents('.form-group').siblings('.lc-show').html(addhtml);
+            } else if (this_status === sp_status.gdzs) {
+                let addhtml = '<ul class="list-unstyled">\n' +
+                    '                                        <li class="d-flex justify-content-start mb-3">\n' +
+                    '                                            <span class="col-auto">授权审批人</span>\n' +
+                    '                                            <span class="col-7">\n' +
+                    '                                                <span class="d-inline-block"></span>\n' +
+                    '                                            </span>\n' +
+                    '                                        </li>\n';
+                addhtml += data ? makeAudit(data) : makeSelectAudit(this_code);
+                addhtml += '</ul>\n';
+                _self.parents('.form-group').siblings('.lc-show').html(addhtml);
+            }
+        });
+    });
+
+    // 选中审批人
+    $('body').on('click', 'dl dd', function () {
+        const id = parseInt($(this).data('id'));
+        if (id) {
+            const user = _.find(accountList, function (item) {
+                return item.id === id;
+            });
+            const this_status = parseInt($(this).parents('.lc-show').siblings('.form-group').find('input:checked').val());
+            const this_code = $(this).parents('.lc-show').siblings('.form-group').find('input:checked').data('code');
+            if (this_status === sp_status.gdspl) {
+                // 判断是否已存在审批人
+                const aid_num = $(this).parents('ul').find('.remove-audit').length;
+                for (let i = 0; i < aid_num; i++) {
+                    const aid = parseInt($(this).parents('ul').find('.remove-audit').eq(i).data('id'));
+                    if (aid === id) {
+                        toastr.warning('该审核人已存在,请勿重复添加');
+                        return;
+                    }
+                }
+
+            }
+            const prop = {
+                status: this_status,
+                code: sp_type[this_code],
+                audit_id: id,
+                type: 'add',
+            };
+            const _self = $(this);
+            const tenderId = window.location.pathname.split('/')[2];
+            postData('/tender/' + tenderId + '/shenpi/audit/save', prop, function (data) {
+                if (this_status === sp_status.gdspl) {
+                    _self.parents('ul').append('<li class="pl-3"><a href="javascript:void(0);" class="add-audit"><i class="fa fa-plus"></i> 添加流程</a></li>');
+                }
+                _self.parents('.spr-span').html('<span class="d-inline-block"></span>\n' +
+                    '<span class="d-inline-block"><span class="badge badge-light">'+ user.name +' <a href="javascript:void(0);" class="remove-audit btn-sm text-danger px-1" title="移除" data-id="'+ user.id +'"><i class="fa fa-remove"></i></a></span> </span>');
+            });
+        }
+    });
+
+    // 移除审批人
+    $('body').on('click', '.remove-audit', function () {
+        const id = parseInt($(this).data('id'));
+        const this_status = parseInt($(this).parents('.lc-show').siblings('.form-group').find('input:checked').val());
+        const this_code = $(this).parents('.lc-show').siblings('.form-group').find('input:checked').data('code');
+        const prop = {
+            status: this_status,
+            code: sp_type[this_code],
+            audit_id: id,
+            type: 'del',
+        };
+        const _self = $(this);
+        const tenderId = window.location.pathname.split('/')[2];
+        postData('/tender/' + tenderId + '/shenpi/audit/save', prop, function (data) {
+            if (this_status === sp_status.gdspl) {
+                const _selflc = _self.parents('.lc-show');
+                _self.parents('li').remove();
+                const aid_num = parseInt(_selflc.children('ul').find('li.d-flex').length);
+                if (aid_num === 0) {
+                    let addhtml = '<ul class="list-unstyled">\n';
+                    addhtml += makeSelectAudit(this_code, '一');
+                    addhtml += '</ul>\n';
+                    _selflc.html(addhtml);
+                } else {
+                    for (let i = 0; i < aid_num; i++) {
+                        _selflc.find('li.d-flex').eq(i).find('.col-auto').text(transFormToChinese(i+1) + '审');
+                    }
+                }
+            } else if (this_status === sp_status.gdzs) {
+                let addhtml = '<ul class="list-unstyled">\n' +
+                    '                                        <li class="d-flex justify-content-start mb-3">\n' +
+                    '                                            <span class="col-auto">授权审批人</span>\n' +
+                    '                                            <span class="col-7">\n' +
+                    '                                                <span class="d-inline-block"></span>\n' +
+                    '                                            </span>\n' +
+                    '                                        </li>\n';
+                addhtml += makeSelectAudit(this_code);
+                addhtml += '</ul>\n';
+                _self.parents('.lc-show').html(addhtml);
+            }
+        })
+    });
+
+    // 固定审批流-添加流程
+    $('body').on('click', '.add-audit', function () {
+        const num = $(this).parents('ul').children('li').length;
+        const this_code = $(this).parents('.lc-show').siblings('.form-group').find('input:checked').data('code');
+        const addhtml = makeSelectAudit(this_code, transFormToChinese(num));
+        $(this).parents('ul').append(addhtml);
+        $(this).parents('li').remove();
+    });
+
+    // 审批流程-审批人html 生成
+    function makeAudit(audit, i = '终') {
+        return '<li class="d-flex justify-content-start mb-3">\n' +
+            '                                            <span class="col-auto">'+ i +'审</span>\n' +
+            '                                            <span class="col-7 spr-span">\n' +
+            '                                            <span class="d-inline-block"></span>\n' +
+            '                                            <span class="d-inline-block"><span class="badge badge-light">'+ audit.name +' <a href="javascript:void(0);" class="remove-audit btn-sm text-danger px-1" title="移除" data-id="'+ audit.audit_id +'"><i class="fa fa-remove"></i></a></span> </span>\n' +
+            '                                            </span>\n' +
+            '                                        </li>';
+    }
+
+    // 审批流程-选择审批人html 生成
+    function makeSelectAudit(code, i = '终') {
+        let divhtml = '';
+        accountGroup.forEach((group, idx) => {
+            let didivhtml = '';
+            if(group) {
+                group.groupList.forEach(item => {
+                    didivhtml += item.id !== cur_uid ? '<dd class="border-bottom p-2 mb-0 " data-id="' + item.id + '" >\n' +
+                        '<p class="mb-0 d-flex"><span class="text-primary">' + item.name + '</span><span\n' +
+                        '                                                                                class="ml-auto">' + item.mobile + '</span></p>\n' +
+                        '                                                                    <span class="text-muted">' + item.role + '</span>\n' +
+                        '                                                                    </dd>\n' : '';
+                });
+                divhtml += '<dt><a href="javascript: void(0);" class="acc-btn" data-groupid="' + idx + '" data-type="hide"><i class="fa fa-plus-square"></i></a> ' + group.groupName + '</dt>\n' +
+                    '                                                                <div class="dd-content" data-toggleid="' + idx + '">\n' + didivhtml +
+                    '                                                                </div>\n';
+            }
+        });
+        let html = '<li class="d-flex justify-content-start mb-3">\n' +
+            '                                            <span class="col-auto">' + i + '审</span>\n' +
+            '                                            <span class="col-7 spr-span">\n' +
+            '                                            <span class="d-inline-block">\n' +
+            '                                                <div class="dropdown text-right">\n' +
+            '                                                    <button class="btn btn-outline-primary btn-sm dropdown-toggle" type="button" id="' + code + '_dropdownMenuButton" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">\n' +
+            '                                                        选择审批人\n' +
+            '                                                    </button>\n' +
+            '                                                    <div class="dropdown-menu dropdown-menu-right" aria-labelledby="' + code + '_dropdownMenuButton" style="width:220px">\n' +
+            '                                                        <div class="mb-2 p-2"><input class="form-control form-control-sm gr-search"\n' +
+            '                                                                                     placeholder="姓名/手机 检索" autocomplete="off"></div>\n' +
+            '                                                        <dl class="list-unstyled book-list">\n' + divhtml +
+            '                                                        </dl>\n' +
+            '                                                    </div>\n' +
+            '                                                </div>\n' +
+            '                                            </span>\n' +
+            '                                        </span>\n' +
+            '                                        </li>';
+        return html;
+    }
+});

+ 141 - 46
app/public/js/stage.js

@@ -962,7 +962,7 @@ $(document).ready(() => {
             }
         },
         measureAllPosInNode(node, ratio = 1) {
-            const posterity = stageTree.getPosterity(node)
+            const posterity = stageTree.getPosterity(node);
             const data = {updateType: 'update', updateData: []};
             for (const p of posterity) {
                 if (p.children && p.children.length > 0) continue;
@@ -1089,6 +1089,46 @@ $(document).ready(() => {
                 }
             }
         },
+        measureByBatch: function (posNames, ratio, apply2sibling) {
+            if (posNames.length <= 0) return;
+            if (ratio <= 0) return;
+
+            const sheet = slSpread.getActiveSheet();
+            const node = SpreadJsObj.getSelectObject(sheet);
+            const parent = stageTree.getParent(node);
+            const nodes = apply2sibling === true ? (parent ? parent.children : stageTree.children) : [node];
+            const data = {updateType: 'batchUpdate', updateData: []};
+            for (const node of nodes) {
+                const posRange = stagePos.getLedgerPos(node.id);
+                if (!posRange || posRange <= 0) continue;
+
+                for (const p of posRange) {
+                    if (posNames.indexOf(p.name) < 0) continue;
+                    data.updateData.push({
+                        pid: p.id, lid: p.lid,
+                        contract_qty: ZhCalc.mul(p.quantity, ratio)
+                    });
+                }
+            }
+            postData(window.location.pathname + '/update', {pos: data}, function (result) {
+                if (result.pos) {
+                    stagePos.updateDatas(result.pos.pos);
+                    stagePos.loadCurStageData(result.pos.curStageData);
+                }
+                const nodes = stageTree.loadPostStageData(result.ledger);
+                stageTreeSpreadObj.refreshTreeNodes(slSpread.getActiveSheet(), nodes);
+                stagePosSpreadObj.loadCurPosData();
+                if (detail) {
+                    detail.loadStagePosUpdateData(result);
+                } else {
+                    stageIm.loadUpdatePosData(result);
+                }
+                $('#calc-by-ratio').modal('hide');
+            }, function () {
+                stagePosSpreadObj.loadCurPosData();
+                $('#calc-by-ratio').modal('hide');
+            });
+        }
     };
     slSpread.bind(spreadNS.Events.EditEnded, stageTreeSpreadObj.editEnded);
     slSpread.bind(spreadNS.Events.SelectionChanged, stageTreeSpreadObj.selectionChanged);
@@ -1673,63 +1713,89 @@ $(document).ready(() => {
             });
         });
     }
-    if (!checkTzMeasureType()) {
-        const mergePeg = NewMergePeg({ callback: stagePosSpreadObj.addPegs});
-        $.contextMenu({
-            selector: '#stage-pos',
-            build: function ($trigger, e) {
-                const target = SpreadJsObj.safeRightClickSelection($trigger, e, spSpread);
-                return target.hitTestType === spreadNS.SheetArea.viewport || target.hitTestType === spreadNS.SheetArea.rowHeader;
-            },
-            items: {
-                'del': {
-                    name: '删除',
-                    icon: 'fa-remove',
-                    disabled: function (key, opt) {
-                        const sheet = spSpread.getActiveSheet();
-                        if (sheet.zh_data && !readOnly) {
-                            const selection = sheet.getSelections();
-                            if (selection && selection[0]) {
-                                let valid = sheet.zh_data.length < selection[0].row + selection[0].rowCount;
-                                for (let iRow = 0; iRow < selection[0].rowCount; iRow++) {
-                                    const posData = sheet.zh_data[selection[0].row + iRow];
-                                    if (posData) {
-                                        if (posData.add_stage_order < stage.order || ZhCalc.isNonZero(posData.gather_qty) || ZhCalc.isNonZero(posData.end_gather_qty)) {
-                                            valid = true;
-                                            break;
-                                        }
-                                    } else {
+
+    const mergePeg = NewMergePeg({ callback: stagePosSpreadObj.addPegs});
+    $.contextMenu({
+        selector: '#stage-pos',
+        build: function ($trigger, e) {
+            const target = SpreadJsObj.safeRightClickSelection($trigger, e, spSpread);
+            return target.hitTestType === spreadNS.SheetArea.viewport || target.hitTestType === spreadNS.SheetArea.rowHeader;
+        },
+        items: {
+            'del': {
+                name: '删除',
+                icon: 'fa-remove',
+                visible: function (key, opt) {
+                    return !checkTzMeasureType();
+                },
+                disabled: function (key, opt) {
+                    const sheet = spSpread.getActiveSheet();
+                    if (sheet.zh_data && !readOnly) {
+                        const selection = sheet.getSelections();
+                        if (selection && selection[0]) {
+                            let valid = sheet.zh_data.length < selection[0].row + selection[0].rowCount;
+                            for (let iRow = 0; iRow < selection[0].rowCount; iRow++) {
+                                const posData = sheet.zh_data[selection[0].row + iRow];
+                                if (posData) {
+                                    if (posData.add_stage_order < stage.order || ZhCalc.isNonZero(posData.gather_qty) || ZhCalc.isNonZero(posData.end_gather_qty)) {
                                         valid = true;
                                         break;
                                     }
+                                } else {
+                                    valid = true;
+                                    break;
                                 }
-                                return valid;
-                            } else {
-                                return true;
                             }
+                            return valid;
                         } else {
                             return true;
                         }
-                    },
-                    callback: function (key, opt) {
-                        stagePosSpreadObj.deletePos(spSpread.getActiveSheet());
+                    } else {
+                        return true;
                     }
                 },
-                'merge-peg': {
-                    name: '合并起讫桩号',
-                    disabled: function (key, opt) {
-                        const node = SpreadJsObj.getSelectObject(slSpread.getActiveSheet());
-                        return _.isNil(node) || _.isNil(node.b_code) || node.b_code === '';
-                    },
-                    callback: function (key, opt) {
-                        mergePeg.show();
-                    }
+                callback: function (key, opt) {
+                    stagePosSpreadObj.deletePos(spSpread.getActiveSheet());
+                }
+            },
+            'merge-peg': {
+                name: '合并起讫桩号',
+                visible: function (key, opt) {
+                    return !checkTzMeasureType();
+                },
+                disabled: function (key, opt) {
+                    const node = SpreadJsObj.getSelectObject(slSpread.getActiveSheet());
+                    return _.isNil(node) || _.isNil(node.b_code) || node.b_code === '';
+                },
+                callback: function (key, opt) {
+                    mergePeg.show();
+                }
+            },
+            'calcByRatio': {
+                name: '按比例计量',
+                visible: function (key, opt) {
+                    const data = spSpread.getActiveSheet().zh_data;
+                    return data && data.length > 0;
+                },
+                callback: function (key, opt) {
+                    $('#cbr-ratio').val('');
+                    $('#apply2sibling')[0].checked = false;
+                    $('#cbr-check-all')[0].checked = false;
+                    const html = [];
+                    for (const [i, p] of spSpread.getActiveSheet().zh_data.entries()) {
+                        html.push('<tr>');
+                        html.push('<td>', i, '</td>');
+                        html.push('<td>', p.name, '</td>');
+                        html.push('<td>', p.quantity, '</td>');
+                        html.push('<td><input type="checkbox" pos-name="' + p.name + '"></td></td>');
+                        html.push('</tr>');
+                    }
+                    $('#cbr-pos-list').html(html.join(''));
+                    $('#calc-by-ratio').modal('show');
                 }
             }
-        })
-    } else {
-        SpreadJsObj.forbiddenSpreadContextMenu('#stage-pos', spSpread);
-    }
+        }
+    });
     $.subMenu({
         menu: '#sub-menu', miniMenu: '#sub-mini-menu', miniMenuList: '#mini-menu-list',
         toMenu: '#to-menu', toMiniMenu: '#to-mini-menu',
@@ -3622,4 +3688,33 @@ $(document).ready(() => {
 
         SpreadExcelObj.exportSimpleXlsxSheet(setting, data, $('.sidebar-title').attr('data-original-title') + "计量台账.xlsx");
     });
+
+    $('#cbr-check-all').click(function () {
+        if (this.checked) {
+            $('input', '#cbr-pos-list').attr('checked', 'checked');
+        } else {
+            $('input', '#cbr-pos-list').removeAttr('checked');
+        }
+    });
+    $('#cbr-ok').click(() => {
+        const ratio = parseInt($('#cbr-ratio').val());
+        if (!ratio) {
+            toastr.warning('请输入计量比例');
+            return;
+        } else if (ratio < 1) {
+            toastr.warning('计量比例不可小于1');
+            return;
+        } else if (ratio > 100) {
+            toastr.warning('计量比例不可大于100');
+            return;
+        }
+
+        const apply2sibling = $('#apply2sibling')[0].checked;
+        const posName = _.map($('input:checked', '#cbr-pos-list'), function (x) {return $(x).attr('pos-name')});
+        if (posName.length === 0) {
+            toastr.warning('请勾选需要按计量比例的计量单元');
+            return;
+        }
+        stageTreeSpreadObj.measureByBatch(posName, ZhCalc.div(ratio, 100), apply2sibling);
+    })
 });

+ 4 - 0
app/router.js

@@ -107,6 +107,9 @@ module.exports = app => {
     app.post('/tender/:id/save', sessionAuth, tenderCheck, 'tenderController.saveTenderInfo');
     app.post('/tender/rule', sessionAuth, 'tenderController.rule');
     app.post('/tender/:id/rule/first', sessionAuth, tenderCheck, 'tenderController.ruleFirst');
+    app.get('/tender/:id/shenpi', sessionAuth, tenderCheck, 'tenderController.shenpiSet');
+    app.post('/tender/:id/shenpi/save', sessionAuth, tenderCheck, 'tenderController.saveTenderInfoShenpi');
+    app.post('/tender/:id/shenpi/audit/save', sessionAuth, tenderCheck, 'tenderController.saveShenpiAudit');
 
     // 预付款
     app.get('/tender/:id/advance', sessionAuth, tenderCheck, 'advanceController.index');
@@ -305,6 +308,7 @@ module.exports = app => {
     app.get('/change/download/file/:id', sessionAuth, 'changeController.downloadFile');
     app.post('/change/download/file/:id', sessionAuth, 'changeController.checkFile');
     app.post('/change/delete/file', sessionAuth, 'changeController.deleteFile');
+    app.get('/tender/:id/change/:cid/download/compresse-file', sessionAuth, tenderCheck, uncheckTenderCheck, 'changeController.downloadZip');
     app.post('/tender/:id/change/delete', sessionAuth, tenderCheck, uncheckTenderCheck, 'changeController.delete');
     app.post('/tender/:id/change/bills', sessionAuth, tenderCheck, uncheckTenderCheck, 'changeController.bills');
 

+ 10 - 5
app/service/change.js

@@ -956,13 +956,18 @@ module.exports = app => {
          * @return {Promise<*>} - 可用的变更令列表
          */
         async getValidChanges(tid, bills, pos) {
+            const self = this;
+            const getFilterPart = function (field, value) {
+                return value
+                    ? field + ' = ' + self.db.escape(value)
+                    : self.db.format("(?? = null or ?? = '')", [field, field]);
+            };
             const timesLen = 100;
-            const filter =
-                'cb.`code` = ' + this.db.escape(bills.b_code) +
-                ' And cb.`name` = ' + this.db.escape(bills.name) +
-                ' And cb.`unit` = ' + this.db.escape(bills.unit) +
+            const filter = getFilterPart('cb.code', bills.b_code) +
+                ' And ' + getFilterPart('cb.name', bills.name) +
+                ' And ' + getFilterPart('cb.unit', bills.unit) +
                 ' And cb.`unit_price` = ' + this.db.escape(bills.unit_price) +
-                (pos ? ' And cb.`bwmx` = ' + this.db.escape(pos.name) : '');
+                (pos ? getFilterPart('cb.bwmx', pos.name) : '');
             const sql =
                 'SELECT c.cid, c.code, c.name, c.w_code, c.p_code, c.peg, c.org_name, c.org_code, c.new_name, c.new_code,' +
                 '    c.content, c.basis, c.memo, c.type, c.class, c.quality, c.company, c.charge, ' +

+ 134 - 76
app/service/change_att.js

@@ -1,76 +1,134 @@
-'use strict';
-
-/**
- *
- *  附件
- * @author Ellisran
- * @date 2019/1/11
- * @version
- */
-
-module.exports = app => {
-    class ChangeAtt extends app.BaseService {
-        /**
-         * 构造函数
-         *
-         * @param {Object} ctx - egg全局变量
-         * @return {void}
-         */
-        constructor(ctx) {
-            super(ctx);
-            this.tableName = 'change_attachment';
-        }
-
-        /**
-         * 添加附件
-         * @param {Object} postData - 表单信息
-         * @param {Object} fileData - 文件信息
-         * @param {int} uid - 上传者id
-         * @return {void}
-         */
-        async save(postData, fileData, uid) {
-            const data = {
-                tid: postData.tid,
-                cid: postData.cid,
-                uid,
-            };
-            Object.assign(data, fileData);
-            const result = await this.db.insert(this.tableName, data);
-            return result;
-        }
-
-        /**
-         * 获取 变更令 所有附件
-         * @param {uuid} cid - 变更令id
-         * @return {Promise<void>}
-         */
-        async getChangeAttachment(cid) {
-            const sql = 'SELECT ca.*, pa.name As u_name, pa.role As u_role ' +
-                '  FROM ?? As ca ' +
-                '  Left Join ?? As pa ' +
-                '  On ca.uid = pa.id ' +
-                '  Where ca.cid = ?';
-            const sqlParam = [this.tableName, this.ctx.service.projectAccount.tableName, cid];
-            return await this.db.query(sql, sqlParam);
-        }
-
-        /**
-         * 获取所有附件
-         * @param {String} cid 变更令id
-         */
-        async getAllChangeFiles(cid) {
-            const { ctx } = this;
-            const result = await this.db.select(this.tableName, { where: { cid } });
-            return result.map(item => {
-                if (!ctx.helper.canPreview(item.fileext)) {
-                    item.filepath = `/change/download/file/${item.id}`;
-                } else {
-                    item.filepath = item.filepath.replace(/^app|\/app/, '');
-                }
-                return item;
-            });
-        }
-    }
-
-    return ChangeAtt;
-};
+'use strict';
+
+/**
+ *
+ *  附件
+ * @author Ellisran
+ * @date 2019/1/11
+ * @version
+ */
+
+const archiver = require('archiver');
+const path = require('path');
+const fs = require('fs');
+module.exports = app => {
+    class ChangeAtt extends app.BaseService {
+        /**
+         * 构造函数
+         *
+         * @param {Object} ctx - egg全局变量
+         * @return {void}
+         */
+        constructor(ctx) {
+            super(ctx);
+            this.tableName = 'change_attachment';
+        }
+
+        /**
+         * 添加附件
+         * @param {Object} postData - 表单信息
+         * @param {Object} fileData - 文件信息
+         * @param {int} uid - 上传者id
+         * @return {void}
+         */
+        async save(postData, fileData, uid) {
+            const data = {
+                tid: postData.tid,
+                cid: postData.cid,
+                uid,
+            };
+            Object.assign(data, fileData);
+            const result = await this.db.insert(this.tableName, data);
+            return result;
+        }
+
+        /**
+         * 获取 变更令 所有附件
+         * @param {uuid} cid - 变更令id
+         * @return {Promise<void>}
+         */
+        async getChangeAttachment(cid) {
+            const sql = 'SELECT ca.*, pa.name As u_name, pa.role As u_role ' +
+                '  FROM ?? As ca ' +
+                '  Left Join ?? As pa ' +
+                '  On ca.uid = pa.id ' +
+                '  Where ca.cid = ?';
+            const sqlParam = [this.tableName, this.ctx.service.projectAccount.tableName, cid];
+            return await this.db.query(sql, sqlParam);
+        }
+
+        /**
+         * 获取所有附件
+         * @param {String} cid 变更令id
+         */
+        async getAllChangeFiles(cid) {
+            const { ctx } = this;
+            const result = await this.db.select(this.tableName, { where: { cid } });
+            return result.map(item => {
+                if (!ctx.helper.canPreview(item.fileext)) {
+                    item.filepath = `/change/download/file/${item.id}`;
+                } else {
+                    item.filepath = item.filepath.replace(/^app|\/app/, '');
+                }
+                return item;
+            });
+        }
+
+        /**
+         * 将文件压缩成zip,并返回zip文件的路径
+         * @param {array} fileIds - 文件数组id
+         * @param {string} zipPath - 压缩文件存储路径
+         * @return {string} 压缩后的zip文件路径
+         */
+        async compressedFile(fileIds, zipPath) {
+            this.initSqlBuilder();
+            this.sqlBuilder.setAndWhere('id', {
+                value: fileIds,
+                operate: 'in',
+            });
+            const [sql, sqlParam] = this.sqlBuilder.build(this.tableName);
+            const files = await this.db.query(sql, sqlParam);
+            return new Promise(resolve => {
+                // 每次开一个新的archiver
+                const ziparchiver = archiver('zip');
+                const outputPath = fs.createWriteStream(path.resolve(this.app.baseDir, zipPath));
+                ziparchiver.pipe(outputPath);
+                files.forEach(item => {
+                    ziparchiver.file(path.resolve(this.app.baseDir, item.filepath), { name: item.filename + item.fileext });
+                });
+
+                // 存档警告
+                ziparchiver.on('warning', function(err) {
+                    console.log(err);
+                    if (err.code === 'ENOENT') {
+                        console.warn('stat故障和其他非阻塞错误');
+                    } else {
+                        throw err;
+                    }
+                });
+
+                // 存档出错
+                ziparchiver.on('error', function(err) {
+                    console.log(err);
+                    throw err;
+                });
+                ziparchiver.finalize();
+                outputPath.on('close', () => {
+                    resolve(ziparchiver.pointer());
+                });
+            });
+        }
+
+        /**
+         * 返回所查询的变更令的名称
+         * @param {string} cid - 变更令id
+         */
+        async getChangeName(cid) {
+            const sql = 'SELECT name FROM ?? WHERE cid = ?';
+            const sqlParam = [this.ctx.service.change.tableName, cid];
+            return await this.db.queryOne(sql, sqlParam);
+        }
+    }
+
+    return ChangeAtt;
+};

+ 136 - 137
app/service/login_logging.js

@@ -1,137 +1,136 @@
-'use strict';
-
-/**
- * 登录日志-数据模型
- *
- * @author lanjianrong
- * @date 2020/8/31
- * @version
- */
-const UAParser = require('ua-parser-js');
-
-module.exports = app => {
-    class LoginLogging extends app.BaseService {
-        constructor(ctx) {
-            super(ctx);
-            this.tableName = 'login_logging';
-        }
-
-        /**
-         * 创建记录
-         * @param {Object} payload - 载荷
-         */
-        async createLog(payload) {
-            const transaction = await this.db.beginTransaction();
-            try {
-                transaction.insert(this.tableName, payload);
-                await transaction.commit();
-            } catch (error) {
-                await transaction.rollback();
-                throw error;
-            }
-        }
-
-        /**
-         * 创建登录日志
-         * @return {Boolean} 日志是否创建成功
-         * @param {Number} type - 登录类型
-         * @param {Number} status - 是否显示记录
-         */
-        async addLoginLog(type, status) {
-            const { ctx } = this;
-            const ip = ctx.request.ip ? ctx.request.ip : '';
-            const ipInfo = await this.getIpInfoFromApi(ip);
-            const parser = new UAParser(ctx.header['user-agent']);
-            const osInfo = parser.getOS();
-            const cpuInfo = parser.getCPU();
-            const browserInfo = parser.getBrowser();
-            const payload = {
-                os: `${osInfo.name} ${osInfo.version} ${cpuInfo.architecture}`,
-                browser: `${browserInfo.name} ${browserInfo.version}`,
-                ip,
-                address: ipInfo,
-                uid: ctx.session.sessionUser.accountId,
-                pid: ctx.session.sessionProject.id,
-                type,
-                show: status,
-            };
-            return await this.createLog(payload);
-        }
-
-        /**
-         * 根据ip请求获取详细地址
-         * @param {String} a_ip - ip地址
-         * @return {String} 详细地址
-         */
-        async getIpInfoFromApi(a_ip = '') {
-            console.log('a_ip', a_ip);
-            if (!a_ip) return '';
-            if (a_ip === '127.0.0.1' || a_ip === '::1' || a_ip.indexOf('192.168') !== -1) return '服务器本机访问';
-            const { ip = '', region = '', city = '', isp = '' } = await this.sendRequest(a_ip);
-            let address = '';
-            region && (address += region + '省');
-            city && (address += city + '市 ');
-            isp && (address += isp + ' ');
-            ip && (address += `(${ip})`);
-            return address;
-        }
-
-        /**
-         * 发送请求获取详细地址
-         * @param {String} ip - ip地址
-         * @param {Number} max - 最大重试次数
-         * @return {Object} the result of request
-         * @private
-         */
-        async sendRequest(ip, max = 3) {
-            return new Promise(resolve => {
-                const start = () => {
-                    if (max <= 0) {
-                        resolve(); // 已达到最大重试次数,返回空的执行承若
-                    }
-                    max--;
-                    this.ctx.curl(`https://api01.aliyun.venuscn.com/ip?ip=${ip}`, {
-                        dateType: 'json',
-                        encoding: 'utf8',
-                        timeout: 2000,
-                        headers: {
-                            Authorization: 'APPCODE 85c64bffe70445c4af9df7ae31c7bfcc',
-                        },
-                    }).then(({ status, data }) => {
-                        if (status === 200) {
-                            const result = JSON.parse(data.toString()).data;
-                            if (!result.ip) {
-                                start();
-                            } else {
-                                max++;
-                                resolve(result);
-                            }
-                        } else {
-                            max--;
-                            start();
-                        }
-                    }).catch(() => {
-                        start();
-                    });
-                };
-                start();
-            });
-        }
-
-        /**
-         * 获取登录日志
-         * @param {Number} pid - 项目id
-         * @param {Number} uid - 用户id
-         * @return {Promise<Array>} 日志数组
-         */
-        async getLoginLogs(pid, uid) {
-            return this.db.select(this.tableName, {
-                where: { pid, uid, show: 0 },
-                orders: [['create_time', 'desc']],
-                columns: ['browser', 'create_time', 'ip', 'os', 'address'],
-                limit: 10, offset: 0,
-            });
-        }
-    }
-    return LoginLogging;
-};
+'use strict';
+
+/**
+ * 登录日志-数据模型
+ *
+ * @author lanjianrong
+ * @date 2020/8/31
+ * @version
+ */
+const UAParser = require('ua-parser-js');
+
+module.exports = app => {
+    class LoginLogging extends app.BaseService {
+        constructor(ctx) {
+            super(ctx);
+            this.tableName = 'login_logging';
+        }
+
+        /**
+         * 创建记录
+         * @param {Object} payload - 载荷
+         */
+        async createLog(payload) {
+            const transaction = await this.db.beginTransaction();
+            try {
+                transaction.insert(this.tableName, payload);
+                await transaction.commit();
+            } catch (error) {
+                await transaction.rollback();
+                throw error;
+            }
+        }
+
+        /**
+         * 创建登录日志
+         * @return {Boolean} 日志是否创建成功
+         * @param {Number} type - 登录类型
+         * @param {Number} status - 是否显示记录
+         */
+        async addLoginLog(type, status) {
+            const { ctx } = this;
+            const ip = ctx.request.ip ? ctx.request.ip : '';
+            const ipInfo = await this.getIpInfoFromApi(ip);
+            const parser = new UAParser(ctx.header['user-agent']);
+            const osInfo = parser.getOS();
+            const cpuInfo = parser.getCPU();
+            const browserInfo = parser.getBrowser();
+            const payload = {
+                os: `${osInfo.name} ${osInfo.version} ${cpuInfo.architecture}`,
+                browser: `${browserInfo.name} ${browserInfo.version}`,
+                ip,
+                address: ipInfo,
+                uid: ctx.session.sessionUser.accountId,
+                pid: ctx.session.sessionProject.id,
+                type,
+                show: status,
+            };
+            return await this.createLog(payload);
+        }
+
+        /**
+         * 根据ip请求获取详细地址
+         * @param {String} a_ip - ip地址
+         * @return {String} 详细地址
+         */
+        async getIpInfoFromApi(a_ip = '') {
+            if (!a_ip) return '';
+            if (a_ip === '127.0.0.1' || a_ip === '::1' || a_ip.indexOf('192.168') !== -1) return '服务器本机访问';
+            const { ip = '', region = '', city = '', isp = '' } = await this.sendRequest(a_ip);
+            let address = '';
+            region && (address += region + '省');
+            city && (address += city + '市 ');
+            isp && (address += isp + ' ');
+            ip && (address += `(${ip})`);
+            return address;
+        }
+
+        /**
+         * 发送请求获取详细地址
+         * @param {String} ip - ip地址
+         * @param {Number} max - 最大重试次数
+         * @return {Object} the result of request
+         * @private
+         */
+        async sendRequest(ip, max = 3) {
+            return new Promise(resolve => {
+                const start = () => {
+                    if (max <= 0) {
+                        resolve(); // 已达到最大重试次数,返回空的执行承若
+                    }
+                    max--;
+                    this.ctx.curl(`https://api01.aliyun.venuscn.com/ip?ip=${ip}`, {
+                        dateType: 'json',
+                        encoding: 'utf8',
+                        timeout: 2000,
+                        headers: {
+                            Authorization: 'APPCODE 85c64bffe70445c4af9df7ae31c7bfcc',
+                        },
+                    }).then(({ status, data }) => {
+                        if (status === 200) {
+                            const result = JSON.parse(data.toString()).data;
+                            if (!result.ip) {
+                                start();
+                            } else {
+                                max++;
+                                resolve(result);
+                            }
+                        } else {
+                            max--;
+                            start();
+                        }
+                    }).catch(() => {
+                        start();
+                    });
+                };
+                start();
+            });
+        }
+
+        /**
+         * 获取登录日志
+         * @param {Number} pid - 项目id
+         * @param {Number} uid - 用户id
+         * @return {Promise<Array>} 日志数组
+         */
+        async getLoginLogs(pid, uid) {
+            return this.db.select(this.tableName, {
+                where: { pid, uid, show: 0 },
+                orders: [['create_time', 'desc']],
+                columns: ['browser', 'create_time', 'ip', 'os', 'address'],
+                limit: 10, offset: 0,
+            });
+        }
+    }
+    return LoginLogging;
+};

+ 10 - 0
app/service/revise_pos.js

@@ -52,6 +52,16 @@ module.exports = app => {
             });
         }
 
+        async getPosDataByUnits(tenderId, 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.reviseBills.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 insertLedgerPosData(transaction, tid, rid, bills, data) {
             const precision = this.ctx.helper.findPrecision(this.ctx.tender.info.precision, bills.unit);
             const insertDatas = [];

+ 63 - 0
app/service/shenpi_audit.js

@@ -0,0 +1,63 @@
+'use strict';
+
+/**
+ * 版本数据模型
+ *
+ * @author CaiAoLin
+ * @date 2017/10/25
+ * @version
+ */
+
+module.exports = app => {
+
+    class ShenpiAudit extends app.BaseService {
+
+        /**
+         * 构造函数
+         *
+         * @param {Object} ctx - egg全局变量
+         * @return {void}
+         */
+        constructor(ctx) {
+            super(ctx);
+            this.tableName = 'shenpi_audit';
+        }
+
+        async getAudit(tid, type, status) {
+            const sql = 'SELECT sp.audit_id, pa.name FROM ?? AS sp LEFT JOIN ?? AS pa ON sp.audit_id = pa.id' +
+                ' WHERE sp.tid = ? AND sp.sp_type = ? AND sp.sp_status = ?';
+            const sqlParam = [this.tableName, this.ctx.service.projectAccount.tableName, tid, type, status];
+            return await this.db.queryOne(sql, sqlParam);
+        }
+
+        async getAuditList(tid, type, status) {
+            const sql = 'SELECT sp.audit_id, pa.name FROM ?? AS sp LEFT JOIN ?? AS pa ON sp.audit_id = pa.id' +
+                ' WHERE sp.tid = ? AND sp.sp_type = ? AND sp.sp_status = ? ORDER BY sp.id ASC';
+            const sqlParam = [this.tableName, this.ctx.service.projectAccount.tableName, tid, type, status];
+            return await this.db.query(sql, sqlParam);
+        }
+
+        async addAudit(data) {
+            const insertData = {
+                tid: this.ctx.tender.id,
+                sp_type: data.code,
+                sp_status: data.status,
+                audit_id: data.audit_id,
+            };
+            const result = await this.db.insert(this.tableName, insertData);
+            return result.effectRows === 1;
+        }
+
+        async removeAudit(data) {
+            const delData = {
+                tid: this.ctx.tender.id,
+                sp_type: data.code,
+                sp_status: data.status,
+                audit_id: data.audit_id,
+            };
+            return await this.db.delete(this.tableName, delData);
+        }
+    }
+
+    return ShenpiAudit;
+};

+ 20 - 1
app/service/stage_change.js

@@ -276,7 +276,7 @@ module.exports = app => {
         async getUsedData(tid, cid) {
             const lastStage = await this.ctx.service.stage.getLastestStage(tid, true);
             let filter;
-            if (lastStage.id === this.ctx.stage.id) {
+            if (this.ctx.stage && lastStage.id === this.ctx.stage.id) {
                 filter = this.db.format(' And (s.`order` < ? || (s.`order` = ? And sChange.`stimes` <= ? And sChange.`sorder` <= ?))',
                     [lastStage.order, lastStage.order, this.ctx.stage.curTimes, this.ctx.stage.curOrder]);
             } else {
@@ -312,6 +312,25 @@ module.exports = app => {
             return await this.db.query(sql, sqlParam);
         }
 
+        async getFinalUsedData(tid, cid) {
+            const sql = 'SELECT c.lid, c.pid, SUM(c.qty) as used_qty,' +
+                '    cb.tid, cb.cid, cb.id, cb.code, cb.name, cb.unit, cb.unit_price, cb.detail, cb.samount' +
+                '  FROM ' + this.ctx.service.changeAuditList.tableName + ' As cb' +
+                '  LEFT JOIN ' + this.tableName + ' As c ON cb.id = c.cbid ' +
+                '  INNER JOIN (' +
+                '    SELECT MAX(`stimes` * ' + timesLen + ' + `sorder`) As `flow`, `lid`, `pid`, `cbid`, sChange.`sid`, `cid` ' +
+                '      FROM ' + this.tableName + ' As sChange' +
+                '      LEFT JOIN ' + this.ctx.service.stage.tableName + ' As s ON sChange.sid = s.id' +
+                '      WHERE sChange.tid = ? AND cid = ?' +
+                '      GROUP By `lid`, `pid`, `cbid`, sChange.`sid`' +
+                '  ) As m' +
+                '  ON (c.stimes * ' + timesLen + ' + c.sorder) = m.flow And c.`cbid` = m.`cbid` AND c.`sid` = m.`sid` And c.`cid` = m.`cid` And c.`lid` = m.`lid` And c.`pid` = m.`pid`' +
+                '  WHERE cb.cid = ?' +
+                '  GROUP By c.`cbid`';
+            const sqlParam = [tid, cid, cid];
+            return await this.db.query(sql, sqlParam);
+        }
+
         /**
          * 获取 变更令 - 变更清单 当期使用情况
          * @param {Number} sid - 查询期id

+ 4 - 1
app/service/stage_pay.js

@@ -75,6 +75,7 @@ module.exports = app => {
          * @returns {Promise<void>}
          */
         async addInitialStageData(stage, transaction) {
+            const basesReg = new RegExp(payConst.calcBase.map(x => {return '(' + x.code + ')'}).join('|'));
             if (!stage) {
                 throw '初始化期合同支付数据失败';
             }
@@ -101,7 +102,9 @@ module.exports = app => {
                         tid: p.tid, sid: stage.id, pid: p.id,
                         stimes: stage.times, sorder: 0,
                         name: pp.name,
-                        expr: (p.ptype === payConst.payType.normal || p.ptype === payConst.payType.wc) ? pp.expr : null,
+                        expr: (p.ptype === payConst.payType.normal || p.ptype === payConst.payType.sf)
+                            ? (basesReg.test(pp.expr) ? pp.expr : null)
+                            : (p.type === payConst.wc ? pp.expr : null),
                         pause: pp.pause,
                         pre_tp: pp.end_tp,
                         pre_used: pp.pre_used || !this.ctx.helper.checkZero(pp.tp),

+ 109 - 1
app/service/stage_pos.js

@@ -38,7 +38,7 @@ module.exports = app => {
             if (where.lid) {
                 if (where.lid instanceof Array) {
                     whereSql += ' And ' + asTable + 'lid in ('  + this.ctx.helper.getInArrStrSqlFilter(where.lid) + ')';
-                } else if (typeof where.pid === "string") {
+                } else if (typeof where.lid === "string") {
                     whereSql += ' And ' + asTable + 'lid = ' + this.db.escape(where.lid);
                 }
             }
@@ -379,6 +379,112 @@ module.exports = app => {
         }
 
         /**
+         * 更新部位明细数据(仅供updateStageData调用)
+         *
+         * @param transaction - 事务
+         * @param data - 更新数据(允许一次性提交多条)
+         * @returns {Promise<{ledger: Array, pos: Array}>}
+         * @private
+         */
+        async _batchUpdateStagePosData(data) {
+            const datas = data instanceof Array ? data : [data];
+            const result = {
+                ledger: this._.uniq(this._.map(datas, 'lid')),
+                pos: this._.map(datas, 'pid'),
+                stageUpdate: true
+            };
+
+            const orgStagePos = await this.getLastestStageData2(this.ctx.tender.id, this.ctx.stage.id,
+                {pid: this._.map(datas, 'pid')});
+
+            const bills = await this.ctx.service.ledger.getAllDataByCondition({where: {id: result.ledger}});
+            for (const b of bills) {
+                b.precision = this.ctx.helper.findPrecision(this.ctx.tender.info.precision, b.unit);
+            }
+
+            const updatePosStage = [], insertPosStage = [];
+            for (const d of datas) {
+                const b = this._.find(bills, {id: d.lid});
+
+                const osp = this._.find(orgStagePos, function (p) { return p.pid === d.pid; });
+
+                if (osp && osp.times === this.ctx.stage.curTimes && osp.order === this.ctx.stage.curOrder) {
+                    const sp = {id: osp.id};
+                    if (d.contract_qty !== undefined) {
+                        sp.contract_qty = this.ctx.helper.round(d.contract_qty, b.precision.value);
+                    }
+                    if (osp.contract_expr) sp.contract_expr = null;
+                    updatePosStage.push(sp);
+                } else {
+                    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
+                    };
+                    if (d.contract_qty !== undefined || osp) {
+                        sp.contract_qty = d.contract_qty === undefined && osp
+                            ? osp.contract_qty
+                            : this.ctx.helper.round(d.contract_qty, b.precision.value);
+                    }
+                    insertPosStage.push(sp);
+                }
+                result.pos.push(d.pid);
+            }
+            const updateBillsStage = [], insertBillsStage = [], info = this.ctx.tender.info;
+            for (const b of bills) {
+                const stageBills = await this.getLastestStageData(b.tender_id, this.ctx.stage.id, b.id);
+
+                const posStage = await this.getLastestStageData2(b.tender_id, this.ctx.stage.id, {lid: b.id});
+                let contract_qty = 0;
+                for (const ps of posStage) {
+                    const nps = this._.find(datas, {pid: ps.pid});
+                    contract_qty = this.ctx.helper.add(contract_qty, nps ? nps.contract_qty : ps.contract_qty);
+                }
+                if (stageBills && stageBills.times === this.ctx.stage.curTimes && stageBills.order === this.ctx.stage.curOrder) {
+                    if (contract_qty === stageBills.contract_qty) continue;
+                    updateBillsStage.push({
+                        id: stageBills.id,
+                        contract_qty: contract_qty,
+                        contract_tp: this.ctx.helper.mul(contract_qty, b.unit_price, info.decimal.tp),
+                    });
+                } else {
+                    insertBillsStage.push({
+                        tid: this.ctx.tender.id,
+                        lid: b.id,
+                        sid: this.ctx.stage.id,
+                        times: this.ctx.stage.curTimes,
+                        order: this.ctx.stage.curOrder,
+                        said: this.ctx.session.sessionUser.accountId,
+                        contract_qty: contract_qty,
+                        contract_tp: this.ctx.helper.mul(contract_qty, b.unit_price, info.decimal.tp),
+                    })
+                }
+            }
+
+            const transaction = await this.db.beginTransaction();
+            try {
+                if (updatePosStage.length > 0) {
+                    await transaction.updateRows(this.tableName, updatePosStage);
+                }
+                if (insertPosStage.length > 0) {
+                    await transaction.insert(this.tableName, insertPosStage);
+                }
+                if (updateBillsStage.length > 0) {
+                    await transaction.updateRows(this.ctx.service.stageBills.tableName, updateBillsStage);
+                }
+                if (insertBillsStage.length > 0) {
+                    await transaction.insert(this.ctx.service.stageBills.tableName, insertBillsStage);
+                }
+                await transaction.commit();
+                return result;
+            } catch (err) {
+                await transaction.rollback();
+                throw err;
+            }
+        }
+
+        /**
          * 删除部位明细数据(仅供updateStageData调用)
          *
          * @param transaction - 事务
@@ -451,6 +557,8 @@ module.exports = app => {
                 refreshData = await this._addStagePosData(data.updateData);
             } else if (data.updateType === 'update') {
                 refreshData = await this._updateStagePosData(data.updateData);
+            } else if (data.updateType === 'batchUpdate') {
+                refreshData = await this._batchUpdateStagePosData(data.updateData);
             } else if (data.updateType === 'delete') {
                 if (!data.updateData || data.updateData.length === 0) {
                     throw '提交数据错误';

+ 24 - 9
app/service/tender_info.js

@@ -13,6 +13,9 @@ const parseInfo = infoConst.parseInfo;
 const arrayInfo = infoConst.arrayInfo;
 const defaultInfo = infoConst.defaultInfo;
 const advanceConst = require('../const/audit').advance;
+const auditConst = require('../const/audit');
+
+
 module.exports = app => {
 
     class TenderInfo extends app.BaseService {
@@ -143,12 +146,21 @@ module.exports = app => {
             }
             changeUnits = this._.flatten(changeUnits);
 
+            let billsService, posService;
+            if (this.ctx.tender.data.status === auditConst.ledger.status.checked) {
+                billsService = this.ctx.service.ledger;
+                posService = this.ctx.service.pos;
+            } else {
+                billsService = this.ctx.service.reviseBills;
+                posService = this.ctx.service.revisePos;
+            }
+
             if (changeUnits.length > 0) {
-                const bills = await this.ctx.service.ledger.getAllDataByCondition({
+                const bills = await billsService.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) : [];
+                const pos = changeUnits.length > 0 ? await posService.getPosDataByUnits(tenderId, changeUnits) : [];
 
                 for (const b of bills) {
                     const precision = this.ctx.helper.findPrecision(newPrecision, b.unit);
@@ -185,8 +197,8 @@ module.exports = app => {
                 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);
+                    if (bills.length > 0) await transaction.updateRows(billsService.tableName, bills);
+                    if (pos.length > 0) await transaction.updateRows(posService.tableName, pos);
                     await transaction.commit();
                 } catch (err) {
                     await transaction.rollback();
@@ -198,11 +210,11 @@ module.exports = app => {
             }
         }
 
-        async _reCalcLedger(tenderId, newDecimal, oldDecimal) {
+        async _reCalcLedger(tenderId, billsService, newDecimal, oldDecimal) {
             const changeBills = [];
             const calcUp = newDecimal.up < oldDecimal.up, calcTp = newDecimal.tp !== oldDecimal.tp;
             if (calcUp || calcTp) {
-                const bills = await this.ctx.service.ledger.getAllDataByCondition({
+                const bills = await billsService.getAllDataByCondition({
                     columns: ['id', 'unit_price', 'sgfh_qty', 'sjcl_qty', 'qtcl_qty', 'deal_qty', 'quantity'],
                     where: { tender_id: tenderId, is_leaf: true },
                 });
@@ -227,7 +239,6 @@ module.exports = app => {
             const upDecimal = newDecimal.up, tpDecimal = newDecimal.extra ? newDecimal.extraTp : newDecimal.tp;
             const calcUp = upDecimal < oldDecimal.up,
                 calcTp = tpDecimal < (oldDecimal.extra ? oldDecimal.extraTp : oldDecimal.tp);
-            console.log(calcUp, calcTp);
 
             if (calcUp || calcTp) {
                 const stageJgcl = await this.ctx.service.stageJgcl.getAllDataByCondition({
@@ -281,7 +292,11 @@ module.exports = app => {
                     changeAdvanceBills.push(cb);
                 }
             }
-            const changeBills = await this._reCalcLedger(tenderId, newDecimal, oldDecimal);
+
+            const billsService = this.ctx.tender.data.status === auditConst.ledger.status.checked
+                ? this.ctx.service.ledger
+                : this.ctx.service.reviseBills;
+            const changeBills = await this._reCalcLedger(tenderId, billsService, newDecimal, oldDecimal);
             const [changeSj, changeSb, changeSo] = await this._reCalcStageExtra(tenderId, newDecimal, oldDecimal);
             if (changeBills.length > 0 ||
                 changeAdvanceBills.length > 0 ||
@@ -290,7 +305,7 @@ module.exports = app => {
                 try {
                     await transaction.update(this.tableName,
                         { decimal: JSON.stringify(newDecimal) }, { where: { tid: tenderId } });
-                    if (changeBills.length > 0) await transaction.updateRows(this.ctx.service.ledger.tableName, changeBills);
+                    if (changeBills.length > 0) await transaction.updateRows(billsService.tableName, changeBills);
                     if (changeSj.length > 0) await transaction.updateRows(this.ctx.service.stageJgcl.tableName, changeSj);
                     if (changeSb.length > 0) await transaction.updateRows(this.ctx.service.stageBonus.tableName, changeSb);
                     if (changeSo.length > 0) await transaction.updateRows(this.ctx.service.stageOther.tableName, changeSo);

+ 9 - 5
app/view/change/info.ejs

@@ -152,13 +152,14 @@
                 </div>
             </div>
             <div id="files_title" class="show_title" style="line-height:34px;display: none">
-                <% if (auditStatus === 1 || auditStatus === 2 || auditStatus === 6) { %>
-                <a href="#addfujian" data-toggle="modal" class="btn btn-sm" data-toggle="tooltip" data-placement="bottom" title="" data-original-title="添加清单">
-                    <i class="fa fa-cloud-upload" aria-hidden="true"></i> 上传附件
-                </a>
+                <!-- <% if (auditStatus === 1 || auditStatus === 2 || auditStatus === 6) { %>
+                <a href="#addfujian" data-toggle="modal" class="btn btn-sm btn-light text-primary" data-placement="bottom" title="" data-original-title="上传附件"><i class="fa fa-cloud-upload" aria-hidden="true"></i> 上传附件</a>
                 <% } else { %>
                 <h2 class="change-title">附件</h2>
-                <% } %>
+                <% } %> -->
+                <a href="#addfujian" data-toggle="modal" class="btn btn-sm btn-light text-primary" data-placement="bottom" title="" data-original-title="上传附件"><i class="fa fa-cloud-upload" aria-hidden="true"></i> 上传附件</a>
+                <a href="javascript: void(0);" data-toggle="modal" class="btn btn-sm btn-light text-primary" id="bach-download"><i class="fa fa-download "></i> 批量下载</a>
+                <a href="" id="downloadZip" style="display: none;" download></a>
             </div>
             <input id="tenderId" value="<%= tender.id %>" type="hidden">
             <input id="changeId" value="<%= change.cid %>" type="hidden">
@@ -705,6 +706,7 @@
             <table class="table table-bordered">
                 <thead>
                 <tr>
+                    <td width="25" style="background-color: #e9ecef;"><input type="checkbox" id="check-all-file" ></td>
                     <th width="50">序号</th>
                     <th>名称</th>
                     <th width="90">大小</th>
@@ -716,11 +718,13 @@
                 <% if (attList !== undefined && attList !== '') { %>
                 <% for (const [index,att] of attList.entries()) { %>
                 <tr>
+                    <td width="25"><input type="checkbox" class="check-file" file-id=<%- att.id %>></td>
                     <td><%- index+1 %></td>
                     <td><a href="javascript: void(0);" class="file-atn" f-id="<%- att.id %>"><%- att.filename %><%- att.fileext %></a></td>
                     <td><%- ctx.helper.bytesToSize(att.filesize) %></td>
                     <td><%- moment(att.in_time * 1000).format('YYYY-MM-DD') %></td>
                     <td>
+                        <a href="/change/download/file/<%- att.id %>" class="btn btn-light btn-sm" title="下载"><span class="fa fa-download text-primary"></span></a>
                         <% if (att.uid === uid && (auditStatus === 1 || auditStatus === 2 || auditStatus === 6)) { %>
                         <a class="btn btn-light btn-sm delete-file" data-attid="<%- att.id %>" title="删除附件"><span class="fa fa-trash text-danger"></span></a>
                         <% } %>

+ 10 - 10
app/view/dashboard/index.ejs

@@ -40,7 +40,7 @@
                                                         <div class="col-3 ml-auto text-right pl-0"><a href="/tender/<%- t.id %>/ledger/audit" class="btn btn-sm btn-outline-primary">审批</a></div>
                                                     </div>
                                                     <p class="mt-1 mb-0"><%- ctx.session.sessionUser.name %><small class="ml-1 text-muted"><%- (role ? '- ' + role : '') %></small>
-                                                        <span class="pull-right text-muted"><%- (t.begin_time ? t.begin_time.toLocaleString() : '') %></span>
+                                                        <span class="pull-right text-muted"><%- (t.begin_time ? ctx.moment(t.begin_time).format('YYYY-MM-DD hh:mm:ss') : '') %></span>
                                                     </p>
                                                 </div>
                                             </li>
@@ -53,7 +53,7 @@
                                                         <div class="col-3 ml-auto text-right pl-0"><a href="/tender/<%- t.id %>/ledger" class="btn btn-sm btn-outline-primary">重新上报</a></div>
                                                     </div>
                                                     <p class="mt-1 mb-0"><%- ctx.session.sessionUser.name %><small class="ml-1 text-muted"><%- (role ? '- ' + role : '') %></small>
-                                                        <span class="pull-right text-muted"><%- (t.end_time ? t.end_time.toLocaleString() : '') %></span>
+                                                        <span class="pull-right text-muted"><%- (t.end_time ? ctx.moment(t.end_time).format('YYYY-MM-DD hh:mm:ss') : '') %></span>
                                                     </p>
                                                 </div>
                                             </li>
@@ -68,7 +68,7 @@
                                                     <div class="col-3 ml-auto text-right pl-0"><a href="/tender/<%- revise.t_id %>/revise/info" class="btn btn-sm btn-outline-primary"><% if (revise.status === acRevise.status.checking) { %>审批<% } else if (revise.status === acRevise.status.checkNo) { %>重新上报<% } %></a></div>
                                                 </div>
                                                 <p class="mt-1 mb-0"><%- revise.audit_name %><small class="ml-1 text-muted"><%- (revise.audit_role ? '- ' + revise.audit_role: '') %></small>
-                                                    <span class="pull-right text-muted"><%- (revise.end_time ? revise.end_time.toLocaleString() : '') %></span>
+                                                    <span class="pull-right text-muted"><%- (revise.end_time ? ctx.moment(revise.end_time).format('YYYY-MM-DD hh:mm:ss') : '') %></span>
                                                 </p>
                                             </div>
                                         </li>
@@ -83,7 +83,7 @@
                                                         <div class="col-3 ml-auto text-right pl-0"><a href="/tender/<%- audit.tid %>/measure/stage/<%- audit.sorder %>" class="btn btn-sm btn-outline-primary"><% if (audit.sstatus === acStage.status.checkNoPre) { %>重新<% } %>审批</a></div>
                                                     </div>
                                                     <p class="mt-1 mb-0"><%- ctx.session.sessionUser.name %><small class="ml-1 text-muted"><%- (role ? '- ' + role : '') %></small>
-                                                        <span class="pull-right text-muted"><%- audit.begin_time.toLocaleString() %></span>
+                                                        <span class="pull-right text-muted"><%- ctx.moment(audit.begin_time).format('YYYY-MM-DD hh:mm:ss') %></span>
                                                     </p>
                                                 </div>
                                             </li>
@@ -96,7 +96,7 @@
                                                         <div class="col-3 ml-auto text-right pl-0"><a href="/tender/<%- audit.tid %>/measure/stage/<%- audit.sorder %>" class="btn btn-sm btn-outline-primary">重新上报</a></div>
                                                     </div>
                                                     <p class="mt-1 mb-0"><%- ctx.session.sessionUser.name %><small class="ml-1 text-muted"><%- (role ? '- ' + role : '') %></small>
-                                                        <span class="pull-right text-muted"><%- audit.end_time.toLocaleString() %></span>
+                                                        <span class="pull-right text-muted"><%- ctx.moment(audit.end_time).format('YYYY-MM-DD hh:mm:ss') %></span>
                                                     </p>
                                                 </div>
                                             </li>
@@ -111,7 +111,7 @@
                                                     <div class="col-3 ml-auto text-right pl-0"><a href="/tender/<%- change.tid %>/change/<%- change.cid %>/info" class="btn btn-sm btn-outline-primary"><% if (change.cstatus === acChange.status.checking) { %>审批<% } else if (change.cstatus === acChange.status.backnew) { %>重新审批<% } else { %>重新上报<% } %></a></div>
                                                 </div>
                                                 <p class="mt-1 mb-0"><%- change.caname %><small class="ml-1 text-muted"><%- (role ? '- ' + role : '') %></small>
-                                                    <span class="pull-right text-muted"><%- (change.sin_time ? change.sin_time.toLocaleString() : '') %></span>
+                                                    <span class="pull-right text-muted"><%- (change.sin_time ? ctx.moment(change.sin_time).format('YYYY-MM-DD hh:mm:ss') : '') %></span>
                                                 </p>
                                             </div>
                                         </li>
@@ -126,7 +126,7 @@
                                                         <div class="col-3 ml-auto text-right pl-0"><a href="/tender/<%- am.tid %>/measure/material/<%- am.morder %>" class="btn btn-sm btn-outline-primary">审批</a></div>
                                                     </div>
                                                     <p class="mt-1 mb-0"><%- ctx.session.sessionUser.name %><small class="ml-1 text-muted"><%- (role ? '- ' + role : '') %></small>
-                                                        <span class="pull-right text-muted"><%- am.begin_time.toLocaleString() %></span>
+                                                        <span class="pull-right text-muted"><%- ctx.moment(am.begin_time).format('YYYY-MM-DD hh:mm:ss') %></span>
                                                     </p>
                                                 </div>
                                             </li>
@@ -139,7 +139,7 @@
                                                         <div class="col-3 ml-auto text-right pl-0"><a href="/tender/<%- am.tid %>/measure/material/<%- am.morder %>" class="btn btn-sm btn-outline-primary">重新上报</a></div>
                                                     </div>
                                                     <p class="mt-1 mb-0"><%- ctx.session.sessionUser.name %><small class="ml-1 text-muted"><%- (role ? '- ' + role : '') %></small>
-                                                        <span class="pull-right text-muted"><%- am.end_time.toLocaleString() %></span>
+                                                        <span class="pull-right text-muted"><%- ctx.moment(am.end_time).format('YYYY-MM-DD hh:mm:ss') %></span>
                                                     </p>
                                                 </div>
                                             </li>
@@ -155,7 +155,7 @@
                                                         <div class="col-3 ml-auto text-right pl-0"><a href="/tender/<%- am.tid %>/advance/<%- am.vid %>/detail" class="btn btn-sm btn-outline-primary">审批</a></div>
                                                     </div>
                                                     <p class="mt-1 mb-0"><%- ctx.session.sessionUser.name %><small class="ml-1 text-muted"><%- (role ? '- ' + role : '') %></small>
-                                                        <span class="pull-right text-muted"><%- am.create_time.toLocaleString() %></span>
+                                                        <span class="pull-right text-muted"><%- ctx.moment(am.create_time).format('YYYY-MM-DD hh:mm:ss') %></span>
                                                     </p>
                                                 </div>
                                             </li>
@@ -168,7 +168,7 @@
                                                         <div class="col-3 ml-auto text-right pl-0"><a href="/tender/<%- am.tid %>/advance/<%- am.vid %>/detail" class="btn btn-sm btn-outline-primary">重新上报</a></div>
                                                     </div>
                                                     <p class="mt-1 mb-0"><%- ctx.session.sessionUser.name %><small class="ml-1 text-muted"><%- (role ? '- ' + role : '') %></small>
-                                                        <span class="pull-right text-muted"><%- am.end_time.toLocaleString() %></span>
+                                                        <span class="pull-right text-muted"><%- ctx.moment(am.end_time).format('YYYY-MM-DD hh:mm:ss') %></span>
                                                     </p>
                                                 </div>
                                             </li>

+ 7 - 1
app/view/ledger/explode.ejs

@@ -41,10 +41,12 @@
                 </div>
                 <div class="d-inline-block ml-3">
                     <a id="exportLedger" class="btn btn-primary btn-sm" href="javascript: void(0)">导出台账Excel</a>
-                    <a class="btn btn-sm btn-primary" href="#ledger-check-modal" data-toggle="modal" data-target="#ledger-check-modal">数据检查</a>
                 </div>
             </div>
             <div class="ml-auto">
+                <% if (tender.ledger_status !== auditConst.status.checked) { %>
+                    <a class="btn btn-sm btn-primary mr-1" href="#ledger-check-modal" data-toggle="modal" data-target="#ledger-check-modal">数据检查</a>
+                <% } %>
                 <% if (tender.ledger_status === auditConst.status.checkNo) { %>
                     <a href="#sp-list" data-type="hide" data-toggle="modal" data-target="#sp-list" class="btn btn-outline-warning btn-sm pull-right text-dark sp-list-btn">审批退回</a>
                 <% } else if (tender.ledger_status === auditConst.status.checking) { %>
@@ -137,7 +139,11 @@
                     <div id="deal-bills" class="tab-pane">
                         <div class="sjs-bar-4">
                             <div class="pb-1">
+                                <% if (dealBillsPermission) { %>
                                 <a href="#upload-deal" data-toggle="modal" data-target="#upload-deal" class="btn btn-sm btn-primary">上传签约清单</a>
+                                <% } else { %>
+                                <a href="javascript: void(0);" class="btn btn-sm btn-outline-dark">上传签约清单</a>
+                                <% } %>
                                 <a href="/tender/<%- ctx.tender.id %>/deal/download/签约清单.xlsx" class="btn btn-sm btn-primary" style="display: none">下载签约清单</a>
                             </div>
                         </div>

+ 3 - 3
app/view/revise/history.ejs

@@ -22,7 +22,7 @@
                     </div>
                 </div>
                 <div class="d-inline-flex">
-                    <%- (revise.in_time ? revise.in_time.toLocaleString() : '') %>
+                    <%- (revise.in_time ? ctx.moment(revise.in_time).format('YYYY-MM-DD hh:mm:ss') : '') %>
                 </div>
             </div>
             <div class="ml-auto">
@@ -66,7 +66,7 @@
                                 <label >创建时间</label>
                                 <select class="form-control form-control-sm" id="reviseHistory">
                                     <% for (const hr of historyRevise) { %>
-                                    <option value="<%- hr.id %>"><%- hr.corder + '#' + (hr.in_time ? hr.in_time.toLocaleDateString() : '') + (hr.valid ? '' : '(作废)')%></option>
+                                    <option value="<%- hr.id %>"><%- hr.corder + '#' + (hr.in_time ? ctx.moment(hr.in_time).format('YYYY-MM-DD') : '') + (hr.valid ? '' : '(作废)')%></option>
                                     <% } %>
                                 </select>
                             </div>
@@ -81,7 +81,7 @@
                             </div>
                             <div class="form-group">
                                 <label >审批完成</label>
-                                <input id="end-time" type="" class="form-control form-control-sm" value="<%- (revise.end_time ? revise.end_time.toLocaleString() : '') %>" disabled>
+                                <input id="end-time" type="" class="form-control form-control-sm" value="<%- (revise.end_time ? ctx.moment(revise.end_time).format('YYYY-MM-DD hh:mm:ss') : '') %>" disabled>
                             </div>
                         </div>
                     </div>

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

@@ -40,7 +40,7 @@
                     <% for (const lr of ledgerRevise) { %>
                     <tr>
                         <td><%- lr.corder %></td>
-                        <td><%- lr.in_time ? lr.in_time.toLocaleDateString() : '' %></td>
+                        <td><%- lr.in_time ? ctx.moment(lr.in_time).format('YYYY-MM-DD') : '' %></td>
                         <td><%- lr.user_name %></td>
                         <% if (!lr.valid) {%>
                         <td class="text-danger">
@@ -54,7 +54,7 @@
                             <%- auditConst.auditProgress[lr.status] %>
                         </td>
                         <% } %>
-                        <td><%- lr.end_time ? lr.end_time.toLocaleDateString() : '' %></td>
+                        <td><%- lr.end_time ? ctx.moment(lr.end_time).format('YYYY-MM-DD') : '' %></td>
                         <td>
                             <% if (lr.valid) { %>
                             <% if (lr.status === auditConst.status.uncheck && lr.uid === ctx.session.sessionUser.accountId) { %>

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

@@ -41,30 +41,30 @@
                         <input type="text" class="form-control form-control-sm m-0" id="bills-expr" readonly="">
                     </div>
                 </div>
-                <div class="d-inline-block ml-3">
-                    <a class="btn btn-sm btn-primary" href="#ledger-check-modal" data-toggle="modal" data-target="#ledger-check-modal">数据检查</a>
-                </div>
             </div>
             <% } %>
             <div class="ml-auto">
+                <% if (revise.status !== audit.status.checked) { %>
+                    <a class="btn btn-sm btn-primary mr-1" href="#ledger-check-modal" data-toggle="modal" data-target="#ledger-check-modal">数据检查</a>
+                <% } %>
                 <% if (revise.status === audit.status.uncheck && revise.uid === ctx.session.sessionUser.accountId) { %>
-                <a href="#sub-sp" data-toggle="modal" data-target="#sub-sp" class="btn btn-primary btn-sm pull-right mr-1">上报审批</a>
+                <a href="#sub-sp" data-toggle="modal" data-target="#sub-sp" class="btn btn-primary btn-sm mr-1">上报审批</a>
                 <% } %>
                 <% if (revise.status === audit.status.checkNo) { %>
-                <a href="#sp-list" data-type="hide" data-toggle="modal" data-target="#sp-list" class="btn btn-outline-secondary btn-sm pull-right mr-1 sp-list-btn">审批退回</a>
+                <a href="#sp-list" data-type="hide" data-toggle="modal" data-target="#sp-list" class="btn btn-outline-secondary btn-sm mr-1 sp-list-btn">审批退回</a>
                     <% if (revise.uid === ctx.session.sessionUser.accountId) { %>
-                    <a href="#sp-list" data-type="show" data-toggle="modal" data-target="#sp-list" class="btn btn-primary btn-sm pull-right mr-1 sp-list-btn">重新上报</a>
+                    <a href="#sp-list" data-type="show" data-toggle="modal" data-target="#sp-list" class="btn btn-primary btn-sm mr-1 sp-list-btn">重新上报</a>
                     <% } %>
                 <% } %>
                 <% if (revise.status === audit.status.checking) { %>
-                <a href="#sp-list" data-toggle="modal" data-target="#sp-list" class="btn btn-outline-warning btn-sm pull-right text-dark mr-1">审批中</a>
+                <a href="#sp-list" data-toggle="modal" data-target="#sp-list" class="btn btn-outline-warning btn-sm text-dark mr-1">审批中</a>
                     <% if (curAuditor.audit_id === ctx.session.sessionUser.accountId) { %>
-                    <a href="#sp-done" data-toggle="modal" data-target="#sp-done" class="btn btn-success btn-sm pull-right mr-1">审批通过</a>
-                    <a href="#sp-back" data-toggle="modal" data-target="#sp-back" class="btn btn-warning btn-sm pull-right mr-1">审批退回</a>
+                    <a href="#sp-done" data-toggle="modal" data-target="#sp-done" class="btn btn-success btn-sm mr-1">审批通过</a>
+                    <a href="#sp-back" data-toggle="modal" data-target="#sp-back" class="btn btn-warning btn-sm mr-1">审批退回</a>
                     <% } %>
                 <% } %>
                 <% if (revise.status === audit.status.checked) { %>
-                <a href="#sp-list" data-type="hide" data-toggle="modal" data-target="#sp-list" class="btn btn-outline-secondary btn-sm pull-right mr-1 sp-list-btn">审批完成</a>
+                <a href="#sp-list" data-type="hide" data-toggle="modal" data-target="#sp-list" class="btn btn-outline-secondary btn-sm mr-1 sp-list-btn">审批完成</a>
                 <% } %>
             </div>
         </div>
@@ -124,7 +124,7 @@
                         <div class="sjs-sh" style="overflow:auto">
                             <div class="form-group mt-2">
                                 <label >创建时间</label>
-                                <input type="" class="form-control form-control-sm" value="<%- revise.in_time.toLocaleString() %>" disabled>
+                                <input type="" class="form-control form-control-sm" value="<%- ctx.moment(revise.in_time).format('YYYY-MM-DD hh:mm:ss') %>" disabled>
                             </div>
                             <div class="form-group mt-2">
                                 <label >提交人</label>

+ 3 - 0
app/view/setting/user_permission_modal.ejs

@@ -81,6 +81,9 @@
                         <div class="form-check form-check-inline">
                             <input class="form-check-input" type="<%= permission[pm].type %>" id="<%= pm %>_<%= ip.value %>" name="<%= pm %><% if (permission[pm].type === 'checkbox') { %>[]<% } %>" value="<%= ip.value %>">
                             <label class="form-check-label" for="<%= pm %>_<%= ip.value %>"><%= ip.title %></label>
+                            <% if (ip.hint && ip.hintIcon) { %>
+                            <a href="" data-toggle="tooltip" data-placement="bottom" title="" data-original-title="<%- ip.hint%>"><i class="fa <%- ip.hintIcon%>"></i></a>
+                            <% } %>
                         </div>
                         <% } %>
                     </div>

+ 40 - 0
app/view/stage/modal.ejs

@@ -410,6 +410,46 @@
         </div>
     </div>
 </div>
+
+<!--按比例计量-->
+<div class="modal fade" id="calc-by-ratio" 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="input-group input-group-sm mb-1 col-5 p-0">
+                    <div class="input-group-prepend">
+                        <span class="input-group-text">计量比例</span>
+                    </div>
+                    <input class="form-control nospin" type="number" min="1" max="100" id="cbr-ratio">
+                    <div class="input-group-append">
+                        <span class="input-group-text">%</span>
+                    </div>
+                </div>
+                <div class="d-inline-block mb-1">
+                    <div class="form-check">
+                        <input class="form-check-input" type="checkbox" value="" id="apply2sibling">
+                        <label class="form-check-label" for="apply2sibling">
+                            当前细目下同名计量单元同步计量
+                        </label>
+                    </div>
+                </div>
+                <div class="modal-height-300">
+                    <table class="table table-bordered">
+                        <thead><tr><th>序号</th><th>计量单元</th><th>台帐数量</th><th><input id="cbr-check-all" type="checkbox"></th></tr></thead>
+                        <tbody id="cbr-pos-list"></tbody>
+                    </table>
+                </div>
+            </div>
+            <div class="modal-footer">
+                <button type="button" class="btn btn-sm btn-secondary" data-dismiss="modal">关闭</button>
+                <a href="javascript: void(0);" class="btn btn-sm btn-primary" id="cbr-ok">确定</a>
+            </div>
+        </div>
+    </div>
+</div>
 <% include ./audit_modal.ejs %>
 <% include ../shares/merge_peg_modal.ejs %>
 <% include ../shares/ledger_check_modal.ejs %>

+ 52 - 37
app/view/tender/detail_modal.ejs

@@ -27,7 +27,7 @@
                                             <div class="input-group-prepend">
                                                 <span class="input-group-text">建设项目名称</span>
                                             </div>
-                                            <input type="text" class="form-control" value="项目A" id="build-name" maxlength="100">
+                                            <input type="text" class="form-control" value="项目A" id="build-name" maxlength="100" oninput="limitReturn(this)">
                                         </div>
                                     </div>
                                     <div class="col-12 mb-2">
@@ -35,7 +35,7 @@
                                             <div class="input-group-prepend">
                                                 <span class="input-group-text" style="width:90px">合同编号</span>
                                             </div>
-                                            <input type="text" class="form-control" value="" id="deal-code" maxlength="100">
+                                            <input type="text" class="form-control" value="" id="deal-code" maxlength="100" oninput="limitReturn(this)">
                                         </div>
                                     </div>
                                     <div class="col-12">
@@ -43,7 +43,7 @@
                                             <div class="input-group-prepend">
                                                 <span class="input-group-text" style="width:90px">合同名称</span>
                                             </div>
-                                            <input type="text" class="form-control" value="" id="deal-name" maxlength="100">
+                                            <input type="text" class="form-control" value="" id="deal-name" maxlength="100" oninput="limitReturn(this)">
                                         </div>
                                     </div>
                                 </div>
@@ -58,7 +58,7 @@
                                             <div class="input-group-prepend">
                                                 <span class="input-group-text">单位名称</span>
                                             </div>
-                                            <input type="text" class="form-control" value="" id="build-company" maxlength="100">
+                                            <input type="text" class="form-control" value="" id="build-company" maxlength="100" oninput="limitReturn(this)">
                                         </div>
                                     </div>
                                     <div class="col-6 pr-0">
@@ -66,7 +66,7 @@
                                             <div class="input-group-prepend">
                                                 <span class="input-group-text">法人代表</span>
                                             </div>
-                                            <input type="text" class="form-control" value="" id="build-corporation" maxlength="100">
+                                            <input type="text" class="form-control" value="" id="build-corporation" maxlength="100" oninput="limitReturn(this)">
                                         </div>
                                     </div>
                                     <div class="col-6">
@@ -87,7 +87,7 @@
                                             <div class="input-group-prepend">
                                                 <span class="input-group-text">单位名称</span>
                                             </div>
-                                            <input type="text" class="form-control" value="" id="contract1-company" maxlength="100">
+                                            <input type="text" class="form-control" value="" id="contract1-company" maxlength="100" oninput="limitReturn(this)">
                                         </div>
                                     </div>
                                     <div class="col-6 pr-0">
@@ -95,7 +95,7 @@
                                             <div class="input-group-prepend">
                                                 <span class="input-group-text">法人代表</span>
                                             </div>
-                                            <input type="text" class="form-control" value="" id="contract1-corporation" maxlength="100">
+                                            <input type="text" class="form-control" value="" id="contract1-corporation" maxlength="100" oninput="limitReturn(this)">
                                         </div>
                                     </div>
                                     <div class="col-6">
@@ -116,7 +116,7 @@
                                             <div class="input-group-prepend">
                                                 <span class="input-group-text">单位名称</span>
                                             </div>
-                                            <input type="text" class="form-control" value="" id="contract2-company" maxlength="100">
+                                            <input type="text" class="form-control" value="" id="contract2-company" maxlength="100" oninput="limitReturn(this)">
                                         </div>
                                     </div>
                                     <div class="col-6 pr-0">
@@ -124,7 +124,7 @@
                                             <div class="input-group-prepend">
                                                 <span class="input-group-text">法人代表</span>
                                             </div>
-                                            <input type="text" class="form-control" value="" id="contract2-corporation" maxlength="100">
+                                            <input type="text" class="form-control" value="" id="contract2-corporation" maxlength="100" oninput="limitReturn(this)">
                                         </div>
                                     </div>
                                     <div class="col-6">
@@ -145,7 +145,7 @@
                                             <div class="input-group-prepend">
                                                 <span class="input-group-text">单位名称</span>
                                             </div>
-                                            <input type="text" class="form-control" value="" id="supervision1-company" maxlength="100">
+                                            <input type="text" class="form-control" value="" id="supervision1-company" maxlength="100" oninput="limitReturn(this)">
                                         </div>
                                     </div>
                                     <div class="col-6 pr-0">
@@ -153,7 +153,7 @@
                                             <div class="input-group-prepend">
                                                 <span class="input-group-text">法人代表</span>
                                             </div>
-                                            <input type="text" class="form-control" value="" id="supervision1-corporation" maxlength="100">
+                                            <input type="text" class="form-control" value="" id="supervision1-corporation" maxlength="100" oninput="limitReturn(this)">
                                         </div>
                                     </div>
                                     <div class="col-6">
@@ -174,7 +174,7 @@
                                             <div class="input-group-prepend">
                                                 <span class="input-group-text">单位名称</span>
                                             </div>
-                                            <input type="text" class="form-control" value="" id="supervision2-company" maxlength="100">
+                                            <input type="text" class="form-control" value="" id="supervision2-company" maxlength="100" oninput="limitReturn(this)">
                                         </div>
                                     </div>
                                     <div class="col-6 pr-0">
@@ -182,7 +182,7 @@
                                             <div class="input-group-prepend">
                                                 <span class="input-group-text">法人代表</span>
                                             </div>
-                                            <input type="text" class="form-control" value="" id="supervision2-corporation" maxlength="100">
+                                            <input type="text" class="form-control" value="" id="supervision2-corporation" maxlength="100" oninput="limitReturn(this)">
                                         </div>
                                     </div>
                                     <div class="col-6">
@@ -219,7 +219,7 @@
                                                 <span class="input-group-text" style="width:90px">长度(KM)</span>
                                             </div>
                                             <input type="text" class="form-control" value="" id="length" min="0" max="10000"
-                                                   oninput="limitDealParamLength(this)" onchange="checkNumberValid(this)">
+                                                   oninput="limitDealParamLength(this);limitReturn(this);" onchange="checkNumberValid(this)">
                                         </div>
                                     </div>
                                     <div class="col-6 pr-0 mb-2">
@@ -227,7 +227,7 @@
                                             <div class="input-group-prepend">
                                                 <span class="input-group-text">起始桩号</span>
                                             </div>
-                                            <input type="text" class="form-control" value="" id="start-peg" maxlength="100">
+                                            <input type="text" class="form-control" value="" id="start-peg" maxlength="100" oninput="limitReturn(this)">
                                         </div>
                                     </div>
                                     <div class="col-6">
@@ -235,7 +235,7 @@
                                             <div class="input-group-prepend">
                                                 <span class="input-group-text" style="width:90px">终止桩号</span>
                                             </div>
-                                            <input type="text" class="form-control" value="" id="end-peg" maxlength="100">
+                                            <input type="text" class="form-control" value="" id="end-peg" maxlength="100" oninput="limitReturn(this)">
                                         </div>
                                     </div>
                                     <div class="col-6 pr-0 mb-2">
@@ -243,7 +243,7 @@
                                             <div class="input-group-prepend">
                                                 <span class="input-group-text" style="width:66px">车道数</span>
                                             </div>
-                                            <input type="text" class="form-control" value="" id="lane-count" maxlength="100">
+                                            <input type="text" class="form-control" value="" id="lane-count" maxlength="100" oninput="limitReturn(this)">
                                         </div>
                                     </div>
                                     <div class="col-6">
@@ -251,7 +251,7 @@
                                             <div class="input-group-prepend">
                                                 <span class="input-group-text" style="width:90px">合同工期</span>
                                             </div>
-                                            <input type="text" class="form-control" value="" id="deal-period" maxlength="100">
+                                            <input type="text" class="form-control" value="" id="deal-period" maxlength="100" oninput="limitReturn(this)">
                                         </div>
                                     </div>
                                     <div class="col-6 pr-0 mb-2">
@@ -284,8 +284,11 @@
     </div>
 </div>
 
-<% 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))) { %>
+<% 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)) ||
+    (!lastStage && revise && revise.valid && (revise.status === audit.revise.status.uncheck || revise.status === audit.revise.status.checkNo) && tender.user_id === ctx.session.sessionUser.accountId)
+) { %>
 <!--标段设置-小数位数-->
 <div class="modal fade" id="bd-set-2" data-backdrop="static">
     <div class="modal-dialog" role="document">
@@ -588,19 +591,19 @@
                 <div class="sjs-option-height">
                     <table class="table table-bordered">
                         <tr><th colspan="2" class="text-center">工程款账户</th></tr>
-                        <tr><td>开户名称</td><td><input type="text" id="project-name" class="form-control form-control-sm"></td></tr>
-                        <tr><td>开户银行</td><td><input type="text" id="project-bank" class="form-control form-control-sm"></td></tr>
-                        <tr><td>开户账号</td><td><input type="text" id="project-account" class="form-control form-control-sm"></td></tr>
-                        <tr><td>分账划拨比例(%)</td><td><input type="text" id="project-rate" class="form-control form-control-sm"></td></tr>
-                        <tr><td>联系人</td><td><input type="text" id="project-contact" class="form-control form-control-sm"></td></tr>
-                        <tr><td>联系电话</td><td><input type="text" id="project-phone" class="form-control form-control-sm"></td></tr>
+                        <tr><td>开户名称</td><td><input type="text" id="project-name" class="form-control form-control-sm" oninput="limitReturn(this)"></td></tr>
+                        <tr><td>开户银行</td><td><input type="text" id="project-bank" class="form-control form-control-sm" oninput="limitReturn(this)"></td></tr>
+                        <tr><td>开户账号</td><td><input type="text" id="project-account" class="form-control form-control-sm" oninput="limitReturn(this)"></td></tr>
+                        <tr><td>分账划拨比例(%)</td><td><input type="text" id="project-rate" class="form-control form-control-sm" oninput="limitReturn(this)"></td></tr>
+                        <tr><td>联系人</td><td><input type="text" id="project-contact" class="form-control form-control-sm" oninput="limitReturn(this)"></td></tr>
+                        <tr><td>联系电话</td><td><input type="text" id="project-phone" class="form-control form-control-sm" oninput="limitReturn(this)"></td></tr>
                         <tr><th colspan="2" class="text-center">农民工工资专用账户</th></tr>
-                        <tr><td>开户名称</td><td><input type="text" id="worker-name" class="form-control form-control-sm"></td></tr>
-                        <tr><td>开户银行</td><td><input type="text" id="worker-bank" class="form-control form-control-sm"></td></tr>
-                        <tr><td>开户账号</td><td><input type="text" id="worker-account" class="form-control form-control-sm"></td></tr>
-                        <tr><td>分账划拨比例(%)</td><td><input type="text" id="worker-rate" class="form-control form-control-sm"></td></tr>
-                        <tr><td>联系人</td><td><input type="text" id="worker-contact" class="form-control form-control-sm"></td></tr>
-                        <tr><td>联系电话</td><td><input type="text" id="worker-phone" class="form-control form-control-sm"></td></tr>
+                        <tr><td>开户名称</td><td><input type="text" id="worker-name" class="form-control form-control-sm" oninput="limitReturn(this)"></td></tr>
+                        <tr><td>开户银行</td><td><input type="text" id="worker-bank" class="form-control form-control-sm" oninput="limitReturn(this)"></td></tr>
+                        <tr><td>开户账号</td><td><input type="text" id="worker-account" class="form-control form-control-sm" oninput="limitReturn(this)"></td></tr>
+                        <tr><td>分账划拨比例(%)</td><td><input type="text" id="worker-rate" class="form-control form-control-sm" oninput="limitReturn(this)"></td></tr>
+                        <tr><td>联系人</td><td><input type="text" id="worker-contact" class="form-control form-control-sm" oninput="limitReturn(this)"></td></tr>
+                        <tr><td>联系电话</td><td><input type="text" id="worker-phone" class="form-control form-control-sm" oninput="limitReturn(this)"></td></tr>
                     </table>
                 </div>
             </div>
@@ -613,7 +616,7 @@
 </div>
 <script>
     let property = JSON.parse('<%- JSON.stringify(tenderInfo) %>');
-    let ledgerChecked = <%- tender.ledger_status === audit.ledger.status.checked %>;
+    let ledgerChecked = <%- (tender.ledger_status === audit.ledger.status.checked) && ((lastStage !== undefined && lastStage !== null) || !revise || !revise.valid || revise.status === audit.revise.status.checked) %>;
     let firstStageChecked = <%- lastStage !== undefined && lastStage !== null && (lastStage.order > 1 || (lastStage.order === 1 && lastStage.status === audit.stage.status.checked)) %>;
 
     // 根据Min Max限制Input输入
@@ -647,6 +650,11 @@
         limitReg(obj, /\.{2,}/g); // 过滤第二个.
         limitInputMinMax(obj);
     }
+
+    function limitReturn(obj) {
+        limitReg(obj, /\s/g);
+    }
+
     function checkNumberValid(obj) {
         const value = _.toNumber(obj.value);
         obj.value = value ? value : '';
@@ -969,7 +977,7 @@
             return precision;
         }
         function checkPrecisionMinLimit(precision, limit) {
-            for (const unit of precision) {
+            for (const unit in precision) {
                 if (precision[unit].value < limit[unit].value) {
                     if (precision[unit].unit) {
                         toastr.warning('台账已审批通过,清单精度不可减少,单位为' + limit[unit].unit + '的清单的精度不可小于' + limit[unit].value);
@@ -985,8 +993,11 @@
         return {loadPrecisonProperty, setReadOnly, getNewPrecisionData, checkPrecisionMinLimit};
     })();
     $('#bd-set-3').on('show.bs.modal', function () {
-        <% 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))) { %>
+        <% 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)) ||
+                (!lastStage && revise && revise.valid && (revise.status === audit.revise.status.uncheck || revise.status === audit.revise.status.checkNo) && tender.user_id === ctx.session.sessionUser.accountId)
+        )) { %>
         precisionObj.setReadOnly(true);
         <% } %>
         precisionObj.loadPrecisonProperty();
@@ -1190,6 +1201,7 @@
             for (let iRow = 0; iRow < sheet.getRowCount(); iRow++) {
                 const cell = sheet.getCell(iRow, 1);
                 const defaultStype = sheet.getDefaultStyle();
+                cell.text(cell.text().replace(/\s/g, ''))
                 if (cell.text().length > 50) {
                     cell.backColor('#f8d7da');
                     hint = '章节名称的长度超出范围,请重新输入';
@@ -1207,6 +1219,9 @@
         spread.bind(spreadNS.Events.EditEnding, function (e, info) {
             checkSheetData(info.sheet);
         });
+        spread.bind(spreadNS.Events.EditEnded, function (e, info) {
+            checkSheetData(info.sheet);
+        });
         spread.bind(spreadNS.Events.ClipboardPasted, function (e, info) {
             checkSheetData(info.sheet);
         });
@@ -1230,7 +1245,7 @@
                 const data = {};
                 for (let iCol = 0; iCol < sheet.getColumnCount(); iCol++) {
                     const col = spreadSetting.cols[iCol];
-                    data[col.field] = sheet.getText(iRow, iCol);
+                    data[col.field] = sheet.getText(iRow, iCol).replace(/\s/g, '');
                     if (col.field === 'name') {
                         if (data.name.length > 50) {
                             return null;

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

@@ -0,0 +1,167 @@
+<% include ./tender_sub_menu.ejs %>
+<div class="panel-content">
+    <div class="panel-title">
+        <div class="title-main d-flex">
+            <% include ./tender_sub_mini_menu.ejs %>
+            <h2>审批流程设置</h2>
+        </div>
+    </div>
+    <div class="content-wrap">
+        <div class="c-body">
+            <div class="sjs-height-0">
+                <div class="row m-0 mt-3">
+                    <div class="col-7">
+                        <% for (const sp of shenpi.sp_lc) { %>
+                            <div class="card mb-3">
+                                <div class="card-body">
+                                    <h5 class="card-title"><%- sp.name %></h5>
+                                    <div class="form-group">
+                                        <div class="form-group form-check">
+                                            <% for (const st in shenpi.sp_status_list) { %>
+                                                <div class="custom-control custom-checkbox custom-control-inline">
+                                                    <input type="radio" class="custom-control-input" data-code="<%- sp.code %>" value="<%- shenpi.sp_status_list[st].status %>" name="<%- sp.code %>" id="<%- sp.code %>_<%- shenpi.sp_status_list[st].status %>" <%if (sp.status === shenpi.sp_status_list[st].status) { %>checked<% } %>>
+                                                    <label class="custom-control-label" for="<%- sp.code %>_<%- shenpi.sp_status_list[st].status %>"><%- shenpi.sp_status_list[st].name %></label>
+                                                </div>
+                                            <% } %>
+                                        </div>
+                                    </div>
+                                    <div class="alert alert-warning"><%- shenpi.sp_status_list[sp.status].name %>:<%- shenpi.sp_status_list[sp.status].msg %></div>
+                                    <div class="lc-show">
+                                    <% if (sp.status === shenpi.sp_status.gdspl) { %>
+                                    <ul class="list-unstyled">
+                                        <% if (sp.auditList.length > 0) { %>
+                                        <% for (const [i, audit] of sp.auditList.entries()) { %>
+                                        <li class="d-flex justify-content-start mb-3">
+                                            <span class="col-auto"><%- ctx.helper.transFormToChinese(i+1) %>审</span>
+                                            <span class="col-7 spr-span">
+                                                <span class="d-inline-block"></span>
+                                                <span class="d-inline-block"><span class="badge badge-light"><%- audit.name %> <a href="javascript:void(0);" class="remove-audit btn-sm text-danger px-1" title="移除" data-id="<%- audit.audit_id %>"><i class="fa fa-remove"></i></a></span> </span>
+                                            </span>
+                                        </li>
+                                        <% } %>
+                                        <li class="pl-3"><a href="javascript:void(0);" class="add-audit" ><i class="fa fa-plus"></i> 添加流程</a></li>
+                                        <% } else { %>
+                                        <li class="d-flex justify-content-start mb-3">
+                                            <span class="col-auto">一审</span>
+                                            <span class="col-7 spr-span">
+                                            <span class="d-inline-block">
+                                                <div class="dropdown text-right">
+                                                    <button class="btn btn-outline-primary btn-sm dropdown-toggle" type="button" id="<%- sp.code %>_dropdownMenuButton" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
+                                                        选择审批人
+                                                    </button>
+                                                    <div class="dropdown-menu dropdown-menu-right" aria-labelledby="<%- sp.code %>_dropdownMenuButton" style="width:220px">
+                                                        <div class="mb-2 p-2"><input class="form-control form-control-sm gr-search"
+                                                                                     placeholder="姓名/手机 检索" autocomplete="off"></div>
+                                                        <dl class="list-unstyled book-list">
+                                                            <% accountGroup.forEach((group, idx) => { %>
+                                                                <dt><a href="javascript: void(0);" class="acc-btn" data-groupid="<%- idx %>" data-type="hide"><i class="fa fa-plus-square"></i></a> <%- group.groupName %></dt>
+                                                                <div class="dd-content" data-toggleid="<%- idx %>">
+                                                                    <% group.groupList.forEach(item => { %>
+                                                                        <% if (item.id !== ctx.session.sessionUser.accountId) { %>
+                                                                            <dd class="border-bottom p-2 mb-0 " data-id="<%- item.id %>" >
+                                                                    <p class="mb-0 d-flex"><span class="text-primary"><%- item.name %></span><span
+                                                                                class="ml-auto"><%- item.mobile %></span></p>
+                                                                    <span class="text-muted"><%- item.role %></span>
+                                                                    </dd>
+                                                                        <% } %>
+                                                                    <% });%>
+                                                                </div>
+                                                            <% }) %>
+                                                        </dl>
+                                                    </div>
+                                                </div>
+                                            </span>
+                                        </span>
+                                        </li>
+                                        <% } %>
+                                    </ul>
+                                    <% } else if (sp.status === shenpi.sp_status.gdzs) { %>
+                                    <ul class="list-unstyled">
+                                        <li class="d-flex justify-content-start mb-3">
+                                            <span class="col-auto">授权审批人</span>
+                                            <span class="col-7">
+                                                <span class="d-inline-block"></span>
+                                            </span>
+                                        </li>
+                                        <% if (sp.audit) { %>
+                                        <li class="d-flex justify-content-start mb-3">
+                                            <span class="col-auto">终审</span>
+                                            <span class="col-7 spr-span">
+                                            <span class="d-inline-block"></span>
+                                            <span class="d-inline-block"><span class="badge badge-light"><%- sp.audit.name %> <a href="javascript:void(0);" class="remove-audit btn-sm text-danger px-1" title="移除" data-id="<%- sp.audit.audit_id %>"><i class="fa fa-remove"></i></a></span> </span>
+                                            </span>
+                                        </li>
+                                        <% } else { %>
+                                        <li class="d-flex justify-content-start mb-3">
+                                            <span class="col-auto">终审</span>
+                                            <span class="col-7 spr-span">
+                                                <span class="d-inline-block">
+                                                    <div class="dropdown text-right">
+                                                        <button class="btn btn-outline-primary btn-sm dropdown-toggle" type="button" id="<%- sp.code %>_dropdownMenuButton" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
+                                                            选择审批人
+                                                        </button>
+                                                        <div class="dropdown-menu dropdown-menu-right" aria-labelledby="<%- sp.code %>_dropdownMenuButton" style="width:220px">
+                                                            <div class="mb-2 p-2"><input class="form-control form-control-sm gr-search"
+                                                                                         placeholder="姓名/手机 检索" autocomplete="off"></div>
+                                                            <dl class="list-unstyled book-list">
+                                                                <% accountGroup.forEach((group, idx) => { %>
+                                                                <dt><a href="javascript: void(0);" class="acc-btn" data-groupid="<%- idx %>" data-type="hide"><i class="fa fa-plus-square"></i></a> <%- group.groupName %></dt>
+                                                                    <div class="dd-content" data-toggleid="<%- idx %>">
+                                                                    <% group.groupList.forEach(item => { %>
+                                                                        <% if (item.id !== ctx.session.sessionUser.accountId) { %>
+                                                                        <dd class="border-bottom p-2 mb-0 " data-id="<%- item.id %>" >
+                                                                        <p class="mb-0 d-flex"><span class="text-primary"><%- item.name %></span><span
+                                                                                    class="ml-auto"><%- item.mobile %></span></p>
+                                                                        <span class="text-muted"><%- item.role %></span>
+                                                                        </dd>
+                                                                        <% } %>
+                                                                    <% });%>
+                                                                    </div>
+                                                                <% }) %>
+                                                            </dl>
+                                                        </div>
+                                                    </div>
+                                                </span>
+                                            </span>
+                                        </li>
+                                        <% } %>
+                                    </ul>
+                                    <% } %>
+                                    </div>
+                                </div>
+                            </div>
+                        <% } %>
+                    </div>
+                </div>
+            </div>
+        </div>
+    </div>
+</div>
+<script src="/public/js/sub_menu.js"></script>
+<script>
+    const sp_type = JSON.parse('<%- JSON.stringify(shenpi.sp_type) %>');
+    const sp_status = JSON.parse('<%- JSON.stringify(shenpi.sp_status) %>');
+    const sp_status_list = JSON.parse('<%- JSON.stringify(shenpi.sp_status_list) %>');
+    const accountGroup = JSON.parse('<%- JSON.stringify(accountGroup) %>');
+    const accountList = JSON.parse('<%- JSON.stringify(accountList) %>');
+    const cur_uid = parseInt('<%- ctx.session.sessionUser.accountId %>');
+</script>
+<script src="/public/js/shenpi.js"></script>
+<script>
+    $.subMenu({
+        menu: '#sub-menu', miniMenu: '#sub-mini-menu', miniMenuList: '#mini-menu-list',
+        toMenu: '#to-menu', toMiniMenu: '#to-mini-menu',
+        key: 'menu.1.0.0',
+        miniHint: '#sub-mini-hint', hintKey: 'menu.hint.1.0.1',
+        callback: function (info) {
+            if (info.mini) {
+                $('.panel-title').addClass('fluid');
+                $('#sub-menu').removeClass('panel-sidebar');
+            } else {
+                $('.panel-title').removeClass('fluid');
+                $('#sub-menu').addClass('panel-sidebar');
+            }
+            autoFlashHeight();
+        }
+    });
+</script>

+ 3 - 1
sql/update.sql

@@ -1,2 +1,4 @@
 ALTER TABLE `zh_s2b_proj`
-ADD COLUMN `common_option`  text CHARACTER SET utf8 COLLATE utf8_general_ci NULL COMMENT '通用配置' AFTER `dagl_option`;
+ADD COLUMN `common_option`  text CHARACTER SET utf8 COLLATE utf8_general_ci NULL COMMENT '通用配置' AFTER `dagl_option`;
+
+ALTER TABLE `zh_tender_info` ADD `shenpi` VARCHAR(1000) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '审批流程设置' AFTER `pay_account`;