소스 검색

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

Tony Kang 2 년 전
부모
커밋
ff5979910d

+ 3 - 3
app/base/base_tree_service.js

@@ -67,11 +67,11 @@ class TreeService extends Service {
     async getData(mid, level) {
         if (level) {
             this.initSqlBuilder();
-            this.sqlBuilder.setAndWhere('list_id', {
+            this.sqlBuilder.setAndWhere(this.setting.mid, {
                 operate: '=',
                 value: mid,
             });
-            this.sqlBuilder.setAndWhere('level', {
+            this.sqlBuilder.setAndWhere(this.setting.level, {
                 operate: '<=',
                 value: level,
             });
@@ -79,7 +79,7 @@ class TreeService extends Service {
             return await this.db.query(sql, sqlParam);
         } else {
             return await this.db.select(this.departTableName(mid), {
-                where: this.getCondition({mid: mid})
+                where: this.getCondition({ mid: mid })
             });
         }
     }

+ 7 - 6
app/const/payment.js

@@ -55,16 +55,17 @@ const modes_value_object = {
 const rpt_dataType = {
     intact_type_text: 'text',
     intact_type_number: 'number',
+    intact_type_date: 'date',
     intact_type_multitext: 'textarea',
 };
 
-const rpt_control_name = ['Title', 'Header', 'Footer'];
+// const rpt_band_name = ['TitleBand', 'HeaderBand', 'FooterBand'];
 
-const rpt_control = {
-    Title: '标题',
-    Header: '表头',
+const rpt_band_name = {
+    TitleBand: '标题',
+    HeaderBand: '表头',
     content: '表单',
-    Footer: '表尾',
+    FooterBand: '表尾',
 };
 
 const signature_msg = {
@@ -83,7 +84,7 @@ module.exports = {
     permission_value_array,
     const_rpt_list,
     rpt_dataType,
-    rpt_control,
+    rpt_band_name,
     signature_msg,
     setting_modes,
     modes_value,

+ 2 - 0
app/controller/dashboard_controller.js

@@ -33,6 +33,7 @@ module.exports = app => {
             const auditChangeProject = ctx.session.sessionProject.page_show.openChangeProject ? await ctx.service.changeProjectAudit.getAuditChangeProject(ctx.session.sessionUser.accountId) : [];
             const auditChangeApply = ctx.session.sessionProject.page_show.openChangeApply ? await ctx.service.changeApplyAudit.getAuditChangeApply(ctx.session.sessionUser.accountId) : [];
             const auditChangePlan = ctx.session.sessionProject.page_show.openChangePlan ? await ctx.service.changePlanAudit.getAuditChangePlan(ctx.session.sessionUser.accountId) : [];
+            const auditPayments = await ctx.service.paymentDetailAudit.getAuditPayment(ctx.session.sessionUser.accountId);
             const pa = await ctx.service.projectAccount.getDataById(ctx.session.sessionUser.accountId);
             const noticeList = await ctx.service.noticePush.getNotice(ctx.session.sessionProject.id, pa.id);
             const projectData = await ctx.service.project.getDataById(ctx.session.sessionProject.id);
@@ -86,6 +87,7 @@ module.exports = app => {
                 auditChangeProject,
                 auditChangeApply,
                 auditChangePlan,
+                auditPayments,
                 shenpi_count,
                 total_count,
                 last_day: ctx.helper.calcDayNum(last_time),

+ 1 - 1
app/controller/login_controller.js

@@ -207,7 +207,7 @@ module.exports = app => {
          */
         async logout(ctx) {
             // 删除session并跳转
-            const code = ctx.session.sessionProject.code ? ctx.session.sessionProject.code : null;
+            const code = ctx.session.sessionProject && ctx.session.sessionProject.code ? ctx.session.sessionProject.code : null;
             ctx.session = null;
             if (code) {
                 ctx.redirect('/login/' + code);

+ 173 - 11
app/controller/payment_controller.js

@@ -4,6 +4,10 @@ const JV = require('../reports/rpt_component/jpc_value_define');
 const shenpiConst = require('../const/shenpi');
 const auditConst = require('../const/audit').stage;
 const paymentConst = require('../const/payment');
+const moment = require('moment');
+const path = require('path');
+const sendToWormhole = require('stream-wormhole');
+const fs = require('fs');
 
 module.exports = app => {
 
@@ -354,12 +358,22 @@ module.exports = app => {
                         }
                     }
                 }
+                const auditIdList = ctx.helper._.map(ctx.detail.auditors, 'aid');
+                const rptAuditIdList = ctx.helper._.map(ctx.detail.rptAudits, 'uid');
+                const uidList = ctx.helper._.uniq([...auditIdList, ...rptAuditIdList]);
+
+                // 获取附件列表
+                const attList = await ctx.service.paymentDetailAtt.getPaymentDetailAttachment(ctx.detail.id, 'desc');
                 const renderData = {
                     trInfo: ctx.trInfo,
                     paymentConst,
                     jsFiles: this.app.jsFiles.common.concat(this.app.jsFiles.payment.detail),
                     auditConst,
                     shenpiConst,
+                    attList,
+                    moment,
+                    whiteList: ctx.app.config.multipart.whitelist,
+                    uidList,
                     preUrl: '/payment/' + ctx.tender.id + '/detail/' + ctx.detail.id,
                     OSS_PATH: ctx.app.config.fujianOssPath,
                 };
@@ -376,14 +390,14 @@ module.exports = app => {
                             label: cell.Label,
                             index: i,
                         };
-                        const thisControl = paymentConst.rpt_control[cell.control] ? cell.control : 'content';
-                        const oneShowContent = ctx.helper._.find(content, { control: thisControl });
+                        const thisBandName = paymentConst.rpt_band_name[cell.BandName] ? cell.BandName : 'content';
+                        const oneShowContent = ctx.helper._.find(content, { BandName: thisBandName });
                         if (oneShowContent) {
                             oneShowContent.items.push(push_item);
                         } else {
                             content.push({
-                                control: thisControl,
-                                title: paymentConst.rpt_control[thisControl],
+                                BandName: thisBandName,
+                                title: paymentConst.rpt_band_name[thisBandName],
                                 items: [push_item],
                             });
                         }
@@ -714,12 +728,15 @@ module.exports = app => {
                     where: { project_id: ctx.session.sessionProject.id, enable: 1 },
                     columns: ['id', 'name', 'company', 'role', 'enable', 'is_admin', 'account_group', 'mobile'],
                 });
-                for (const s of trDetailList) {
-                    // s.curAuditor = null;
-                    // 根据期状态返回展示用户
-                    s.curAuditor = await ctx.service.paymentDetailAudit.getAuditorByStatus(s.id, s.status, s.times);
-                    const userInfo = ctx.helper._.find(accountList, { id: s.uid });
-                    s.user_name = userInfo ? userInfo.name : '';
+                if (trDetailList.length > 0) {
+                    for (const s of trDetailList) {
+                        // s.curAuditor = null;
+                        // 根据期状态返回展示用户
+                        s.curAuditor = await ctx.service.paymentDetailAudit.getAuditorByStatus(s.id, s.status, s.times);
+                        const userInfo = ctx.helper._.find(accountList, { id: s.uid });
+                        s.user_name = userInfo ? userInfo.name : '';
+                    }
+                    trDetailList[0].emptySign = await ctx.service.paymentRptAudit.haveEmptySign(trDetailList[0].id);
                 }
                 const renderData = {
                     tender: ctx.tender,
@@ -748,7 +765,7 @@ module.exports = app => {
                             const report_items_json = JSON.parse(trInfo.report_items_json);
                             const items = ['cells', 'signature_audit_cells', 'signature_cells', 'signature_date_cells', 'interact_cells'];
                             for (const item of items) {
-                                if (!difference && report_items_json[item] &&
+                                if (report_items_json[item] &&
                                     !ctx.helper._.isEmpty(ctx.helper._.differenceWith(JSON.parse(JSON.stringify(renderData.rptMsg[item])), report_items_json[item], ctx.helper._.isEqual))) {
                                     // 因为interact_cells里存在undefind值,必须先用JSON.parse和JSON.stringify转义才能对比
                                     difference = true;
@@ -918,6 +935,151 @@ module.exports = app => {
                 ctx.redirect(ctx.request.header.referer);
             }
         }
+
+        /**
+         * 上传附件
+         * @param {Object} ctx - egg全局变量
+         * @return {void}
+         */
+        async uploadDetailFile(ctx) {
+            const responseData = {
+                err: 0,
+                msg: '',
+                data: [],
+            };
+            let stream;
+            try {
+                const parts = ctx.multipart({ autoFields: true });
+                const files = [];
+                let index = 0;
+                const extra_upload = ctx.detail.status === auditConst.status.checked;
+                const original_data = {
+                    tender_id: ctx.tender.id,
+                    tr_id: ctx.trInfo.id,
+                    td_id: ctx.detail.id,
+                }
+                while ((stream = await parts()) !== undefined) {
+                    // 判断用户是否选择上传文件
+                    if (!stream.filename) {
+                        throw '请选择上传的文件!';
+                    }
+                    const fileInfo = path.parse(stream.filename);
+                    const create_time = Date.parse(new Date()) / 1000;
+                    const filepath = `app/public/upload/payment/${original_data.tender_id}/detail/fujian_${create_time + index.toString() + fileInfo.ext}`;
+                    await ctx.app.fujianOss.put(ctx.app.config.fujianOssFolder + filepath, stream);
+                    await sendToWormhole(stream);
+                    // 保存数据到att表
+                    const fileData = {
+                        upload_time: new Date(),
+                        filename: fileInfo.name,
+                        fileext: fileInfo.ext,
+                        filesize: Array.isArray(parts.field.size) ? parts.field.size[index] : parts.field.size,
+                        filepath,
+                        extra_upload,
+                    };
+                    const result = await ctx.service.paymentDetailAtt.save(original_data, fileData, ctx.session.sessionUser.accountId);
+                    if (!result) {
+                        throw '导入数据库保存失败';
+                    }
+                    fileData.uid = ctx.session.sessionUser.accountId;
+                    fileData.id = result.insertId;
+                    fileData.orginpath = ctx.app.config.fujianOssPath + filepath;
+                    delete fileData.filepath;
+                    if (!ctx.helper.canPreview(fileData.fileext)) {
+                        fileData.filepath = `/payment/${original_data.tender_id}/detail/${original_data.td_id}/file/${fileData.id}/download`;
+                    } else {
+                        fileData.filepath = ctx.app.config.fujianOssPath + filepath;
+                    }
+                    files.push(fileData);
+                    ++index;
+                }
+                responseData.data = files;
+            } catch (err) {
+                this.log(err);
+                // 失败需要消耗掉stream 以防卡死
+                if (stream) {
+                    await sendToWormhole(stream);
+                }
+                this.setMessage(err.toString(), this.messageType.ERROR);
+            }
+            ctx.body = responseData;
+        }
+
+        /**
+         * 下载附件
+         * @param {Object} ctx - egg全局变量
+         * @return {void}
+         */
+        async downloadDetailFile(ctx) {
+            const id = ctx.params.fid;
+            if (id) {
+                try {
+                    const fileInfo = await ctx.service.paymentDetailAtt.getDataById(id);
+                    if (fileInfo !== undefined && fileInfo !== '') {
+                        // 解决中文无法下载问题
+                        const userAgent = (ctx.request.header['user-agent'] || '').toLowerCase();
+                        let disposition = '';
+                        if (userAgent.indexOf('msie') >= 0 || userAgent.indexOf('chrome') >= 0) {
+                            disposition = 'attachment; filename=' + encodeURIComponent(fileInfo.filename + fileInfo.fileext);
+                        } else if (userAgent.indexOf('firefox') >= 0) {
+                            disposition = 'attachment; filename*="utf8\'\'' + encodeURIComponent(fileInfo.filename + fileInfo.fileext) + '"';
+                        } else {
+                            /* safari等其他非主流浏览器只能自求多福了 */
+                            disposition = 'attachment; filename=' + new Buffer(fileInfo.filename + fileInfo.fileext).toString('binary');
+                        }
+                        ctx.response.set({
+                            'Content-Type': 'application/octet-stream',
+                            'Content-Disposition': disposition,
+                            'Content-Length': fileInfo.filesize,
+                        });
+                        // ctx.body = await fs.createReadStream(fileName);
+                        ctx.body = await ctx.helper.ossFileGet(fileInfo.filepath);
+                    } else {
+                        throw '不存在该文件';
+                    }
+                } catch (err) {
+                    this.log(err);
+                    this.setMessage(err.toString(), this.messageType.ERROR);
+                }
+            }
+        }
+
+        /**
+         * 删除附件
+         * @param {Object} ctx - egg全局变量
+         * @return {void}
+         */
+        async deleteDetailFile(ctx) {
+            const responseData = {
+                err: 0,
+                msg: '',
+                data: '',
+            };
+            try {
+                const data = JSON.parse(ctx.request.body.data);
+                const fileInfo = await ctx.service.paymentDetailAtt.getDataById(data.id);
+                if (!fileInfo || !Object.keys(fileInfo).length) {
+                    throw '该文件不存在';
+                }
+                if (!fileInfo.extra_upload && ctx.detail.status === auditConst.status.checked) {
+                    throw '无权限删除';
+                }
+                if (fileInfo !== undefined && fileInfo !== '') {
+                    // 先删除文件
+                    await ctx.app.fujianOss.delete(ctx.app.config.fujianOssFolder + fileInfo.filepath);
+                    // 再删除数据库
+                    await ctx.service.paymentDetailAtt.deleteById(data.id);
+                    responseData.data = '';
+                } else {
+                    throw '不存在该文件';
+                }
+            } catch (err) {
+                responseData.err = 1;
+                responseData.msg = err;
+            }
+
+            ctx.body = responseData;
+        }
     }
 
     return PaymentController;

+ 2 - 0
app/controller/stage_controller.js

@@ -262,6 +262,7 @@ module.exports = app => {
             const ledgerData = ctx.stage.ledgerHis
                 ? await ctx.helper.loadLedgerDataFromOss(ctx.stage.ledgerHis.bills_file)
                 : await ctx.service.ledger.getAllDataByCondition({ columns: this.ledgerColumn, where: { tender_id: ctx.tender.id } });
+            const memoData = ctx.stage.ledgerHis ? await ctx.service.ledger.getAllDataByCondition({ columns: ['id', 'memo'], where: { tender_id: ctx.tender.id } }) : [];
             const dgnData = await ctx.service.stageBillsDgn.getDgnData(ctx.tender.id);
             const extraData = await ctx.service.ledgerExtra.getData(ctx.tender.id, this.ledgerExtraColumn);
             const pcData = await ctx.service.stageBillsPc.getAllDataByCondition({ where: { sid: ctx.stage.id } });
@@ -281,6 +282,7 @@ module.exports = app => {
             const preStageData = ctx.stage.order > 1 ? await ctx.service.stageBillsFinal.getFinalData(ctx.tender.data, ctx.stage.order - 1) : [];
             this.ctx.helper.assignRelaData(ledgerData, [
                 { data: dgnData, fields: ['deal_dgn_qty1', 'deal_dgn_qty2', 'c_dgn_qty1', 'c_dgn_qty2'], prefix: '', relaId: 'id' },
+                { data: memoData, fields: ['memo'], prefix: '', relaId: 'id' },
                 { data: extraData, fields: this.ledgerExtraColumn, prefix: '', relaId: 'id' },
                 { data: importData, fields: ['is_import'], prefix: '', relaId: 'lid' },
                 { data: curStageData, fields: ['contract_qty', 'contract_expr', 'contract_tp', 'qc_qty', 'qc_tp', 'qc_minus_qty', 'postil'], prefix: '', relaId: 'lid' },

+ 2 - 9
app/middleware/payment_detail_check.js

@@ -50,15 +50,7 @@ module.exports = options => {
                 } else {
                     detail.curOrder = detail.curAuditor.aid === accountId ? detail.curAuditor.order : detail.curAuditor.order - 1;
                 }
-            } else if (this.tender.isTourist) {
-                detail.curTimes = detail.times;
-                if (detail.status === status.uncheck || detail.status === status.checkNo) {
-                    detail.curOrder = 0;
-                } else if (detail.status === status.checked) {
-                    detail.curOrder = _.max(_.map(detail.auditors, 'order'));
-                } else {
-                    detail.curOrder = detail.curAuditor.order;
-                }
+                detail.filePermission = true;
             } else if (auditorIds.indexOf(accountId) !== -1 || rptAuditIds.indexOf(accountId) !== -1 || this.payment.auditPermission.view_all) { // 审批人及签署人及查看所有权人
                 if (detail.status === status.uncheck) {
                     throw '您无权查看该数据';
@@ -75,6 +67,7 @@ module.exports = options => {
                 } else {
                     detail.curOrder = accountId === detail.curAuditor.aid ? detail.curAuditor.order : detail.curAuditor.order - 1;
                 }
+                detail.filePermission = auditorIds.indexOf(accountId) !== -1 || rptAuditIds.indexOf(accountId) !== -1;
             } else { // 其他不可见
                 throw '您无权查看该数据';
             }

+ 8 - 2
app/public/css/main.css

@@ -1882,6 +1882,9 @@ overflow-y: auto;
 .bg-new-material{
     background: rgba(187, 41, 210, 0.08) !important;
 }
+.bg-new-payment{
+    background: rgba(128, 128, 0, 0.08) !important;
+}
 .text-new-advance{
   color: rgba(241, 82, 91, 1) !important;
 }
@@ -1909,6 +1912,9 @@ overflow-y: auto;
 .text-new-material{
     color: rgba(187, 41, 210, 1); !important;
 }
+.text-new-payment{
+    color: rgba(128, 128, 0, 1) !important;
+}
 .text-width{
   width: 66px;
   text-align: center;
@@ -2054,7 +2060,7 @@ animation:shake 1s .2s ease both;}
 .private-stamp-img{
   display: inline-block;
   margin: auto;
-  vertical-align: middle; 
+  vertical-align: middle;
 }
 .private-stamp-img .check-state{
   position: absolute;
@@ -2090,4 +2096,4 @@ animation:shake 1s .2s ease both;}
 }
 .form-control-width{
     min-width: 450px;
-}
+}

+ 226 - 4
app/public/js/payment_detail.js

@@ -25,11 +25,27 @@ $(function () {
     autoFlashHeight();
     auditRptPrintHelper.showPage();
     iniPage();
+    //初始化所有附件列表
+    getAllList();
 
     $('#rpt-form input').on('change', function () {
-       const newVal = $(this).val();
+       let newVal = $(this).val();
        const index = parseInt($(this).data('index'));
-        checkAndUpdate(index, newVal, $(this));
+       const type = $(this).attr('type');
+       if (type === 'number' && newVal) {
+           const val = _.toNumber(newVal);
+           if (countDigits(val) > 11) {
+               toastr.error('请输入11位整数内的数');
+               return;
+           }
+           const reg = new RegExp("^\\d+(\\.\\d{1,"+ 6 +"})?$");
+           if (!reg.test(val)) {
+               toastr.warning('已保留6位小数');
+               newVal = ZhCalc.round(val, 6);
+               $(this).val(newVal);
+           }
+       }
+       checkAndUpdate(index, newVal, $(this));
     });
 
     $('#rpt-form textarea').on('change', function () {
@@ -72,16 +88,21 @@ $(function () {
         $('#sign_path').prop('checked', rptAudit.signature_msg.sign_path !== null);
         $('#company_stamp').prop('checked', rptAudit.signature_msg.company_stamp !== null);
         $('#stamp_path').prop('checked', rptAudit.signature_msg.stamp_path !== null);
-        $('#signature_date').val(rptAudit.signature_msg.date ? rptAudit.signature_msg.date : '');
+        $('#signature_date').val(rptAudit.signature_msg.date ? rptAudit.signature_msg.date : new Date());
         signatureDate = !signatureDate ? $('#signature_date').datepicker().data('datepicker') : signatureDate;
         if (signatureDate && rptAudit.signature_msg.date) {
             signatureDate.selectDate(new Date(rptAudit.signature_msg.date));
         } else if (signatureDate) {
-            signatureDate.clear();
+            signatureDate.selectDate(new Date());
+            // signatureDate.clear();
         }
         $('#signature_content').val(rptAudit.signature_msg.content ? rptAudit.signature_msg.content : '');
     });
 
+    $('#sp-done').on('show.bs.modal', function () {
+        $('#sp-done').find('textarea').val(rptAudit && rptAudit.signature_msg.content ? rptAudit.signature_msg.content : '同意');
+    });
+
     $('#commit_sign').click(function () {
         rptAudit.signature_msg.sign_path = $('#sign_path').is(':checked') ? $('#sign_path').val() : null;
         rptAudit.signature_msg.company_stamp = $('#company_stamp').is(':checked') ? $('#company_stamp').val() : null;
@@ -132,4 +153,205 @@ $(function () {
             clearTimeout(timer);
         }, 500);
     }
+
+    //tab change
+    $('a[data-toggle="tab"]').on('shown.bs.tab', function () {
+        const tab = $(this).data('tab');
+        if (tab === 'fujian') {
+            $('#fujian_btn').show();
+        } else {
+            $('#fujian_btn').hide();
+        }
+    });
+
+    // 切换页数
+    $('.page-select').on('click', function () {
+        const totalPageNum = parseInt($('#totalPage').text());
+        const lastPageNum = parseInt($('#currentPage').text());
+        const status = $(this).attr('content');
+        if (status === 'pre' && lastPageNum > 1) {
+            getAllList(lastPageNum-1);
+            $('#annex .check-all-file').prop('checked', false)
+        } else if (status === 'next' && lastPageNum < totalPageNum) {
+            getAllList(lastPageNum+1);
+            $('#annex .check-all-file').prop('checked', false)
+        }
+    });
+    // 上传附件
+    $('#upload-file-btn').click(function () {
+        const files = $('#upload-file')[0].files;
+        const formData = new FormData();
+        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);
+        }
+        if (uidList.indexOf(accountId) === -1) {
+            return toastr.error('暂无权限上传!');
+        }
+        postDataWithFile(window.location.pathname + '/file/upload', formData, function (data) {
+            attData = data.concat(attData);
+            // 重新生成List
+            getAllList();
+            $('#addfujian').modal('hide');
+        }, function () {
+        });
+        $('#upload-file').val('');
+
+    });
+
+    // 删除附件
+    $('body').on('click', '.delete-file', function () {
+        let attid = $(this).data('attid');
+        let self = $(this);
+        const data = {id: attid};
+        postData(window.location.pathname + '/file/delete', data, function (result) {
+            // 删除到attData中
+            const att_index = attData.findIndex(function (item) {
+                return item.id === parseInt(attid);
+            });
+            attData.splice(att_index, 1);
+            // 重新生成List
+            if ($('#attList tr').length === 1) {
+                getAllList(parseInt($('#currentPage').text()) - 1);
+
+            } else {
+                getAllList(parseInt($('#currentPage').text()));
+            }
+        });
+    });
+
+    $('#attList').on('click', '.file-atn', function() {
+        const id = $(this).attr('f-id');
+        postData(`/payment/${tenderId}/detail/${detailId}/file/${id}/download`, {}, (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) {
+            if (fileIds.length > 20) {
+                return toastr.warning(`最大允许20个文件(当前${fileIds.length}个)`);
+            }
+            toastr.success('正在进行压缩文件...', '', { timeOut: 0, extendedTimeOut: 0});
+            $(this).attr('disabled', "true");
+            const btn = $(this);
+
+            const fileArr = [];
+            for (const id of fileIds) {
+                const fileInfo = _.find(currPageFileData, { id: parseInt(id) });
+                fileArr.push({
+                    url: fileInfo.orginpath, //文件的oss存储路径 (必填)
+                    name: fileInfo.filename, // 文件名 (可选, 不需要填扩展名)
+                    foldPath: '' // (可选, 文件在压缩包中的存储路径)
+                });
+            }
+            const packageName = `${trName}-${detailName}-附件.zip`;
+            try {
+                zipOss.downloadFromAliOss(fileArr, packageName, btn);
+            } catch (e) {
+                btn.removeAttr('disabled');
+                toastr.clear();
+                toastr.error('批量下载失败');
+            }
+        }
+    });
 });
+
+// 生成附件列表
+function getAllList(currPageNum = 1) {
+    // 每页最多几个附件
+    const pageCount = 20;
+    // 附件总数
+    const total = attData.length;
+    // 总页数
+    const pageNum = Math.ceil(total/pageCount);
+    $('#totalPage').text(pageNum);
+    $('#currentPage').text(total === 0 ? 0 : currPageNum);
+    // 当前页附件内容
+    const currPageAttData = attData.slice((currPageNum-1)*pageCount, currPageNum*pageCount);
+    currPageFileData = currPageAttData;
+    let html = '';
+    // '/tender/' + tender.id + '/measure/stage/' + stage.order + '/download/file/' + att.id
+    for(const [index,att] of currPageAttData.entries()) {
+        html += `<tr>
+        <td width="25"><input type="checkbox" class="check-file" file-id=${att.id}></td>
+        <td>${((currPageNum-1)*pageCount)+index+1}</td>
+        <td><a href="${att.filepath}" target="_blank" class="pl-0 col-11 att-file-name" file-id=${att.id}>${att.filename}${att.fileext}</a></td>
+        <td>${moment(att.upload_time).format("YYYY-MM-DD HH:mm:ss")}<br>${bytesToSize(att.filesize)}</td>
+        <td>
+            <a href="/payment/${tenderId}/detail/${detailId}/file/${att.id}/download" class="mr-2" title="下载"><span class="fa fa-download text-primary"></span></a>`
+        html += (att.uid === accountId && (detailStatus === auditConst.status.checked ? Boolean(att.extra_upload) : true)) ?
+            `<a href="javascript:void(0)" class="mr-2 delete-file" data-attid="${att.id}" title="删除附件"><span class="fa fa-trash text-danger"></span></a>` : '';
+        html += `</td>`;
+    }
+    $('#attList').html(html);
+    $('#attList').on('click', 'tr', function() {
+        $('#attList tr').removeClass('bg-light');
+        $(this).addClass('bg-light');
+    });
+}
+// 整数位数判断
+function countDigits(number) {
+    var numberString = Math.abs(number).toString();
+    var decimalIndex = numberString.indexOf('.'); // 查找小数点的索引
+    var scientificIndex = numberString.toLowerCase().indexOf('e'); // 查找科学计数法中的'e'索引
+    var integerPart;
+
+    if (decimalIndex !== -1) {
+        integerPart = numberString.slice(0, decimalIndex); // 提取小数点之前的部分作为整数部分
+    } else if (scientificIndex !== -1) {
+        integerPart = numberString.slice(0, scientificIndex); // 提取科学计数法中'e'之前的部分作为整数部分
+    } else {
+        integerPart = numberString; // 整个数字都是整数部分
+    }
+
+    return integerPart.length;
+}
+
+function bytesToSize(bytes) {
+    if (parseInt(bytes) === 0) return '0 B';
+    const k = 1024;
+    const sizes = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
+    const i = Math.floor(Math.log(bytes) / Math.log(k));
+    // return (bytes / Math.pow(k, i)) + ' ' + sizes[i];
+    return (bytes / Math.pow(k, i)).toPrecision(3) + ' ' + sizes[i];
+}

+ 6 - 2
app/public/js/payment_list.js

@@ -136,7 +136,11 @@ $(function () {
             toastr.error('未配置好表单角色无法新建表单');
             $('#set-bdjs').modal('show');
         } else {
-            $('#add-catalogue').modal('show');
+            if (emptySign) {
+                $('#add-tips').modal('show');
+            } else {
+                $('#add-catalogue').modal('show');
+            }
         }
     });
 
@@ -295,7 +299,7 @@ $(function () {
                                             class="pull-right
                                                             ${auditConst.statusClass[auditor.status]}">${auditor.status !== auditConst.status.uncheck ? auditConst.statusString[auditor.status] : ''}
                                             ${auditor.status === auditConst.status.checkNo ? user.name : ''}
-                                            ${auditor.status === auditConst.status.checkNoPre ? leftAuditors.find(item => item.sort === auditor.sort-1).name : ''}
+                                            ${auditor.status === auditConst.status.checkNoPre ? auditors.find(item => item.sort === auditor.sort-1).name : ''}
                                         </span>
                                     </p>
                                     <p class="text-muted mb-0">${auditor.role}</p>

+ 5 - 0
app/router.js

@@ -754,6 +754,11 @@ module.exports = app => {
     app.post('/payment/:id/list/:trid/auditors', sessionAuth, paymentTenderCheck, 'paymentController.detailAuditors');
     app.post('/payment/:id/detail/:did/audit/start', sessionAuth, paymentTenderCheck, paymentDetailCheck, 'paymentController.startAudit');
     app.post('/payment/:id/detail/:did/audit/check', sessionAuth, paymentTenderCheck, paymentDetailCheck, 'paymentController.checkAudit');
+    // 附件
+    app.post('/payment/:id/detail/:did/file/upload', sessionAuth, paymentTenderCheck, paymentDetailCheck, 'paymentController.uploadDetailFile');
+    app.post('/payment/:id/detail/:did/file/delete', sessionAuth, paymentTenderCheck, paymentDetailCheck, 'paymentController.deleteDetailFile');
+    app.get('/payment/:id/detail/:did/file/:fid/download', sessionAuth, paymentTenderCheck, paymentDetailCheck, 'paymentController.downloadDetailFile');
+
 
     // 企业微信回调
     app.get('/wx/work/callback/command', 'wechatController.command');

+ 1 - 0
app/service/ledger_history.js

@@ -49,6 +49,7 @@ module.exports = app => {
 
             const billsHis = `${this.ctx.session.sessionProject.id}/${tender.id}/ledger/bills${timestamp}.json`;
             const bills = await this.ctx.service.ledger.getData(tender.id);
+            console.log(bills[0]);
             await this.ctx.hisOss.put(this.ctx.hisOssPath + billsHis, Buffer.from(JSON.stringify(bills), 'utf8'));
 
             const posHis = `${this.ctx.session.sessionProject.id}/${tender.id}/ledger/pos${timestamp}.json`;

+ 35 - 7
app/service/payment_detail.js

@@ -52,6 +52,10 @@ module.exports = app => {
                     throw '上一期未审批通过,请等待上一期审批通过后,再新增';
                 }
 
+                if (this._.findIndex(details, { code }) !== -1) {
+                    throw '编号不能重复';
+                }
+
                 trInfo.rpt_audit = JSON.parse(trInfo.rpt_audit);
                 if (this._.findIndex(trInfo.rpt_audit, { uid: null }) !== -1) {
                     throw '未配置好表单角色,无法新建';
@@ -194,20 +198,31 @@ module.exports = app => {
         }
 
         async haveNotice2Tender(tid, uid) {
-            const sql = 'SELECT count(pd.`id`) as count FROM ?? as pd LEFT JOIN ?? as pda' +
-                ' ON pd.`id` = pda.`td_id` LEFT JOIN ?? as pra ON pd.`id` = pra.`td_id` WHERE pd.`tender_id` = ? AND ((pd.`uid` = ? AND (pd.`status` = ? OR pd.`status` = ?))' +
+            const sql = 'SELECT pd.`id`, pd.`tr_id`, pd.`order` FROM ?? as pd LEFT JOIN ?? as pda' +
+                ' ON pd.`id` = pda.`td_id` LEFT JOIN ?? as pra ON pd.`id` = pra.`td_id` WHERE pd.`tender_id` = ? AND' +
+                ' ((pd.`uid` = ? AND (pd.`status` = ? OR pd.`status` = ?))' +
                 ' OR ((pd.`status` = ? OR pd.`status` = ?) AND pda.aid = ? AND pda.`status` = ?)' +
                 ' OR (pra.`uid` = ? AND pra.`signature_msg` is null AND pd.`status` != ? AND pd.`status` != ?))';
             const params = [this.tableName, this.ctx.service.paymentDetailAudit.tableName, this.ctx.service.paymentRptAudit.tableName, tid,
                 uid, auditConst.status.uncheck, auditConst.status.checkNo,
                 auditConst.status.checking, auditConst.status.checkNoPre, uid, auditConst.status.checking,
                 uid, auditConst.status.uncheck, auditConst.status.checkNo];
-            const result = await this.db.queryOne(sql, params);
-            return result ? result.count : 0;
+            const result = await this.db.query(sql, params);
+            if (result && result.length > 0) {
+                for (const one of this._.uniqBy(result, 'id')) {
+                    const maxOrder = await this.count({
+                        tr_id: one.tr_id,
+                    });
+                    if (one.order === maxOrder) {
+                        return 1;
+                    }
+                }
+            }
+            return 0;
         }
 
         async haveNotice2TenderRpt(tr_id, uid) {
-            const sql = 'SELECT count(pd.`id`) as count FROM ?? as pd LEFT JOIN ?? as pda' +
+            const sql = 'SELECT pd.`id`, pd.`tr_id`, pd.`order` FROM ?? as pd LEFT JOIN ?? as pda' +
                 ' ON pd.`id` = pda.`td_id` LEFT JOIN ?? as pra ON pd.`id` = pra.`td_id` WHERE pd.`tr_id` = ? AND ((pd.`uid` = ? AND (pd.`status` = ? OR pd.`status` = ?))' +
                 ' OR ((pd.`status` = ? OR pd.`status` = ?) AND pda.aid = ? AND pda.`status` = ?)' +
                 ' OR (pra.`uid` = ? AND pra.`signature_msg` is null AND pd.`status` != ? AND pd.`status` != ?))';
@@ -215,8 +230,18 @@ module.exports = app => {
                 uid, auditConst.status.uncheck, auditConst.status.checkNo,
                 auditConst.status.checking, auditConst.status.checkNoPre, uid, auditConst.status.checking,
                 uid, auditConst.status.uncheck, auditConst.status.checkNo];
-            const result = await this.db.queryOne(sql, params);
-            return result ? result.count : 0;
+            const result = await this.db.query(sql, params);
+            if (result && result.length > 0) {
+                for (const one of this._.uniqBy(result, 'id')) {
+                    const maxOrder = await this.count({
+                        tr_id: one.tr_id,
+                    });
+                    if (one.order === maxOrder) {
+                        return 1;
+                    }
+                }
+            }
+            return 0;
         }
 
         async haveDetail2Tender(tid) {
@@ -239,6 +264,9 @@ module.exports = app => {
             if (details && details.length > 0) {
                 const detailInfo = details[0];
                 if (detailInfo.status === auditConst.status.uncheck || detailInfo.status === auditConst.status.checkNo) {
+                    // 判断该人是否在审批流程里,是则移除并调整order
+                    const result2 = await this.ctx.service.paymentDetailAudit.updateAuditByReport(transaction, detailInfo.id, detailInfo.times, uid);
+                    // 更新为上报人
                     return await transaction.update(this.tableName, { id: detailInfo.id, uid });
                 }
             }

+ 69 - 0
app/service/payment_detail_att.js

@@ -0,0 +1,69 @@
+'use strict';
+
+/**
+ *
+ *  附件
+ * @author Ellisran
+ * @date 2019/1/11
+ * @version
+ */
+
+module.exports = app => {
+    class PaymentDetailAtt extends app.BaseService {
+        /**
+         * 构造函数
+         *
+         * @param {Object} ctx - egg全局变量
+         * @return {void}
+         */
+        constructor(ctx) {
+            super(ctx);
+            this.tableName = 'payment_detail_attachment';
+        }
+
+        /**
+         * 添加附件
+         * @param {Object} postData - 表单信息
+         * @param {Object} fileData - 文件信息
+         * @param {int} uid - 上传者id
+         * @return {void}
+         */
+        async save(postData, fileData, uid) {
+            const data = {
+                tender_id: postData.tender_id,
+                tr_id: postData.tr_id,
+                td_id: postData.td_id,
+                uid,
+            };
+            Object.assign(data, fileData);
+            const result = await this.db.insert(this.tableName, data);
+            return result;
+        }
+
+        /**
+         * 获取 详情 所有附件
+         * @param {uuid} td_id - 详情id
+         * @return {Promise<void>}
+         */
+        async getPaymentDetailAttachment(td_id, sort = 'asc') {
+            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.td_id = ? order by id ' + sort;
+            const sqlParam = [this.tableName, this.ctx.service.projectAccount.tableName, td_id];
+            const result = await this.db.query(sql, sqlParam);
+            return result.map(item => {
+                item.orginpath = this.ctx.app.config.fujianOssPath + item.filepath;
+                if (!this.ctx.helper.canPreview(item.fileext)) {
+                    item.filepath = `/payment/${item.tender_id}/detail/${item.td_id}/file/${item.id}/download`;
+                } else {
+                    item.filepath = this.ctx.app.config.fujianOssPath + item.filepath;
+                }
+                return item;
+            });
+        }
+    }
+
+    return PaymentDetailAtt;
+};

+ 72 - 13
app/service/payment_detail_audit.js

@@ -51,16 +51,18 @@ module.exports = app => {
             const auditors = await this.getAuditGroupByList(preDetail.id, preDetail.times);
             const newAuditors = [];
             for (const a of auditors) {
-                const na = {
-                    tender_id: preDetail.tender_id,
-                    tr_id: preDetail.tr_id,
-                    td_id: newDetail.id,
-                    aid: a.aid,
-                    times: newDetail.times,
-                    order: newAuditors.length + 1,
-                    status: auditConst.status.uncheck,
-                };
-                newAuditors.push(na);
+                if (a.aid !== newDetail.uid) {
+                    const na = {
+                        tender_id: preDetail.tender_id,
+                        tr_id: preDetail.tr_id,
+                        td_id: newDetail.id,
+                        aid: a.aid,
+                        times: newDetail.times,
+                        order: newAuditors.length + 1,
+                        status: auditConst.status.uncheck,
+                    };
+                    newAuditors.push(na);
+                }
             }
             const result = await transaction.insert(this.tableName, newAuditors);
             return (result.effectRows = auditors.length);
@@ -348,6 +350,8 @@ module.exports = app => {
                     const report_json = JSON.parse(this.ctx.detail.report_json);
                     const sign_msg = JSON.parse(rptAudit.signature_msg);
                     updateDetailData.report_json = JSON.stringify(await this.ctx.service.paymentDetail.signOneSignatureData(report_json, rptAudit.signature_name, sign_msg));
+                    // 更新签字确定时间
+                    await this.ctx.service.paymentRptAudit.updateSignTime(transaction, rptAudit.id);
                 }
                 await transaction.update(this.ctx.service.paymentDetail.tableName, updateDetailData);
                 // 判断用户是否有权限查看支付审批,没有则自动加入到权限中
@@ -399,6 +403,7 @@ module.exports = app => {
                     const report_json = JSON.parse(this.ctx.detail.report_json);
                     const sign_msg = JSON.parse(rptAudit.signature_msg);
                     updateDetailData.report_json = JSON.stringify(await this.ctx.service.paymentDetail.signOneSignatureData(report_json, rptAudit.signature_name, sign_msg));
+                    await this.ctx.service.paymentRptAudit.updateSignTime(transaction, rptAudit.id);
                 }
 
                 // 无下一审核人表示,审核结束
@@ -423,6 +428,8 @@ module.exports = app => {
 
         async _checkNo(pid, detailId, checkData, times) {
             const time = new Date();
+            const detailInfo = await this.ctx.service.paymentDetail.getDataById(detailId);
+            const trInfo = await this.ctx.service.paymentTenderRpt.getDataById(detailInfo.tr_id);
             // 整理当前流程审核人状态更新
             const audit = await this.getDataByCondition({ td_id: detailId, times, status: auditConst.status.checking });
             if (!audit) {
@@ -431,6 +438,11 @@ module.exports = app => {
             const sql = 'SELECT `tender_id`, `tr_id`, `td_id`, `aid`, `order` FROM ?? WHERE `td_id` = ? and `times` = ? GROUP BY `aid` ORDER BY `id` ASC';
             const sqlParam = [this.tableName, detailId, times];
             const auditors = await this.db.query(sql, sqlParam);
+            // 可能更换了上报人且存在于审批流程中,需要删除
+            const userIndex = this._.findIndex(auditors, { aid: trInfo.uid });
+            if (userIndex !== -1) {
+                auditors.splice(userIndex, 1);
+            }
             let order = 1;
             for (const a of auditors) {
                 a.times = times + 1;
@@ -444,15 +456,21 @@ module.exports = app => {
                 // 清空所有签名数据
                 let report_json = JSON.parse(this.ctx.detail.report_json);
                 report_json = await this.ctx.service.paymentDetail.clearAllSignatureData(report_json);
-                // 同步期信息
-                await transaction.update(this.ctx.service.paymentDetail.tableName, {
+                const detailUpdateData = {
                     id: detailId, status: checkData.checkType,
                     times: times + 1,
                     report_json: JSON.stringify(report_json),
-                });
+                };
+                // 可能存在更换了上报人的情况,需要替换
+                if (detailInfo.uid !== trInfo.uid) {
+                    detailUpdateData.uid = trInfo.uid;
+                }
+                // 同步期信息
+                await transaction.update(this.ctx.service.paymentDetail.tableName, detailUpdateData);
                 // 清空签名
                 await transaction.update(this.ctx.service.paymentRptAudit.tableName, {
                     signature_msg: null,
+                    sign_time: null,
                 }, {
                     where: {
                         td_id: detailId,
@@ -581,6 +599,47 @@ module.exports = app => {
                 throw error;
             }
         }
+
+        /**
+         * 获取审核人需要审核的期列表
+         *
+         * @param auditorId
+         * @return {Promise<*>}
+         */
+        async getAuditPayment(auditorId) {
+            const sql =
+                'SELECT pda.`aid`, pda.`times`, pda.`order`, pda.`begin_time`, pda.`end_time`, pda.`tender_id`, pda.`tr_id`, pda.`td_id`,' +
+                '    pd.`code` As `scode`, pd.`order` As `sorder`, pd.`status` As `sstatus`,' +
+                '    pr.`rpt_name` As `rpt_name`,' +
+                '    t.`name`, t.`pid`, t.`uid` ' +
+                '  FROM ?? AS pda ' +
+                '    Left Join ?? AS pd On pda.`td_id` = pd.`id` ' +
+                '    Left Join ?? AS pr On pda.`tr_id` = pr.`id` ' +
+                '    Left Join ?? As t ON pda.`tender_id` = t.`id`' +
+                '  WHERE ((pda.`aid` = ? and pda.`status` = ?) OR (pd.`uid` = ? and pda.`status` = ? and pd.`status` = ? and pda.`times` = (pd.`times`-1)))' +
+                '  ORDER BY pda.`begin_time` DESC';
+            const sqlParam = [
+                this.tableName,
+                this.ctx.service.paymentDetail.tableName,
+                this.ctx.service.paymentTenderRpt.tableName,
+                this.ctx.service.paymentTender.tableName,
+                auditorId,
+                auditConst.status.checking,
+                auditorId,
+                auditConst.status.checkNo,
+                auditConst.status.checkNo,
+            ];
+            return await this.db.query(sql, sqlParam);
+        }
+
+        async updateAuditByReport(transaction, td_id, times, uid) {
+            const auditor = await this.getDataByCondition({ td_id, times, aid: uid });
+            if (auditor) {
+                // 更新order
+                await this._syncOrderByDelete(transaction, td_id, auditor.order, times);
+                return await transaction.delete(this.tableName, { id: auditor.id });
+            }
+        }
     }
 
     return PaymentDetailAudit;

+ 12 - 8
app/service/payment_permission_audit.js

@@ -23,15 +23,19 @@ module.exports = app => {
         }
 
         async saveAudits(pid, accountList, transaction = null) {
+            // 判断是否已存在该用户,存在则不插入
+            const pauditList = await this.getAllDataByCondition({ where: { pid } });
             const pushData = [];
-            for (const a of accountList) {
-                const data = {
-                    pid,
-                    groupid: a.account_group,
-                    uid: a.id,
-                    create_time: new Date(),
-                };
-                pushData.push(data);
+            for (const a of this._.uniqBy(accountList, 'id')) {
+                if (this._.findIndex(pauditList, { uid: a.id }) === -1) {
+                    const data = {
+                        pid,
+                        groupid: a.account_group,
+                        uid: a.id,
+                        create_time: new Date(),
+                    };
+                    pushData.push(data);
+                }
             }
             if (pushData.length > 0) {
                 return transaction ? await transaction.insert(this.tableName, pushData) : await this.db.insert(this.tableName, pushData);

+ 15 - 2
app/service/payment_rpt_audit.js

@@ -32,7 +32,10 @@ module.exports = app => {
                 if (!rptAudit) {
                     throw '当前人不存在报表角色中';
                 }
-                if (this.ctx.detail.uid !== uid && this._.findIndex(this.ctx.detail.auditors, { aid: uid }) === -1) {
+                let sign_time = null;
+                if ((this.ctx.detail.status !== auditConst.status.uncheck && this.ctx.detail.status !== auditConst.status.checkNo && this.ctx.detail.uid === uid) ||
+                    (this._.findIndex(this.ctx.detail.auditors, { aid: uid }) === -1 && this.ctx.detail.uid !== uid) ||
+                    (this._.findIndex(this.ctx.detail.auditors, { aid: uid }) !== -1 && this.ctx.detail.curAuditor && this.ctx.detail.curAuditor.aid !== uid)) {
                     let report_json = JSON.parse(this.ctx.detail.report_json);
                     report_json = await this.ctx.service.paymentDetail.signOneSignatureData(report_json, rptAudit.signature_name, signature_msg);
                     // 同步期信息
@@ -40,8 +43,9 @@ module.exports = app => {
                         id: this.ctx.detail.id,
                         report_json: JSON.stringify(report_json),
                     });
+                    sign_time = new Date();
                 }
-                await transaction.update(this.tableName, { sign_time: new Date(), signature_msg: JSON.stringify(signature_msg) }, { where: { td_id, uid } });
+                await transaction.update(this.tableName, { sign_time, signature_msg: JSON.stringify(signature_msg) }, { where: { td_id, uid } });
                 await transaction.commit();
                 return true;
             } catch (error) {
@@ -50,6 +54,10 @@ module.exports = app => {
             }
         }
 
+        async updateSignTime(transaction, id) {
+            return await transaction.update(this.tableName, { id, sign_time: new Date() });
+        }
+
         async clearSignatureMsg(transaction, td_id, uid) {
             return await transaction.update(this.tableName, { sign_time: null, signature_msg: null }, { where: { td_id, uid } });
         }
@@ -82,6 +90,11 @@ module.exports = app => {
             }
             return list;
         }
+
+        async haveEmptySign(td_id) {
+            const count = await this.count({ td_id, signature_msg: null });
+            return count > 0;
+        }
     }
 
     return PaymentRptAudit;

+ 5 - 1
app/service/report_memory.js

@@ -478,6 +478,7 @@ module.exports = app => {
                 const billsData = this.ctx.stage.ledgerHis
                     ? await this.ctx.helper.loadLedgerDataFromOss(this.ctx.stage.ledgerHis.bills_file)
                     : await this.ctx.service.ledger.getData(this.ctx.tender.id);
+                const memoData = this.ctx.stage.ledgerHis ? await this.ctx.service.ledger.getAllDataByCondition({ columns: ['id', 'memo'], where: { tender_id: this.ctx.tender.id } }) : [];
                 if (this._checkFieldsExist(fields, billsFields.stageDgn)) {
                     const dgnData = await this.ctx.service.stageBillsDgn.getDgnData(this.ctx.tender.id);
                     for (const d of dgnData) {
@@ -503,6 +504,7 @@ module.exports = app => {
                     ? await this._loadStageBillsYear(this.ctx.tender, this.ctx.stage.s_time.split('-')[0])
                     : [];
                 this.ctx.helper.assignRelaData(billsData, [
+                    { data: memoData, fields: ['memo'], prefix: '', relaId: 'id' },
                     { data: curStage, fields: ['contract_qty', 'contract_tp', 'contract_expr', 'qc_qty', 'qc_tp', 'qc_minus_qty', 'postil'], prefix: '', relaId: 'lid' },
                     { data: preStage, fields: ['contract_qty', 'contract_tp', 'qc_qty', 'qc_tp', 'qc_minus_qty'], prefix: 'pre_', relaId: 'lid' },
                     { data: bpcStage, fields: ['contract_pc_tp', 'qc_pc_tp', 'pc_tp', 'org_price'], prefix: '', relaId: 'lid' },
@@ -634,6 +636,7 @@ module.exports = app => {
                 const billsData = this.ctx.stage.ledgerHis
                     ? await this.ctx.helper.loadLedgerDataFromOss(this.ctx.stage.ledgerHis.bills_file)
                     : await this.ctx.service.ledger.getData(this.ctx.tender.id);
+                const memoData = this.ctx.stage.ledgerHis ? await this.ctx.service.ledger.getAllDataByCondition({ columns: ['id', 'memo'], where: { tender_id: this.ctx.tender.id } }) : [];
                 const allStageBills = await this.ctx.service.stageBills.getAllDataByCondition({where: {sid: sid}});
 
                 const stageBillsIndex = {}, timesLen = 100;
@@ -667,7 +670,8 @@ module.exports = app => {
                     : [];
 
                 this.ctx.helper.assignRelaData(billsData, [
-                    {data: preStage, fields: ['contract_qty', 'contract_tp', 'qc_qty', 'qc_tp', 'qc_minus_qty'], prefix: 'pre_', relaId: 'lid'},
+                    { data: memoData, fields: ['memo'], prefix: '', relaId: 'id' },
+                    { data: preStage, fields: ['contract_qty', 'contract_tp', 'qc_qty', 'qc_tp', 'qc_minus_qty'], prefix: 'pre_', relaId: 'lid'},
                     { data: bpcStage, fields: ['contract_pc_tp', 'qc_pc_tp', 'pc_tp', 'org_price'], prefix: '', relaId: 'lid' },
                     { data: endBpcStage, fields: ['end_contract_pc_tp', 'end_qc_pc_tp', 'end_pc_tp', 'org_price_his'], prefix: '', relaId: 'lid' },
                 ]);

+ 23 - 1
app/view/dashboard/index.ejs

@@ -66,12 +66,15 @@
                                     <% if (auditMaterial.length !== 0) { %>
                                     <option value="1">材料调差(<%- auditMaterial.length %>)</option>
                                     <% } %>
+                                    <% if (auditPayments.length !== 0) { %>
+                                        <option value="10">支付审批(<%- auditPayments.length %>)</option>
+                                    <% } %>
                                 </select>
                             </div>
                         </div>
                         <div class="card-body p-0">
                             <div class="contant-height-one">
-                                <% if (auditTenders.length !== 0 || auditRevise.length !== 0 || auditStages.length !== 0 || auditChanges.length !== 0 || auditMaterial.length !== 0 || auditAdvance.length !== 0 || auditChangeProject.length !== 0 || auditChangeApply.length !== 0 || auditChangePlan.length !== 0) { %>
+                                <% if (auditTenders.length !== 0 || auditRevise.length !== 0 || auditStages.length !== 0 || auditChanges.length !== 0 || auditMaterial.length !== 0 || auditAdvance.length !== 0 || auditChangeProject.length !== 0 || auditChangeApply.length !== 0 || auditChangePlan.length !== 0 || auditPayments.length !== 0) { %>
                                 <style>
                                     #doing-list td {
                                         word-wrap:break-word;
@@ -226,6 +229,25 @@
                                                 </tr>
                                             <% } %>
                                         <% } %>
+                                        <% for (const audit of auditPayments) { %>
+                                            <% if (audit.sstatus !== acStage.status.checkNo) { %>
+                                                <tr data-type="10">
+                                                    <td><span class="bg-new-payment text-new-payment badge text-width">支付审批</span></td>
+                                                    <td><a href="/payment/<%- audit.tender_id %>/list/<%- audit.tr_id %>"><%- audit.name %> <%- audit.rpt_name %></a> <a href="/payment/<%- audit.tender_id %>/detail/<%- audit.td_id %>"><%- audit.scode %></a></td>
+                                                    <td>第<%- audit.sorder %>期</td>
+                                                    <td><%- ctx.moment(audit.begin_time).format('YYYY/MM/DD HH:mm') %></td>
+                                                    <td><a href="/payment/<%- audit.tender_id %>/detail/<%- audit.td_id %>" class="btn btn-outline-primary btn-sm btn-table" role="button"><% if (audit.sstatus === acStage.status.checkNoPre) { %>重新<% } %>审 批</a></td>
+                                                </tr>
+                                            <% } else { %>
+                                                <tr data-type="10">
+                                                    <td><span class="bg-new-payment text-new-payment badge text-width">支付审批</span></td>
+                                                    <td><a href="/payment/<%- audit.tender_id %>/list"><%- audit.name %> <%- audit.rpt_name %></a> <a href="/payment/<%- audit.tender_id %>/detail/<%- audit.td_id %>"><%- audit.scode %></a></td>
+                                                    <td>第<%- audit.sorder %>期</td>
+                                                    <td><%- ctx.moment(audit.end_time).format('YYYY/MM/DD HH:mm') %></td>
+                                                    <td><a href="/payment/<%- audit.tender_id %>/detail/<%- audit.td_id %>" class="btn btn-outline-warning btn-sm btn-table text-warning" role="button">重新上报</a></td>
+                                                </tr>
+                                            <% } %>
+                                        <% } %>
                                     </tbody>
                                 </table>
                                 <% } else { %>

+ 88 - 16
app/view/payment/detail.ejs

@@ -31,27 +31,90 @@
     <div class="content-wrap">
         <div class="c-body">
             <div class="sjs-height-0">
-                <div class="row m-0 mt-3">
-                    <div id="rpt-form" class="col-5 mr-3">
-                        <% for (const c of content) { %>
-                        <div>
-                            <h5><%- c.title %>内容</h5>
-                            <% for (const item of c.items) { %>
-                            <div class="form-group">
-                                <label><%- item.label %></label>
-                                <% if (item.type === 'textarea') { %>
-                                    <textarea <% if (ctx.detail.readOnly) { %>readonly<% } %> class="form-control form-control-sm" data-index="<%- item.index %>"><%- item.value %></textarea>
-                                <% } else { %>
-                                    <input <% if (ctx.detail.readOnly) { %>readonly<% } %> type="<%- item.type %>" value="<%- item.type === 'number' && ctx.helper._.indexOf(item.value, '元') !== -1 ? parseFloat(item.value) : item.value %>"  data-index="<%- item.index %>" class="form-control form-control-sm" placeholder="请输入">
-                                <% } %>
+                <div class="row m-0 my-2">
+                    <div class="col-5">
+                        <div class="sjs-bar-1">
+                            <ul class="nav nav-tabs" id="myTab" role="tablist">
+                                <li class="nav-item">
+                                    <a class="nav-link active" id="content-tab" data-tab="form" data-toggle="tab" href="#content" role="tab" aria-controls="content" aria-selected="true">表单内容</a>
+                                </li>
+                                <li class="nav-item">
+                                    <a class="nav-link" id="annex-tab" data-tab="fujian" data-toggle="tab" href="#annex" role="tab" aria-controls="annex" aria-selected="false">附件</a>
+                                </li>
+                                <li class="nav-item ml-auto pt-1" id="fujian_btn" style="display:none;">
+                                    <!--所有附件 翻页-->
+                                    <button  data-toggle="modal" class="btn btn-sm btn-primary" id="bach-download"><i class="fa fa-download "></i> 批量下载</button>
+                                    <!-- <a href="javascript: void(0);" data-toggle="modal" class="btn btn-sm btn-primary" id="bach-download"><i class="fa fa-download "></i> 批量下载</a> -->
+                                    <a href="javascript:void(0);" class="page-select ml-3" content="pre"><i class="fa fa-chevron-left"></i></a> <span id="currentPage">1</span>/<span id="totalPage">10</span> <a href="javascript:void(0);" class="page-select mr-3" content="next"><i class="fa fa-chevron-right"></i></a>
+                                    <% if (ctx.detail.filePermission) { %>
+                                        <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);" id="zipDown" download style="display: none;"></a>
+                                </li>
+                            </ul>
+                        </div>
+                        <div class="tab-content mt-2" id="myTabContent">
+                            <div class="tab-pane fade show active" id="content" role="tabpanel" aria-labelledby="content-tab">
+                                <div id="rpt-form">
+                                    <% for (const c of content) { %>
+                                    <div>
+                                        <h5><%- c.title %>内容</h5>
+                                        <% for (const item of c.items) { %>
+                                        <div class="form-group">
+                                            <label><%- item.label %></label>
+                                            <% if (item.type === 'textarea') { %>
+                                                <textarea <% if (ctx.detail.readOnly) { %>readonly<% } %> class="form-control form-control-sm" data-index="<%- item.index %>"><%- item.value %></textarea>
+                                            <% } else { %>
+                                                <input <% if (ctx.detail.readOnly) { %>readonly<% } %> type="<%- item.type %>" value="<%- item.type === 'number' && ctx.helper._.indexOf(item.value, '元') !== -1 ? parseFloat(item.value) : item.value %>" <% if (item.type === 'number') { %>step="0.000001"<% } %>  data-index="<%- item.index %>" class="form-control form-control-sm" placeholder="请输入">
+                                            <% } %>
+                                        </div>
+                                        <% } %>
+                                    </div>
+                                    <% } %>
+                                </div>
+                            </div>
+                            <!-- 附件 -->
+                            <div class="tab-pane fade" id="annex"  role="tabpanel" aria-labelledby="annex-tab">
+                                <div class="sjs-sh-1" style="overflow-y: auto;">
+                                    <div class="p-2">
+                                        <table class="table table-bordered" style="word-break:break-all; table-layout: fixed">
+                                            <thead>
+                                            <tr>
+                                                <td width="25" style="background-color: #e9ecef;"><input type="checkbox" id="check-all-file" ></td>
+                                                <th width="40">序号</th>
+                                                <th>名称</th>
+                                                <th width="130">上传时间/大小</th>
+                                                <th width="60">操作</th>
+                                            </tr>
+                                            </thead>
+                                            <tbody id="attList">
+                                            <% 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><%- moment(att.upload_time).format("YYYY-MM-DD HH:mm:ss") %><br><%- ctx.helper.bytesToSize(att.filesize) %></td>
+                                                        <td>
+                                                            <a href="/payment/<%- ctx.tender.id %>/detail/<%- ctx.detail.id %>/file/<%- att.id %>/download" class="mr-2" title="下载"><span class="fa fa-download text-primary"></span></a>
+                                                            <% if (att.uid === ctx.session.sessionUser.accountId && (ctx.detail.status === auditConst.status.checked ? Boolean(att.extra_upload) : true )) { %>
+                                                                <a href="javascript:void(0)" class="mr-2 delete-file" data-attid="<%- att.id %>" title="删除附件"><span class="fa fa-trash text-danger"></span></a>
+                                                            <% } %>
+                                                        </td>
+                                                    </tr>
+                                                <% } %>
+                                            <% } %>
+                                            </tbody>
+                                        </table>
+                                        <a href="" id="file-upload" target="_blank" style="display: none;"></a>
+                                    </div>
+                                </div>
                             </div>
-                            <% } %>
                         </div>
-                        <% } %>
                     </div>
                     <div class="col-6 ml-5">
                         <div class="d-flex flex-row">
-                            <a href="javascript: void(0);" onclick="auditRptPrintHelper.showPage()" class="mr-2" >刷新</a>
+                            <!--<a href="javascript: void(0);" onclick="auditRptPrintHelper.showPage()" class="mr-2" >刷新</a>-->
                             <a href="javascript: void(0);" class="mr-2"  onclick="auditRptPrintHelper.directPDF()">导出pdf</a>
                             <a href="javascript: void(0);" onclick="auditRptPrintHelper.directPrint()">打印</a>
                         </div>
@@ -66,8 +129,13 @@
 </div>
 
 <script type="text/javascript">
+    const accountId = parseInt('<%- ctx.session.sessionUser.accountId %>');
     const tenderId = parseInt('<%- ctx.tender.id %>');
     const detailId = parseInt('<%- ctx.detail.id %>');
+    const detailStatus = parseInt('<%- ctx.detail.status %>');
+    const trName =  JSON.parse(unescape('<%- escape(JSON.stringify(trInfo.rpt_name)) %>'));
+    const detailName =  JSON.parse(unescape('<%- escape(JSON.stringify(ctx.detail.code)) %>'));
+    const auditConst = JSON.parse(unescape('<%- escape(JSON.stringify(auditConst)) %>'));
     const rptAudit = JSON.parse(unescape('<%- escape(JSON.stringify(rptAudit)) %>'));
     <% if (rptAudit &&
             ((ctx.detail.status !== auditConst.status.checkNo && ctx.detail.status !== auditConst.status.checked) ||
@@ -78,6 +146,10 @@
     <% } %>
     let tesRpttData = JSON.parse(unescape('<%- escape(JSON.stringify(report_json)) %>'));
     console.log(tesRpttData);
+    const whiteList = JSON.parse('<%- JSON.stringify(whiteList) %>');
+    let attData = JSON.parse(unescape('<%- escape(JSON.stringify(attList)) %>'));
+    const uidList = JSON.parse(unescape('<%- escape(JSON.stringify(uidList)) %>'));
+    let currPageFileData = [];
     const SCREEN_DPI = [];
     const PAGE_SHOW = {closeWaterMark: 1};
     const current_stage_status = -1;

+ 25 - 2
app/view/payment/detail_modal.ejs

@@ -32,7 +32,9 @@
                                     <div class="form-check form-check-inline">
                                         <input class="form-check-input" type="checkbox" id="stamp_path" value="<%- currentStamp %>" <% if (rptAudit.signature_msg.stamp_path !== null) { %>checked<% } %>>
                                         <label class="form-check-label" for="stamp_path">个人章</label>
+                                        <% if (stampPathList.length > 1) { %>
                                         <a class="pl-2" href="#chose-private-stamp-path" data-toggle="modal" data-target="#chose-private-stamp-path">选择个人章</a>
+                                        <% } %>
                                     </div>
                                     <% } %>
                                 </div>
@@ -124,8 +126,8 @@
                         <td><%- index+1 %></td>
                         <td><%- ra.signature_name %></td>
                         <td><%- ra.user_name %></td>
-                        <td><% if (ra.signature_msg && ra.signature_msg.sign_path !== null ) { %><i class="fa fa-check text-success"></i><% } %></td>
-                        <td><% if (ra.signature_msg && (ra.signature_msg.stamp_path !== null || ra.signature_msg.company_stamp !== null)) { %><i class="fa fa-check text-success"></i><% } %></td>
+                        <td><% if (ra.signature_msg && ra.signature_msg.sign_path !== null && (ra.sign_time || ra.uid === ctx.session.sessionUser.accountId)) { %><i class="fa fa-check text-success"></i><% } %></td>
+                        <td><% if (ra.signature_msg && (ra.signature_msg.stamp_path !== null || ra.signature_msg.company_stamp !== null) && (ra.sign_time || ra.uid === ctx.session.sessionUser.accountId)) { %><i class="fa fa-check text-success"></i><% } %></td>
                     </tr>
                     <% } %>
                     </tbody>
@@ -137,4 +139,25 @@
         </div>
     </div>
 </div>
+<!--添加附件-->
+<div class="modal fade" tabindex="-1" role="dialog" aria-hidden="true" id="addfujian">
+    <div class="modal-dialog" role="document">
+        <div class="modal-content">
+            <div class="modal-header">
+                <h5 class="modal-title" id="myModalLabel">上传附件</h5>
+                <button type="button" class="close" data-dismiss="modal" aria-label="Close">
+                    <span aria-hidden="true">&times;</span>
+                </button>
+            </div>
+            <div class="modal-body">
+                <p>单个文件大小限制:30MB,支持office等文档格式、图片格式、压缩包格式</p>
+                <p><input value="选择文件" type="file" id="upload-file" multiple /></p>
+            </div>
+            <div class="modal-footer">
+                <button type="button" class="btn btn-secondary btn-sm" data-dismiss="modal">取消</button>
+                <button type="button" class="btn btn-primary btn-sm" id="upload-file-btn">添加</button>
+            </div>
+        </div>
+    </div>
+</div>
 <% include ./audit_modal.ejs %>

+ 1 - 0
app/view/payment/list.ejs

@@ -93,5 +93,6 @@
     let rpt_audit = JSON.parse(unescape('<%- escape(JSON.stringify(trInfo.rpt_audit)) %>'));
     let old_rpt_audit = _.cloneDeep(rpt_audit);
     const is_first = parseInt('<%- trInfo.is_first %>');
+    const emptySign = <%- trDetailList && trDetailList[0] ? trDetailList[0].emptySign : false %>
     console.log(rpt_audit);
 </script>

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

@@ -64,6 +64,22 @@
 </div>
 
 <!-- 弹窗新建目录 -->
+<div class="modal fade" id="add-tips" data-backdrop="static">
+    <div class="modal-dialog" role="document">
+        <div class="modal-content">
+            <div class="modal-header">
+                <h5 class="modal-title">新建审批</h5>
+            </div>
+            <div class="modal-body">
+                <h5>第<%= trDetailList && trDetailList[0] ? trDetailList[0].order : 1 %>期存在人员报表未签署,新增期后将无法进行签署,请确认是否新增?</h5>
+            </div>
+            <div class="modal-footer">
+                <button type="button" class="btn btn-sm btn-secondary" data-dismiss="modal">取消</button>
+                <a class="btn btn-sm btn-primary" data-dismiss="modal" href="#add-catalogue" data-toggle="modal" data-target="#add-catalogue">确定</a>
+            </div>
+        </div>
+    </div>
+</div>
 <div class="modal fade" id="add-catalogue" data-backdrop="static">
     <div class="modal-dialog" role="document">
         <div class="modal-content">

+ 5 - 1
config/web.js

@@ -1159,8 +1159,12 @@ const JsFiles = {
                 mergeFile: 'payment_list',
             },
             detail: {
-                files: ['/public/js/datepicker/datepicker.min.js', '/public/js/datepicker/datepicker.zh.js'],
+                files: ['/public/js/datepicker/datepicker.min.js', '/public/js/datepicker/datepicker.zh.js', '/public/js/moment/moment.min.js',
+                    '/public/js/axios/axios.min.js', '/public/js/js-xlsx/jszip.min.js', '/public/js/file-saver/FileSaver.min.js',
+                    '/public/js/decimal.min.js'],
                 mergeFiles: [
+                    '/public/js/zh_calc.js',
+                    '/public/js/zip_oss.js',
                     '/public/js/payment_detail.js',
                     '/public/js/payment_detail_audit.js',
                 ],

+ 196 - 2
sql/update.sql

@@ -8,6 +8,202 @@ ADD COLUMN `self_category_level`  varchar(255) NOT NULL DEFAULT '' AFTER `invali
 ALTER TABLE `zh_project_account`
 ADD COLUMN `show_revise_invalid`  tinyint(4) UNSIGNED NOT NULL DEFAULT 0 COMMENT '是否显示,台账修订作废' AFTER `self_category_level`;
 
+CREATE TABLE `zh_sub_project` (
+  `id` varchar(36) COLLATE utf8_unicode_ci NOT NULL COMMENT 'uuid',
+  `project_id` int(11) unsigned NOT NULL COMMENT '项目id',
+  `tree_pid` varchar(36) COLLATE utf8_unicode_ci NOT NULL COMMENT 'uuid',
+  `tree_order` int(11) unsigned NOT NULL,
+  `tree_level` int(11) unsigned NOT NULL,
+  `name` varchar(255) COLLATE utf8_unicode_ci NOT NULL DEFAULT '' COMMENT '名称',
+  `management` varchar(50) COLLATE utf8_unicode_ci NOT NULL DEFAULT '' COMMENT '管理单位',
+  `user_id` int(11) unsigned NOT NULL COMMENT '用户id',
+  `rela_tender` varchar(1000) CHARACTER SET ascii NOT NULL DEFAULT '' COMMENT '关联标段id('',''分隔)',
+  `create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+  `update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '最后更新时间',
+  `is_folder` tinyint(4) unsigned NOT NULL DEFAULT '0' COMMENT '是否为文件夹',
+  `is_delete` tinyint(4) unsigned NOT NULL DEFAULT '0',
+  `budget_id` int(11) unsigned NOT NULL DEFAULT '0' COMMENT 'budget_id(zh_budget)',
+  `std_id` int(11) NOT NULL DEFAULT '0' COMMENT '概算标准id',
+  `std_name` varchar(50) COLLATE utf8_unicode_ci NOT NULL DEFAULT '' COMMENT '概算标准名称',
+  PRIMARY KEY (`id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;
+
+CREATE TABLE `zh_sub_project_permission` (
+  `id` varchar(36) COLLATE utf8_unicode_ci NOT NULL,
+  `spid` varchar(36) COLLATE utf8_unicode_ci NOT NULL COMMENT 'sub_project_id(zh_sub_project)',
+  `pid` int(11) unsigned NOT NULL COMMENT 'project_id(zh_project)',
+  `uid` int(11) unsigned NOT NULL COMMENT 'user_id(zh_project_acccount)',
+  `manage_permission` varchar(255) COLLATE utf8_unicode_ci NOT NULL DEFAULT '' COMMENT '管理,权限id列表('',''分隔)',
+  `budget_permission` varchar(255) COLLATE utf8_unicode_ci NOT NULL DEFAULT '' COMMENT '动态投资,权限id列表('',''分隔)',
+  `file_permission` varchar(255) COLLATE utf8_unicode_ci NOT NULL DEFAULT '' COMMENT '资料归集,权限id列表('',''分隔)',
+  `filing_type` varchar(255) COLLATE utf8_unicode_ci NOT NULL DEFAULT '' COMMENT '资料归集,固定分类授权',
+  `create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '添加时间',
+  `update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '最后修改时间',
+  PRIMARY KEY (`id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;
+
+CREATE TABLE `zh_filing` (
+  `id` varchar(36) COLLATE utf8_unicode_ci NOT NULL COMMENT 'uuid',
+  `spid` varchar(36) COLLATE utf8_unicode_ci NOT NULL DEFAULT '' COMMENT 'sub_project_id(zh_sub_project)',
+  `tree_pid` varchar(36) COLLATE utf8_unicode_ci NOT NULL COMMENT 'uuid',
+  `tree_order` int(11) unsigned NOT NULL,
+  `tree_level` int(11) unsigned NOT NULL,
+  `name` varchar(255) COLLATE utf8_unicode_ci NOT NULL DEFAULT '' COMMENT '名称',
+  `filing_type` tinyint(4) unsigned NOT NULL DEFAULT '0' COMMENT '固定分类(具体见代码app/service/filing.js->filingType)',
+  `add_user_id` int(11) NOT NULL DEFAULT '0' COMMENT '添加人',
+  `create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+  `update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '最后更新时间',
+  `is_fixed` tinyint(4) unsigned NOT NULL DEFAULT '0' COMMENT '是否为固定项',
+  `is_deleted` tinyint(4) unsigned NOT NULL DEFAULT '0' COMMENT '是否已删除',
+  `file_count` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '文件数量',
+  PRIMARY KEY (`id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci COMMENT='资料归集,资料分类';
+
+CREATE TABLE `zh_file` (
+  `id` varchar(36) COLLATE utf8_unicode_ci NOT NULL DEFAULT '' COMMENT 'uuid',
+  `spid` varchar(36) COLLATE utf8_unicode_ci NOT NULL DEFAULT '' COMMENT 'sub_project_id(zh_sub_project.id)',
+  `filing_id` varchar(36) COLLATE utf8_unicode_ci NOT NULL DEFAULT '' COMMENT '分类id(zh_filing.id)',
+  `filing_type` tinyint(4) unsigned NOT NULL COMMENT '固定分类类型',
+  `user_id` int(11) unsigned NOT NULL COMMENT '用户id(zh_project_account.id)',
+  `user_name` varchar(50) COLLATE utf8_unicode_ci NOT NULL DEFAULT '' COMMENT '用户名(缓存)',
+  `user_company` varchar(255) COLLATE utf8_unicode_ci NOT NULL COMMENT '公司(缓存)',
+  `user_role` varchar(50) COLLATE utf8_unicode_ci NOT NULL COMMENT '角色(缓存)',
+  `filename` varchar(255) COLLATE utf8_unicode_ci NOT NULL COMMENT '文件名',
+  `fileext` varchar(50) COLLATE utf8_unicode_ci NOT NULL COMMENT '文件类型',
+  `filesize` int(11) unsigned NOT NULL COMMENT '文件大小',
+  `filepath` varchar(255) COLLATE utf8_unicode_ci NOT NULL COMMENT '文件路径(oss路径)',
+  `create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+  `update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '最后更新时间',
+  `is_deleted` tinyint(4) unsigned NOT NULL DEFAULT '0' COMMENT '是否删除',
+  `is_rela` tinyint(4) unsigned NOT NULL DEFAULT '0' COMMENT '是否从标段内引用',
+  `rela_info` varchar(50) COLLATE utf8_unicode_ci NOT NULL DEFAULT '' COMMENT '引用信息(json),非引用文件时为空字符串',
+  PRIMARY KEY (`id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;
+
+ALTER TABLE `zh_project` ADD `payment_setting` JSON NULL DEFAULT NULL COMMENT '支付审批模块设置' AFTER `fun_set`;
+
+CREATE TABLE `zh_payment_attachment`  (
+  `id` bigint(20) UNSIGNED NOT NULL AUTO_INCREMENT,
+  `tid` int(11) UNSIGNED NOT NULL COMMENT '标段id',
+  `sid` int(11) UNSIGNED NOT NULL COMMENT '期id',
+  `pid` int(11) NOT NULL COMMENT '合同支付id',
+  `uid` int(11) UNSIGNED NOT NULL COMMENT '用户id',
+  `filename` varchar(255) CHARACTER SET utf16 COLLATE utf16_general_ci NOT NULL COMMENT '文件名',
+  `fileext` varchar(10) CHARACTER SET utf8 COLLATE utf8_unicode_ci NOT NULL COMMENT '文件后缀',
+  `filesize` varchar(30) CHARACTER SET utf8 COLLATE utf8_unicode_ci NOT NULL COMMENT '文件大小',
+  `filepath` varchar(500) CHARACTER SET utf8 COLLATE utf8_unicode_ci NOT NULL COMMENT '文件存储路径',
+  `in_time` varchar(20) CHARACTER SET utf8 COLLATE utf8_unicode_ci NOT NULL DEFAULT '',
+  `renew` tinyint(1) UNSIGNED NOT NULL DEFAULT 0 COMMENT '是否审批通过后上传',
+  `username` varchar(10) CHARACTER SET utf8 COLLATE utf8_unicode_ci NOT NULL DEFAULT '' COMMENT '用户名',
+  PRIMARY KEY (`id`) USING BTREE
+) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8 COLLATE = utf8_unicode_ci COMMENT = '支付审批详情附件表';
+
+CREATE TABLE `zh_payment_detail`  (
+  `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '自增',
+  `tender_id` int(11) NOT NULL COMMENT '标段id',
+  `tr_id` int(11) NOT NULL COMMENT '标段报表id(tender_rpt)',
+  `type` tinyint(2) NOT NULL DEFAULT 0 COMMENT '详情类型,0为报表审批表单,1为安全生产费',
+  `uid` int(11) NOT NULL COMMENT '创建者id',
+  `status` tinyint(4) NOT NULL DEFAULT 1 COMMENT '审批状态',
+  `order` int(11) NOT NULL COMMENT '期数',
+  `times` tinyint(4) NOT NULL COMMENT '审批次数',
+  `s_time` varchar(255) CHARACTER SET utf8 COLLATE utf8_unicode_ci NULL DEFAULT NULL COMMENT '日期',
+  `code` varchar(255) CHARACTER SET utf8 COLLATE utf8_unicode_ci NOT NULL COMMENT '编号',
+  `report_json` json NULL COMMENT '报表json',
+  `in_time` datetime NOT NULL COMMENT '创建时间',
+  PRIMARY KEY (`id`) USING BTREE
+) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8 COLLATE = utf8_unicode_ci COMMENT = '支付审批详情表';
+
+CREATE TABLE `zh_payment_detail_audit`  (
+  `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键',
+  `tender_id` int(11) NOT NULL COMMENT '标段id',
+  `tr_id` int(11) NOT NULL COMMENT '标段表单关联id',
+  `td_id` int(11) NOT NULL COMMENT '标段报表详情id',
+  `aid` int(11) NOT NULL COMMENT '审批人id',
+  `order` int(11) NOT NULL COMMENT '审批顺序',
+  `times` int(11) NOT NULL COMMENT '审批次数',
+  `status` tinyint(1) NOT NULL COMMENT '审批状态',
+  `begin_time` datetime NULL DEFAULT NULL COMMENT '开始审批时间',
+  `end_time` datetime NULL DEFAULT NULL COMMENT '结束审批时间',
+  `opinion` varchar(1000) CHARACTER SET utf8 COLLATE utf8_unicode_ci NULL DEFAULT NULL COMMENT '审批意见',
+  PRIMARY KEY (`id`) USING BTREE
+) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8 COLLATE = utf8_unicode_ci COMMENT = '材料调差审批表';
+
+CREATE TABLE `zh_payment_folder`  (
+  `id` int(11) NOT NULL AUTO_INCREMENT,
+  `pid` int(11) NOT NULL COMMENT '项目id',
+  `uid` int(11) NOT NULL COMMENT '创建人id',
+  `name` varchar(255) CHARACTER SET utf8 COLLATE utf8_unicode_ci NULL DEFAULT NULL COMMENT '名称',
+  `parent_id` int(11) NOT NULL DEFAULT 0 COMMENT '父节点,默认为0',
+  `parent_path` varchar(1000) CHARACTER SET utf8 COLLATE utf8_unicode_ci NULL DEFAULT '' COMMENT '层级定位辅助字段,父节点id集合',
+  `level` int(11) NOT NULL COMMENT '层级',
+  `order` int(11) NOT NULL DEFAULT 0 COMMENT '同级排序',
+  `is_leaf` tinyint(1) NOT NULL DEFAULT 1 COMMENT '是否为子节点',
+  `had_tender` tinyint(1) NOT NULL DEFAULT 0 COMMENT '是否存在标段或子目录下存在标段',
+  `in_time` datetime NOT NULL COMMENT '入库时间',
+  PRIMARY KEY (`id`) USING BTREE
+) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8 COLLATE = utf8_unicode_ci COMMENT = '支付审批目录表';
+
+CREATE TABLE `zh_payment_permission_audit`  (
+  `id` int(11) NOT NULL AUTO_INCREMENT,
+  `pid` int(11) NOT NULL COMMENT '项目id',
+  `groupid` int(11) NULL DEFAULT NULL COMMENT '用户组id',
+  `uid` int(11) NULL DEFAULT NULL COMMENT '用户id',
+  `permission_json` text CHARACTER SET utf8 COLLATE utf8_unicode_ci NULL COMMENT '权限json字符串',
+  `create_time` datetime NOT NULL COMMENT '创建时间',
+  PRIMARY KEY (`id`) USING BTREE
+) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8 COLLATE = utf8_unicode_ci COMMENT = '支付审批用户权限表';
+
+CREATE TABLE `zh_payment_rpt_audit`  (
+  `id` int(11) NOT NULL AUTO_INCREMENT,
+  `tender_id` int(11) NOT NULL COMMENT '标段id',
+  `tr_id` int(11) NOT NULL COMMENT '标段报表关联id',
+  `td_id` int(11) NOT NULL COMMENT '详情id',
+  `uid` int(11) NOT NULL COMMENT '用户id',
+  `signature_index` tinyint(3) NOT NULL COMMENT '签名序号,区分签名位置',
+  `signature_name` varchar(255) CHARACTER SET utf8 COLLATE utf8_unicode_ci NOT NULL COMMENT '报表角色',
+  `signature_msg` json NULL COMMENT '签章的内容json',
+  `sign_time` datetime NULL DEFAULT NULL COMMENT '签字内容最后确定时间',
+  `in_time` datetime NOT NULL COMMENT '入库时间',
+  PRIMARY KEY (`id`) USING BTREE
+) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8 COLLATE = utf8_unicode_ci COMMENT = '支付审批报表表单角色表';
+
+CREATE TABLE `zh_payment_shenpi_audit`  (
+  `id` int(11) NOT NULL AUTO_INCREMENT,
+  `tid` int(11) NOT NULL COMMENT '标段id',
+  `tr_id` int(11) NOT NULL COMMENT '标段报表关联id(tender_rpt)',
+  `sp_status` tinyint(4) NOT NULL COMMENT '所选审批流程状态',
+  `audit_id` int(11) NOT NULL COMMENT '审批人id',
+  PRIMARY KEY (`id`) USING BTREE
+) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8 COLLATE = utf8_unicode_ci COMMENT = '审批流程人设置表';
+
+CREATE TABLE `zh_payment_tender`  (
+  `id` int(11) NOT NULL AUTO_INCREMENT,
+  `pid` int(11) NOT NULL COMMENT '项目id',
+  `uid` int(11) NOT NULL COMMENT '创建者id',
+  `folder_id` int(11) NOT NULL COMMENT '目录id',
+  `name` varchar(255) CHARACTER SET utf8 COLLATE utf8_unicode_ci NOT NULL COMMENT '标段名称',
+  `in_time` datetime NOT NULL COMMENT '入库时间',
+  PRIMARY KEY (`id`) USING BTREE
+) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8 COLLATE = utf8_unicode_ci COMMENT = '支付审批标段表';
+
+CREATE TABLE `zh_payment_tender_rpt`  (
+  `id` int(11) NOT NULL AUTO_INCREMENT,
+  `tender_id` int(11) NOT NULL COMMENT '标段id',
+  `rpt_id` int(11) NOT NULL COMMENT '报表id,从rpt_node取',
+  `rpt_name` varchar(255) CHARACTER SET utf8 COLLATE utf8_unicode_ci NULL DEFAULT NULL COMMENT '报表名称,防止报表删除后无法读取到旧数据',
+  `type` tinyint(2) NOT NULL DEFAULT 0 COMMENT '模块类型,0为表单,1为安全生产费',
+  `uid` int(11) NULL DEFAULT NULL COMMENT '上报人id',
+  `create_uid` int(11) NULL DEFAULT NULL COMMENT '创建者id',
+  `sp_status` tinyint(1) NOT NULL DEFAULT 1 COMMENT '审批流状态,默认为授权审批流',
+  `rpt_audit` json NULL COMMENT '报表签名人员对应的表单人员json',
+  `report_items_json` json NULL COMMENT '报表信息json',
+  `is_del` tinyint(1) NOT NULL DEFAULT 0 COMMENT '报表是否已删除',
+  `is_first` tinyint(1) NOT NULL DEFAULT 1 COMMENT '是否未配置角色',
+  `is_change` tinyint(1) NOT NULL DEFAULT 0 COMMENT '判断报表是否有变动过,当新建详情时更新这个值为0(用来判断是否删除详情)',
+  `in_time` datetime NOT NULL COMMENT '入库时间',
+  PRIMARY KEY (`id`) USING BTREE
+) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8 COLLATE = utf8_unicode_ci COMMENT = '标段报表关联及审批流程设置表';
 
 -- 很重要!!!!
 -- 请在该sql语句上方添加下一版本升级所需sql
@@ -16,5 +212,3 @@ UPDATE `zh_sub_project` p LEFT JOIN `zh_budget_std` bs ON p.std_id = bs.id SET p
 
 -- index
 ALTER TABLE `zh_material_bills_history` ADD INDEX `idx_mbid_mid_mspread` (`mb_id`, `mid`, `m_spread`);
-
-ALTER TABLE `zh_project` ADD `payment_setting` JSON NULL DEFAULT NULL COMMENT '支付审批模块设置' AFTER `fun_set`;