Browse Source

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

Tony Kang 9 months ago
parent
commit
7cf830cca3
74 changed files with 8345 additions and 247 deletions
  1. 1 0
      app/base/base_controller.js
  2. 161 8
      app/const/audit.js
  3. 34 0
      app/const/financial.js
  4. 1 0
      app/const/page_show.js
  5. 6 0
      app/const/shenpi.js
  6. 1 1
      app/controller/change_controller.js
  7. 5 0
      app/controller/dashboard_controller.js
  8. 1043 0
      app/controller/financial_controller.js
  9. 2 4
      app/controller/measure_controller.js
  10. 90 0
      app/controller/stage_controller.js
  11. 1 1
      app/extend/helper.js
  12. 121 0
      app/lib/crypt.js
  13. 72 0
      app/middleware/financial_check.js
  14. 79 0
      app/middleware/financial_pay_audit_check.js
  15. 92 0
      app/middleware/financial_pay_check.js
  16. 6 0
      app/public/css/main.css
  17. 54 2
      app/public/js/budget_detail.js
  18. 85 85
      app/public/js/contract_detail.js
  19. 179 0
      app/public/js/financial_index.js
  20. 929 0
      app/public/js/financial_pay.js
  21. 733 0
      app/public/js/financial_pay_detail.js
  22. 186 0
      app/public/js/financial_transfer.js
  23. 188 0
      app/public/js/financial_transfer_tender.js
  24. 6 0
      app/public/js/measure_compare.js
  25. 3 2
      app/public/js/shares/batch_import.js
  26. 10 0
      app/public/js/spreadjs_rela/spreadjs_zh.js
  27. 44 13
      app/public/js/stage.js
  28. 0 1
      app/public/js/tender_list_info.js
  29. 31 0
      app/router.js
  30. 36 36
      app/service/change.js
  31. 1 1
      app/service/construction_unit.js
  32. 33 0
      app/service/extra_pay.js
  33. 108 0
      app/service/financial_audit.js
  34. 289 0
      app/service/financial_pay.js
  35. 65 0
      app/service/financial_pay_att.js
  36. 535 0
      app/service/financial_pay_audit.js
  37. 171 0
      app/service/financial_pay_contract.js
  38. 37 0
      app/service/financial_pay_tender.js
  39. 256 0
      app/service/financial_pay_tender_audit.js
  40. 108 0
      app/service/financial_transfer.js
  41. 60 0
      app/service/financial_transfer_att.js
  42. 119 0
      app/service/financial_transfer_tender.js
  43. 60 0
      app/service/financial_transfer_tender_att.js
  44. 11 11
      app/service/shenpi_audit.js
  45. 4 2
      app/service/stage_bills.js
  46. 86 3
      app/service/stage_stash.js
  47. 15 0
      app/service/sub_project.js
  48. 72 41
      app/service/tender_cache.js
  49. 34 2
      app/view/dashboard/index.ejs
  50. 35 0
      app/view/financial/index.ejs
  51. 266 0
      app/view/financial/modal.ejs
  52. 134 0
      app/view/financial/pay.ejs
  53. 128 0
      app/view/financial/pay_detail.ejs
  54. 758 0
      app/view/financial/pay_detail_modal.ejs
  55. 205 0
      app/view/financial/pay_modal.ejs
  56. 15 0
      app/view/financial/sub_menu.ejs
  57. 26 0
      app/view/financial/sub_menu_list.ejs
  58. 16 0
      app/view/financial/sub_mini_menu.ejs
  59. 59 0
      app/view/financial/transfer.ejs
  60. 80 0
      app/view/financial/transfer_modal.ejs
  61. 73 0
      app/view/financial/transfer_tender.ejs
  62. 111 0
      app/view/financial/transfer_tender_modal.ejs
  63. 6 2
      app/view/measure/compare_modal.ejs
  64. 2 4
      app/view/measure/stage.ejs
  65. 1 1
      app/view/shares/batch_import_modal.ejs
  66. 66 0
      app/view/shares/import_file_modal.ejs
  67. 1 1
      app/view/shares/tender_select_modal.ejs
  68. 2 1
      app/view/stage/modal.ejs
  69. 2 1
      config/config.default.js
  70. 0 13
      config/config.local.js
  71. 0 11
      config/config.remoteqa.js
  72. 8 0
      config/menu.js
  73. 87 0
      config/web.js
  74. 1 0
      package.json

+ 1 - 0
app/base/base_controller.js

@@ -48,6 +48,7 @@ class BaseController extends Controller {
         menuList.file.display = ctx.session && ctx.session.sessionProject ? ctx.session.sessionProject.page_show.openFile : false;
         menuList.construction.display = ctx.session && ctx.session.sessionProject ? ctx.session.sessionProject.page_show.openConstruction : false;
         menuList.contract.display = ctx.session && ctx.session.sessionProject ? ctx.session.sessionProject.page_show.openContract : false;
+        menuList.financial.display = ctx.session && ctx.session.sessionProject ? ctx.session.sessionProject.page_show.openFinancial : false;
         // 菜单列表
         ctx.menuList = menuList;
         ctx.showProject = false;

+ 161 - 8
app/const/audit.js

@@ -11,8 +11,8 @@
 const auditType = (function () {
     const types = [
         { key: 'common', name: '个人', value: 1, short: '', long: '', class: '', },
-        { key: 'and', name: '会签', value: 2, short: '会', long: '多人会签', class: 'primary', valid: ['ledger', 'revise', 'stage', 'change'] },
-        { key: 'or', name: '或签', value: 3, short: '或', long: '多人或签', class: 'success', valid: ['ledger', 'revise', 'stage', 'change'] },
+        { key: 'and', name: '会签', value: 2, short: '会', long: '多人会签', class: 'primary', valid: ['ledger', 'revise', 'stage', 'change', 'financial'] },
+        { key: 'or', name: '或签', value: 3, short: '或', long: '多人或签', class: 'success', valid: ['ledger', 'revise', 'stage', 'change', 'financial'] },
         { key: 'union', name: '协同', value: 4, short: '协', long: '多人协同', class: 'warning', valid: ['stage']},
     ];
     const key = {};
@@ -24,6 +24,36 @@ const auditType = (function () {
     return { types, key, info };
 })();
 
+const auditMasterType = {
+    stage: 'stage',
+    material: 'material',
+    ledger: 'ledger',
+    revise: 'revise',
+    pay: 'pay', // 独立合同支付
+};
+
+// 期审批流程
+const common = (function() {
+    const auditStatusInfo = [
+        { key: 'uncheck', value: 1, title: '待上报', class: '', btnTitle: '上报', btnClass: 'btn-primary' },
+        { key: 'checking', value: 2, title: '审批中', class: 'text-warning', btnTitle: '审批', btnClass: 'btn-success' },
+        { key: 'checked', value: 3, title: '审批通过', class: 'text-success', btnTitle: '', btnClass: 'btn-primary' },
+        { key: 'checkNo', value: 4, title: '审批退回', class: 'text-warning', btnTitle: '重新上报', btnClass: 'btn-primary' },
+        { key: 'checkNoPre', value: 5, title: '审批退回', class: 'text-warning', btnTitle: '重新审批', btnClass: 'btn-primary' },
+        { key: 'checkSkip', value: 6, title: '', class: '', btnTitle: '', btnClass: '' },
+        { key: 'checkCancel', value: 7, title: '撤回', class: 'text-warning', btnTitle: '', btnClass: '' },
+        { key: 'checkAgain', value: 8, title: '重新审批', class: 'text-warning', btnTitle: '', btnClass: '' },
+    ];
+    return (function(){
+        const status = {}, info = [];
+        for (const i of auditStatusInfo) {
+            status[i.key] = i.value;
+            info[i.value] = i;
+        }
+        return {status, info, timesLen: 100, backType: { org: 1, pre: 2 }};
+    })();
+})();
+
 // 台账审批流程
 const ledger = (function() {
     const status = {
@@ -187,6 +217,7 @@ const stage = (function() {
     statusButtonClass[status.checkNoPre] = 'btn-warning';
     statusButtonClass[status.checkAgain] = 'btn-warning';
     statusButtonClass[status.checkCancel] = 'btn-warning';
+
     // 描述文本
     const auditString = [];
     auditString[status.uncheck] = '';
@@ -217,19 +248,19 @@ const stage = (function() {
     auditProgress[status.uncheck] = '待上报';
     auditProgress[status.checking] = '审批中';
     auditProgress[status.checked] = '审批通过';
-    auditProgress[status.checkNo] = '审批退回';
-    auditProgress[status.checkNoPre] = '审批退回';
-    auditProgress[status.checkAgain] = '重新审批';
-    auditProgress[status.checkCancel] = '撤回';
+    auditProgress[status.checkNo] = '重新上报';
+    auditProgress[status.checkNoPre] = '审批';
+    auditProgress[status.checkAgain] = '审批';
+    auditProgress[status.checkCancel] = '';
     // 样式
     const auditProgressClass = [];
     auditProgressClass[status.uncheck] = '';
     auditProgressClass[status.checking] = 'text-warning';
     auditProgressClass[status.checked] = 'text-success';
-    auditProgressClass[status.checkNo] = 'text-warning';
+    auditProgressClass[status.checkNo] = '';
     auditProgressClass[status.checkNoPre] = 'text-warning';
     auditProgressClass[status.checkAgain] = 'text-warning';
-    auditProgressClass[status.checkCancel] = 'text-warning';
+    auditProgressClass[status.checkCancel] = '';
     /* ------------------------------------------------------- */
 
     const tiStatusString = [];
@@ -1073,6 +1104,125 @@ const changePlan = (function() {
     return { status, statusString, statusClass, auditString, auditStringClass, auditProgress, auditProgressClass, filter, statusButton, statusButtonClass };
 })();
 
+// 资金支付
+const financial = (function() {
+    const status = {
+        uncheck: 1, // 待审批
+        checking: 2, // 审批中或者原报人待上报或者原报上报修订中
+        checked: 3, // 审批通过或者原报人上报完成
+        // checkNo: 4,     // 审批终止
+        checkNo: 5, // 退回到原报人重新上报
+        // checkNoPre: 6, // 退回到上一个审批人
+        // checkAgain: 7, // 重新审批
+        checkSkip: 8, // 跳过
+        // revise: 9, // 修订变更
+        // cancelRevise: 10, // 撤销修订
+        // checkCancel: 11, // 撤回 // 该状态为上一审批人可发起,回到它到审批阶段,并同时新增一条新的审批中记录
+    };
+    const statusString = [];
+    statusString[status.uncheck] = '待上报';
+    statusString[status.checking] = '审批中';
+    statusString[status.checked] = '审批通过';
+    statusString[status.checkNo] = '审批退回';
+    // statusString[status.checkNoPre] = '审批退回';
+    // statusString[status.checkAgain] = '重新审批';
+    // statusString[status.revise] = '修订';
+    // statusString[status.cancelRevise] = '撤销修订';
+    // statusString[status.checkCancel] = '撤回';
+
+    const statusClass = [];
+    statusClass[status.uncheck] = '';
+    statusClass[status.checking] = 'text-warning';
+    statusClass[status.checked] = 'text-success';
+    statusClass[status.checkNo] = 'text-warning';
+    // statusClass[status.checkNoPre] = 'text-warning';
+    // statusClass[status.checkAgain] = 'text-warning';
+    // statusClass[status.revise] = 'text-warning';
+    // statusClass[status.cancelRevise] = 'text-success';
+    // statusClass[status.checkCancel] = 'text-warning';
+
+    // 标段概况页
+    // 描述文本
+    const auditString = [];
+    auditString[status.uncheck] = '待上报';
+    auditString[status.checking] = '审批中';
+    auditString[status.checked] = '审批通过';
+    auditString[status.checkNo] = '审批退回';
+    // auditString[status.checkNoPre] = '审批退回';
+    // auditString[status.checkAgain] = '重新审批';
+    // auditString[status.revise] = '修订';
+    // auditString[status.cancelRevise] = '撤销修订';
+    // auditString[status.checkCancel] = '撤回';
+    auditString[status.checkSkip] = '审批通过';
+    // 文字样式
+    const auditStringClass = [];
+    auditStringClass[status.uncheck] = '';
+    auditStringClass[status.checking] = 'text-warning';
+    auditStringClass[status.checked] = 'text-success';
+    auditStringClass[status.checkNo] = 'text-warning';
+    // auditStringClass[status.checkNoPre] = 'text-warning';
+    // auditStringClass[status.checkAgain] = 'text-warning';
+    // auditStringClass[status.revise] = 'text-warning';
+    // auditStringClass[status.cancelRevise] = 'text-success';
+    // auditStringClass[status.checkCancel] = 'text-warning';
+    auditStringClass[status.checkSkip] = 'text-success';
+    // 描述文本
+    const auditProgress = [];
+    auditProgress[status.uncheck] = '待上报';
+    auditProgress[status.checking] = '审批中';
+    auditProgress[status.checked] = '审批通过';
+    auditProgress[status.checkNo] = '审批退回';
+    auditProgress[status.checkNoPre] = '审批退回';
+    // auditProgress[status.checkAgain] = '重新审批';
+    // auditProgress[status.revise] = '修订中';
+    // auditProgress[status.cancelRevise] = '撤销修订';
+    // auditProgress[status.checkCancel] = '撤回';
+    auditProgress[status.checkSkip] = '审批通过';
+    // 样式
+    const auditProgressClass = [];
+    auditProgressClass[status.uncheck] = '';
+    auditProgressClass[status.checking] = 'text-warning';
+    auditProgressClass[status.checked] = 'text-success';
+    auditProgressClass[status.checkNo] = 'text-warning';
+    auditProgressClass[status.checkNoPre] = 'text-warning';
+    // auditProgressClass[status.checkAgain] = 'text-warning';
+    // auditProgressClass[status.revise] = 'text-warning';
+    // auditProgressClass[status.cancelRevise] = 'text-success';
+    // auditProgressClass[status.checkCancel] = 'text-warning';
+    auditProgressClass[status.checkSkip] = 'text-success';
+
+    const filter = {
+        status: {
+            pending: 1,
+            uncheck: 5,
+            checking: 2,
+            checked: 3,
+        },
+        statusString: [],
+    };
+    filter.statusString[filter.status.pending] = '待处理';
+    filter.statusString[filter.status.uncheck] = '待上报';
+    filter.statusString[filter.status.checking] = '审批中';
+    filter.statusString[filter.status.checked] = '审批通过';
+
+    // 按钮
+    const statusButton = [];
+    statusButton[status.uncheck] = '上报';
+    statusButton[status.checking] = '审批';
+    statusButton[status.checked] = '';
+    statusButton[status.checkNo] = '重新上报';
+    // statusButton[status.revise] = '修订';
+
+    // 按钮样式
+    const statusButtonClass = [];
+    statusButtonClass[status.uncheck] = 'btn-primary';
+    statusButtonClass[status.checking] = 'btn-success';
+    statusButtonClass[status.checked] = '';
+    statusButtonClass[status.checkNo] = 'btn-warning';
+    // statusButtonClass[status.revise] = 'btn-warning';
+    return { status, statusString, statusClass, auditString, auditStringClass, auditProgress, auditProgressClass, filter, statusButton, statusButtonClass };
+})();
+
 // 推送类型
 const pushType = {
     material: 1,
@@ -1085,9 +1235,11 @@ const pushType = {
     changeApply: 8,
     changePlan: 9,
     settle: 10,
+    financial: 11,
 };
 
 module.exports = {
+    common,
     auditType,
     ledger,
     stage,
@@ -1111,4 +1263,5 @@ module.exports = {
     changeProject,
     changeApply,
     changePlan,
+    financial,
 };

+ 34 - 0
app/const/financial.js

@@ -0,0 +1,34 @@
+'use strict';
+
+/**
+ * 资金监管
+ *
+ * @author ELlisran
+ * @date 2019/10/20
+ * @version
+ */
+const used = ['劳务工工资', '材料款', '机械设备', '劳务结算', '安全经费', '技术服务费', '备用金', '报销款', '电费', '水费', '税费', '零星材料', '内部往来款', '征迁款', '职工工资', '职工社保', '其它'];
+const pay_type = ['网上转账', '支付宝', '微信', '现金', '发票', '其他'];
+// 类型
+// const type = {
+//     expenses: 1,
+//     income: 2,
+// };
+//
+// const typeMap = {
+//     1: 'expenses',
+//     2: 'income',
+// };
+//
+// const typeName = {
+//     1: '支付',
+//     2: '回款',
+// };
+
+module.exports = {
+    used,
+    pay_type,
+    // type,
+    // typeMap,
+    // typeName,
+};

+ 1 - 0
app/const/page_show.js

@@ -61,6 +61,7 @@ const defaultSetting = {
     openConstruction: 1,
     openMaterialStageRepeat: 0,
     openContract: 1,
+    openFinancial: 1,
 };
 
 

+ 6 - 0
app/const/shenpi.js

@@ -16,6 +16,10 @@ const sp_type = {
     settle: 7,
     change: 5,
     material: 6,
+    // financial: 8, // 资金支付审批流程设置不出现在这里,但请别用8这个类型控制审批流程,因为数据库我用了8来表示资金支付固定审批流
+};
+const sp_other_type = {
+    financial: 8,
 };
 // const sp_name = [];
 // sp_name[sp_type.advance] = '预付款审批';
@@ -33,6 +37,7 @@ const sp_lc = [
     { code: 'settle', type: sp_type.settle, name: '结算期审批' },
     { code: 'change', type: sp_type.change, name: '工程变更审批' },
     { code: 'material', type: sp_type.material, name: '材料调差审批' },
+    // { code: 'financial', type: sp_type.financial, name: '资金支付审批' },
 ];
 
 const sp_status = {
@@ -60,6 +65,7 @@ const change_type_list = [
 
 module.exports = {
     sp_type,
+    sp_other_type,
     sp_lc,
     sp_status,
     sp_status_list,

+ 1 - 1
app/controller/change_controller.js

@@ -58,7 +58,7 @@ module.exports = app => {
             const changes = await ctx.service.change.getListByStatus(tender.id, status, 1, sorts, orders, state);
             const total = await ctx.service.change.getCountByStatus(tender.id, status, state);
             let page_total = 0;
-            const tp = await ctx.service.change.getTp(tender.id, status);
+            const tp = await ctx.service.change.getTp(tender.id, status, state);
             if (changes !== null) {
                 for (const c of changes) {
                     if (c.status !== audit.change.status.checked || !c.final_auditor_str) {

+ 5 - 0
app/controller/dashboard_controller.js

@@ -36,6 +36,7 @@ module.exports = app => {
             const auditChangePlan = ctx.session.sessionProject.page_show.openChangePlan ? await ctx.service.changePlanAudit.getAuditChangePlan(ctx.session.sessionUser.accountId) : [];
             const auditPayments = ctx.session.sessionProject.page_show.openPayment ? await ctx.service.paymentDetailAudit.getAuditPayment(ctx.session.sessionUser.accountId) : [];
             const auditStageAss = await ctx.service.stageAuditAss.getAuditStageAss(ctx.session.sessionUser.accountId);
+            const auditFinancials = ctx.session.sessionProject.page_show.openFinancial ? await ctx.service.financialPayAudit.getAuditFinancial(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);
@@ -64,6 +65,7 @@ module.exports = app => {
             if (ctx.session.sessionProject.page_show.openChangeApply) shenpi_count.push({ count: await ctx.service.changeApplyAudit.getCountByChecked(ctx.session.sessionUser.accountId), name: '变更申请' });
             if (ctx.session.sessionProject.page_show.openChangePlan) shenpi_count.push({ count: await ctx.service.changePlanAudit.getCountByChecked(ctx.session.sessionUser.accountId), name: '变更方案' });
             if (ctx.session.sessionProject.page_show.openMaterial) shenpi_count.push({ count: await ctx.service.materialAudit.getCountByChecked(ctx.session.sessionUser.accountId), name: '材料调差' });
+            if (ctx.session.sessionProject.page_show.openFinancial) shenpi_count.push({ count: await ctx.service.financialPayAudit.getCountByChecked(ctx.session.sessionUser.accountId), name: '资金支付' });
             // shenpi_count.push({ count: await ctx.service.advanceAudit.getCountByChecked(ctx.session.sessionUser.accountId), name: '预付款' });
             const total_count = ctx.app._.sumBy(shenpi_count, 'count');
             const shenpi_lastime = [
@@ -76,6 +78,7 @@ module.exports = app => {
                 ctx.session.sessionProject.page_show.openChangeApply ? await ctx.service.changeApplyAudit.getLastEndTimeByChecked(ctx.session.sessionUser.accountId) : null,
                 ctx.session.sessionProject.page_show.openChangePlan ? await ctx.service.changePlanAudit.getLastEndTimeByChecked(ctx.session.sessionUser.accountId) : null,
                 ctx.session.sessionProject.page_show.openMaterial ? await ctx.service.materialAudit.getLastEndTimeByChecked(ctx.session.sessionUser.accountId) : null,
+                ctx.session.sessionProject.page_show.openFinancial ? await ctx.service.financialPayAudit.getLastEndTimeByChecked(ctx.session.sessionUser.accountId) : null,
             ];
             const last_time = ctx.app._.max(shenpi_lastime);
             // console.log(ctx.app._.max(shenpi_lastime), ctx.helper.calcDayNum(last_time));
@@ -91,6 +94,7 @@ module.exports = app => {
                 auditChangePlan,
                 auditPayments,
                 auditStageAss,
+                auditFinancials,
                 shenpi_count,
                 total_count,
                 last_day: ctx.helper.calcDayNum(last_time),
@@ -105,6 +109,7 @@ module.exports = app => {
                 acChangeProject: auditConst.changeProject,
                 acChangeApply: auditConst.changeApply,
                 acChangePlan: auditConst.changeApply,
+                acFinancial: auditConst.financial,
                 noticeList,
                 pushType: auditConst.pushType,
                 projectData,

File diff suppressed because it is too large
+ 1043 - 0
app/controller/financial_controller.js


+ 2 - 4
app/controller/measure_controller.js

@@ -55,10 +55,8 @@ module.exports = app => {
                     }
                     if (!s.final_auditor_str || s.status !== auditConst.status.checked) {
                         // 根据期状态返回展示用户
-                        s.curAuditors = await ctx.service.stageAudit.getAuditorsByStatus(s.id, s.status, s.times);
-                        if (s.status === auditConst.status.checkNoPre) {
-                            s.curAuditors2 = await ctx.service.stageAudit.getAuditorsByStatus(s.id, auditConst.status.checking, s.times);
-                        }
+                        if (s.status === auditConst.status.checkNoPre) s.status = auditConst.status.checking;
+                        if (s.status !== auditConst.status.checkNo) s.curAuditors = await ctx.service.stageAudit.getAuditorsByStatus(s.id, s.status, s.times);
                         if (s.status === auditConst.status.checked) {
                             const final_auditor_str = (s.curAuditors[0].audit_type === auditType.key.common)
                                 ? `${s.curAuditors[0].name}${(s.curAuditors[0].role ? '-' + s.curAuditors[0].role : '')}`

+ 90 - 0
app/controller/stage_controller.js

@@ -2291,6 +2291,96 @@ module.exports = app => {
                 ctx.ajaxErrorBody(err, '导入计量台账数据错误');
             }
         }
+
+        async exportStageData(ctx) {
+            try {
+                const ledgerData = await ctx.service.ledger.getAllDataByCondition({
+                    columns: ['id', 'ledger_id', 'ledger_pid', 'level', 'order', 'full_path', 'is_leaf', 'code', 'b_code', 'name', 'unit', 'unit_price', 'node_type'],
+                    where: { tender_id: ctx.tender.id }
+                });
+                const extraLedgerData = await ctx.service.ledgerExtra.getData(ctx.tender.id, this.ledgerExtraColumn);
+                const dgnData = await ctx.service.stageBillsDgn.getDgnData(ctx.tender.id);
+                const curStageLedgerData = ctx.stage.readOnly
+                    ? await ctx.service.stageBills.getAuditorStageData2(ctx.tender.id, ctx.stage.id, ctx.stage.curTimes, ctx.stage.curOrder)
+                    : await ctx.service.stageBills.getLastestStageData2(ctx.tender.id, ctx.stage.id);
+                this.ctx.helper.assignRelaData(ledgerData, [
+                    { data: dgnData, fields: ['deal_dgn_qty1', 'deal_dgn_qty2', 'c_dgn_qty1', 'c_dgn_qty2'], prefix: '', relaId: 'id' },
+                    { data: extraLedgerData, fields: ['is_tp'], prefix: '', relaId: 'id' },
+                    { data: curStageLedgerData, fields: ['contract_qty', 'contract_tp', 'postil'], prefix: '', relaId: 'lid' },
+                ]);
+
+                const posData = await ctx.service.pos.getAllDataByCondition({
+                    columns: ['id', 'lid', 'name', 'position', 'porder'],
+                    where: { tid: ctx.tender.id }
+                });
+                // 根据当前人,或指定对象查询数据
+                const curStagePosData = ctx.stage.readOnly
+                    ? await ctx.service.stagePos.getAuditorStageData2(ctx.tender.id, ctx.stage.id, ctx.stage.curTimes, ctx.stage.curOrder)
+                    : await ctx.service.stagePos.getLastestStageData2(ctx.tender.id, ctx.stage.id);
+                this.ctx.helper.assignRelaData(posData, [
+                    { data: curStagePosData, fields: ['contract_qty', 'postil'], prefix: '', relaId: 'pid' },
+                ]);
+
+                const Cpd = require('../lib/crypt').cpd;
+                const cpd = new Cpd();
+                const filename = `第${ctx.stage.order}期计量数据.cpd`;
+                const filepath = path.join(this.ctx.app.baseDir, 'temp', `第${ctx.stage.order}期计量数据.cpd`);
+                await cpd.encrypt({ ledger: ledgerData, pos: posData }, filepath);
+                const userAgent = (ctx.request.header['user-agent'] || '').toLowerCase();
+                let disposition = '';
+                if (userAgent.indexOf('msie') >= 0 || userAgent.indexOf('chrome') >= 0) {
+                    disposition = 'attachment; filename=' + encodeURIComponent(filename);
+                } else if (userAgent.indexOf('firefox') >= 0) {
+                    disposition = 'attachment; filename*="utf8\'\'' + encodeURIComponent(filename) + '"';
+                } else {
+                    /* safari等其他非主流浏览器只能自求多福了 */
+                    disposition = 'attachment; filename=' + new Buffer(filename).toString('binary');
+                }
+                ctx.response.set({
+                    'Content-Type': 'application/octet-stream',
+                    'Content-Disposition': disposition,
+                });
+                ctx.body = await fs.readFileSync(filepath);
+            } catch(err) {
+                ctx.log(err);
+                ctx.postError(err, '导出计量法数据失败');
+            }
+        }
+
+        async importStageData(ctx) {
+            const create_time = Date.parse(new Date()) / 1000;
+            const filepath = path.join(this.ctx.app.baseDir, 'temp', `计量数据${create_time}.cpd`);
+            let stream, index = 0;
+            try {
+                const parts = ctx.multipart({ autoFields: true });
+                stream = await parts();
+                while (stream !== undefined) {
+                    if (!stream.filename) throw '未发现上传文件!';
+                    // 保存文件
+                    await ctx.helper.saveStreamFile(stream, filepath);
+                    await sendToWormhole(stream);
+                    ++index;
+                    if (Array.isArray(parts.field.size) && index < parts.field.size.length) {
+                        stream = await parts();
+                    } else {
+                        stream = undefined;
+                    }
+                }
+                const Cpd = require('../lib/crypt').cpd;
+                const cpd = new Cpd();
+                const data = await cpd.decrypt(filepath);
+                await ctx.service.stageStash.loadStageDealData(ctx.stage, data);
+                await ctx.service.stage.updateCheckCalcFlag(ctx.stage, true);
+                await ctx.service.stage.updateCacheTime(ctx.stage.id);
+                ctx.body = { err: 0, mgs: '', data: '' };
+            } catch (err) {
+                // 失败需要消耗掉stream 以防卡死
+                if (stream) await sendToWormhole(stream);
+                ctx.log(err);
+                ctx.ajaxErrorBody(err, '导入文件失败');
+            }
+        }
+
     }
 
     return StageController;

+ 1 - 1
app/extend/helper.js

@@ -1516,7 +1516,7 @@ module.exports = {
 
     async ossFileGet(path) {
         // 判断开头是否带app,否则加上
-        if (!_.includes(path, 'app/') && !_.includes(path, 'sp/contract/')) {
+        if (!_.includes(path, 'app/') && !_.includes(path, 'sp/contract/') && !_.includes(path, 'sp/financial/')) {
             path = 'app/' + path;
         }
         const result = await this.ctx.app.fujianOss.get(this.ctx.app.config.fujianOssFolder + path);

+ 121 - 0
app/lib/crypt.js

@@ -0,0 +1,121 @@
+'use strict';
+
+/**
+ *
+ *
+ * @author Mai
+ * @date
+ * @version
+ */
+
+const crypto = require('crypto-js');
+const fs = require('fs');
+const path = require('path');
+const encryptSpr = '|----|';
+const baseKey = crypto.enc.Utf8.parse('Sc3850?Calc888Zh'); // 十六位十六进制数作为密钥
+const baseIv = crypto.enc.Utf8.parse('Zh888!Sc3850Calc'); // 十六位十六进制数作为密钥偏移量
+
+const baseUtils = {
+    aesDecrypt: function(str, key, iv) {
+        const encryptedHexStr = crypto.enc.Hex.parse(str);
+        const src = crypto.enc.Base64.stringify(encryptedHexStr);
+        const decrypt = crypto.AES.decrypt(src, key, {
+            iv,
+            mode: crypto.mode.CBC,
+            padding: crypto.pad.Pkcs7,
+        });
+        const decryptedStr = decrypt.toString(crypto.enc.Utf8);
+        return decryptedStr.toString();
+    },
+    aesEncrypt: function(str, key, iv) {
+        const src = crypto.enc.Utf8.parse(str);
+        const encrypted = crypto.AES.encrypt(src, key, {
+            iv,
+            mode: crypto.mode.CBC,
+            padding: crypto.pad.Pkcs7,
+        });
+        return encrypted.ciphertext.toString().toUpperCase();
+    },
+    md5: function(str) {
+        return crypto.MD5(str).toString();
+    },
+    decryptBuffer: function(buffer) {
+        try {
+            const result = [];
+            const arr = buffer.split(encryptSpr);
+            const header = JSON.parse(this.aesDecrypt(arr[0], baseKey, baseIv));
+            header.key = crypto.enc.Hex.parse(header.key);
+            header.iv = crypto.enc.Hex.parse(header.iv);
+            arr.shift();
+            const check = this.md5(arr.join(encryptSpr));
+            if (check !== header.check) throw '文件被篡改';
+            for (const a of arr) {
+                result.push(JSON.parse(this.aesDecrypt(a, header.key, header.iv)));
+            }
+            return header.data === 'array' ? result : result[0];
+        } catch(err) {
+            throw err;
+        }
+    },
+    getRamdonHeader: function () {
+        return {
+            key: crypto.lib.WordArray.random(16),
+            iv: crypto.lib.WordArray.random(16),
+        };
+    },
+    encryptBuffer: function(data) {
+        const datas = data instanceof Array ? data : [data];
+        const header = this.getRamdonHeader();
+        const arr = [];
+        for (const d of datas) {
+            arr.push(this.aesEncrypt(JSON.stringify(d), header.key, header.iv ));
+        }
+        header.key = header.key.toString();
+        header.iv = header.iv.toString();
+        header.check = this.md5(arr.join(encryptSpr));
+        header.data = data instanceof Array ? 'array' : 'object';
+        return this.aesEncrypt(JSON.stringify(header), baseKey, baseIv) + encryptSpr + arr.join(encryptSpr);
+    },
+};
+
+class baseFile {
+    doDecrypt(str) {
+
+    }
+    async decrypt(file) {
+        const str = await fs.readFileSync(file, 'utf-8');
+        return this.doDecrypt(str);
+    }
+    async recursiveMkdirSync(pathName) {
+        if (!fs.existsSync(pathName)) {
+            const upperPath = path.dirname(pathName);
+            if (!fs.existsSync(upperPath)) {
+                await this.recursiveMkdirSync(upperPath);
+            }
+            await fs.mkdirSync(pathName);
+        }
+    }
+    doEncrypt(data) {
+
+    }
+    async encrypt(data, file) {
+        const buffer = this.doEncrypt(data);
+        // 检查文件夹是否存在,不存在则直接创建文件夹
+        const pathName = path.dirname(file);
+        if (!fs.existsSync(pathName)) await this.recursiveMkdirSync(pathName);
+        await fs.writeFileSync(file, buffer);
+    }
+}
+
+class cpd extends baseFile {
+    doEncrypt(data) {
+        return baseUtils.encryptBuffer(data);
+    }
+    doDecrypt(str) {
+        return baseUtils.decryptBuffer(str);
+    }
+}
+
+module.exports = {
+    cpd
+};

+ 72 - 0
app/middleware/financial_check.js

@@ -0,0 +1,72 @@
+'use strict';
+
+/**
+ *
+ *
+ * @author Mai
+ * @date
+ * @version
+ */
+
+const messageType = require('../const/message_type');
+const _ = require('lodash');
+const financialConst = require('../const/financial');
+
+module.exports = options => {
+    /**
+     * 标段校验 中间件
+     * 1. 读取标段数据(包括属性)
+     * 2. 检验用户是否可见标段(不校验具体权限)
+     *
+     * @param {function} next - 中间件继续执行的方法
+     * @return {void}
+     */
+    return function* financialCheck(next) {
+        try {
+            if (!this.session.sessionProject.page_show.openFinancial) {
+                throw '该功能已关闭或无法查看';
+            }
+            const spid = this.params.spid;
+            if (!spid) {
+                throw '参数数据错误';
+            }
+            this.subProject = yield this.service.subProject.getDataById(spid);
+            if (this.subProject.project_id !== this.session.sessionProject.id) throw '您无权查看该项目资金监管';
+            const fAudit = yield this.service.financialAudit.getDataByCondition({ spid: this.subProject.id, uid: this.session.sessionUser.accountId });
+            if (!fAudit && !this.session.sessionUser.is_admin) throw '您无权查看该项目资金监管,请联系管理员添加';
+            if (!this.subProject) throw '项目不存在';
+            yield next;
+        } catch (err) {
+            // 输出错误到日志
+            if (err.stack) {
+                this.logger.error(err);
+            } else {
+                this.session.message = {
+                    type: messageType.ERROR,
+                    icon: 'exclamation-circle',
+                    message: err,
+                };
+                this.getLogger('fail').info(JSON.stringify({
+                    error: err,
+                    project: this.session.sessionProject,
+                    user: this.session.sessionUser,
+                    body: this.session.body,
+                }));
+            }
+            if (this.helper.isAjax(this.request)) {
+                if (err.stack) {
+                    this.body = { err: 4, msg: '标段数据未知错误', data: null };
+                } else {
+                    this.body = { err: 3, msg: err.toString(), data: null };
+                }
+            } else {
+                if (this.helper.isWap(this.request)) {
+                    this.redirect('/wap/list');
+                } else {
+                    this.postError(err, '未知错误');
+                    err === '该功能已关闭或无法查看' ? this.redirect('/dashboard') : this.request.headers.referer ? this.redirect(this.request.headers.referer) : this.redirect('/financial');
+                }
+            }
+        }
+    };
+};

+ 79 - 0
app/middleware/financial_pay_audit_check.js

@@ -0,0 +1,79 @@
+'use strict';
+
+/**
+ *
+ *
+ * @author Ellisran
+ * @date 2020/10/15
+ * @version
+ */
+
+const status = require('../const/audit').changeApply.status;
+const shenpiConst = require('../const/shenpi');
+const _ = require('lodash');
+
+module.exports = options => {
+    /**
+     * 标段校验 中间件
+     * 1. 读取标段数据(包括属性)
+     * 2. 检验用户是否可见标段(不校验具体权限)
+     *
+     * @param {function} next - 中间件继续执行的方法
+     * @return {void}
+     */
+    return function* financailPayAuditCheck(next) {
+        try {
+            // 获取revise
+            const id = this.params.fpid || this.request.body.fpid;
+            if (!id) {
+                throw '您访问的资金支付不存在';
+            }
+            // const change = yield this.service.change.getDataByCondition({ cid });
+            if (!this.financialPay) {
+                const financialPay = yield this.service.financialPay.getDataById(id);
+                if (!financialPay) throw '资金支付数据有误';
+                yield this.service.financialPay.loadChangeUser(financialPay);
+                this.financialPay = financialPay;
+            }
+            const financialPay = this.financialPay;
+            if ((financialPay.status === status.uncheck || financialPay.status === status.checkNo)) {
+                // 进一步比较审批流是否与审批流程设置的相同,不同则替换为固定审批流或固定的终审
+                const auditList = yield this.service.financialPayAudit.getAllDataByCondition({ where: { fpid: financialPay.id, times: financialPay.times }, orders: [['order', 'asc']] });
+                const condition = { tid: financialPay.tid, sp_type: shenpiConst.sp_other_type.financial, sp_status: shenpiConst.sp_status.gdspl };
+                const shenpiList = yield this.service.shenpiAudit.getAllDataByCondition({ where: condition, orders: [['audit_order', 'asc']] });
+                yield this.service.shenpiAudit.noYbShenpiList(financialPay.uid, shenpiList);
+                // 判断2个id数组是否相同,不同则删除原审批流,切换成固定的审批流
+                let sameAudit = auditList.length === shenpiList.length;
+                if (sameAudit) {
+                    for (const audit of auditList) {
+                        const shenpi = shenpiList.find(x => { return x.audit_id === audit.aid; });
+                        if (!shenpi || shenpi.audit_order !== audit.audit_order || shenpi.audit_type !== audit.audit_type) {
+                            sameAudit = false;
+                            break;
+                        }
+                    }
+                }
+                if (!sameAudit) {
+                    yield this.service.financialPayAudit.updateNewAuditList(financialPay, shenpiList);
+                    yield this.service.financialPay.loadPayUser(financialPay);
+                }
+            }
+            yield next;
+        } catch (err) {
+            console.log(err);
+            // 输出错误到日志
+            if (err.stack) {
+                this.logger.error(err);
+            } else {
+                this.getLogger('fail').info(JSON.stringify({
+                    error: err,
+                    project: this.session.sessionProject,
+                    user: this.session.sessionUser,
+                    body: this.session.body,
+                }));
+            }
+            // 重定向值标段管理
+            this.redirect(this.request.headers.referer);
+        }
+    };
+};

+ 92 - 0
app/middleware/financial_pay_check.js

@@ -0,0 +1,92 @@
+'use strict';
+
+/**
+ *
+ *
+ * @author Mai
+ * @date
+ * @version
+ */
+
+const messageType = require('../const/message_type');
+const _ = require('lodash');
+const financialConst = require('../const/financial');
+const status = require('../const/audit').financial.status;
+
+module.exports = options => {
+    /**
+     * 标段校验 中间件
+     * 1. 读取标段数据(包括属性)
+     * 2. 检验用户是否可见标段(不校验具体权限)
+     *
+     * @param {function} next - 中间件继续执行的方法
+     * @return {void}
+     */
+    return function* financialPayCheck(next) {
+        try {
+            if (!this.session.sessionProject.page_show.openFinancial) {
+                throw '该功能已关闭或无法查看';
+            }
+            if (!this.subProject) throw '项目不存在';
+            const fpid = this.params.fpid;
+            if (!fpid) {
+                throw '参数数据错误';
+            }
+            const financialPay = yield this.service.financialPay.getOnePay(fpid);
+            yield this.service.financialPay.loadPayUser(financialPay);
+            // 权限相关
+            // todo 校验权限 (标段参与人、分享)
+            const accountId = this.session.sessionUser.accountId,
+                auditorIds = _.map(financialPay.auditors, 'aid');
+            if (financialPay.permission.pay_show) {
+                const fptAudits = yield this.service.financialPayTenderAudit.getDataByCondition({ spid: financialPay.spid, tid: financialPay.tid, uid: accountId });
+                if (!this.session.sessionUser.is_admin && !fptAudits) {
+                    throw '您无权查看该数据';
+                }
+                financialPay.filePermission = financialPay.permission.pay_file ||
+                    (financialPay.status === status.checking && auditorIds.indexOf(accountId) !== -1) ||
+                    ((financialPay.status === status.uncheck || financialPay.status === status.checkNo) && accountId === financialPay.uid);
+            } else { // 其他不可见
+                throw '您无权查看该数据';
+            }
+            financialPay.filePermission = financialPay.filePermission ? true : financialPay.permission.pay_file;
+            // 调差的readOnly 指表格和页面只能看不能改,和审批无关
+            // financialPay.readOnly = true;
+            financialPay.readOnly = !((financialPay.status === status.uncheck || financialPay.status === status.checkNo) && accountId === financialPay.uid);
+            financialPay.shenpiPower = financialPay.status === status.checking && financialPay.curAuditorIds.indexOf(accountId) !== -1;
+            this.financialPay = financialPay;
+            yield next;
+        } catch (err) {
+            // 输出错误到日志
+            if (err.stack) {
+                this.logger.error(err);
+            } else {
+                this.session.message = {
+                    type: messageType.ERROR,
+                    icon: 'exclamation-circle',
+                    message: err,
+                };
+                this.getLogger('fail').info(JSON.stringify({
+                    error: err,
+                    project: this.session.sessionProject,
+                    user: this.session.sessionUser,
+                    body: this.session.body,
+                }));
+            }
+            if (this.helper.isAjax(this.request)) {
+                if (err.stack) {
+                    this.body = { err: 4, msg: '标段数据未知错误', data: null };
+                } else {
+                    this.body = { err: 3, msg: err.toString(), data: null };
+                }
+            } else {
+                if (this.helper.isWap(this.request)) {
+                    this.redirect('/wap/list');
+                } else {
+                    this.postError(err, '未知错误');
+                    err === '该功能已关闭或无法查看' ? this.redirect('/dashboard') : this.request.headers.referer ? this.redirect(this.request.headers.referer) : this.redirect('/financial');
+                }
+            }
+        }
+    };
+};

+ 6 - 0
app/public/css/main.css

@@ -1888,6 +1888,9 @@ overflow-y: auto;
 .bg-new-payment{
     background: rgba(128, 128, 0, 0.08) !important;
 }
+.bg-new-financial{
+    background: rgba(58, 88, 50, 0.08) !important;
+}
 .text-new-advance{
   color: rgba(241, 82, 91, 1) !important;
 }
@@ -1918,6 +1921,9 @@ overflow-y: auto;
 .text-new-payment{
     color: rgba(128, 128, 0, 1) !important;
 }
+.text-new-financial{
+    color: rgba(58, 88, 50, 1) !important;
+}
 .text-width{
   width: 66px;
   text-align: center;

+ 54 - 2
app/public/js/budget_detail.js

@@ -477,7 +477,48 @@ $(document).ready(() => {
                 self.refreshOperationValid(sheet);
                 removeLocalCache(copyBlockTag);
             }, null, true);
-        }
+        },
+        sortCode: function (sheet) {
+            const tree = sheet.zh_tree;
+            if (!tree) return;
+
+            const select = SpreadJsObj.getSelectObject(sheet);
+            if (!select || !select.code) return;
+
+            const getChildSort = function (i, split) {
+                if (i <= 0) throw '参数错误';
+                switch(split) {
+                    case '0':
+                        return i < 10 ? split + i : i + '';
+                    case '-':
+                    default:
+                        return split + i;
+                }
+            };
+            const recursiveSortCode = function (data, parentCode, children, split) {
+                if (!children || children.length === 0) return;
+
+                for (const [i, child] of children.entries()) {
+                    if (!child.b_code || child.b_code === '') {
+                        const code = parentCode + getChildSort(i + 1, split);
+                        const cData = tree.getNodeKeyData(child);
+                        cData.code = code;
+                        data.push(cData);
+                        if (!tree.isLeafXmj(child)) {
+                            recursiveSortCode(data, code, child.children, split);
+                        }
+                    }
+                }
+            };
+            const data = [];
+            recursiveSortCode(data, select.code, select.children, tree.getCodeSplit());
+            if (data.length > 0) {
+                postData(window.location.pathname + '/update', {postType: 'update', postData: data}, function (result) {
+                    const refreshNode = tree.loadPostData(result);
+                    budgetTreeOpr.refreshTree(sheet, refreshNode);
+                })
+            }
+        },
     };
     budgetSpread.bind(spreadNS.Events.SelectionChanged, budgetTreeOpr.selectionChanged);
     if (!readOnly) {
@@ -582,7 +623,18 @@ $(document).ready(() => {
                     visible: function (key, opt) {
                         return !readOnly;
                     }
-                }
+                },
+                sortChildren: {
+                    name: '顺序重排子项编号',
+                    icon: 'fa-sort-numeric-asc',
+                    disabled: function (key, opt) {
+                        const node = SpreadJsObj.getSelectObject(budgetSheet);
+                        return !node || !node.code || !node.children || node.children === 0 || node.code.length < 3;
+                    },
+                    callback: function (key, opt) {
+                        budgetTreeOpr.sortCode(budgetSheet);
+                    },
+                },
             }
         });
     }

+ 85 - 85
app/public/js/contract_detail.js

@@ -1018,90 +1018,90 @@ $(document).ready(function() {
         }
     };
     contractContextMenuOptions.items.sprBase = '----';
-    contractContextMenuOptions.items.copyBlock = {
-        name: '复制整块(只复制节点)',
-        icon: 'fa-files-o',
-        callback: function (key, opt) {
-            const copyBlockList = [];
-            const sheet = contractSheet;
-            const sel = sheet.getSelections()[0];
-            let iRow = sel.row;
-            const pid = sheet.zh_tree.nodes[iRow].contract_pid;
-            while (iRow < sel.row + sel.rowCount) {
-                const node = sheet.zh_tree.nodes[iRow];
-                if (node.contract_pid !== pid) {
-                    toastr.error('仅可同时选中同层节点');
-                    return;
-                }
-                const posterity = sheet.zh_tree.getPosterity(node);
-                iRow += posterity.length + 1;
-                posterity.unshift(node);
-                const copyData = sheet.zh_tree.getDefaultData(posterity);
-                const newCopyData = _.filter(_.cloneDeep(copyData), function (d) {
-                    return d.c_code === undefined;
-                });
-                const newCopyBlock = [];
-                for (const p of newCopyData) {
-                    const children = _.filter(newCopyData, function (item) {
-                        return item.contract_pid === p.contract_id;
-                    });
-                    if (children.length > 0) {
-                        let i = 1;
-                        for (const c of children) {
-                            c.order = i;
-                            i++;
-                        }
-                    } else {
-                        p.is_leaf = 1;
-                    }
-                    if (_.findIndex(newCopyBlock, { contract_id: p.contract_id }) === -1) {
-                        newCopyBlock.push(p);
-                    }
-                }
-                copyBlockList.push(newCopyBlock);
-            }
-            setLocalCache(copyBlockTag, JSON.stringify({ block: copyBlockList }));
-        },
-        visible: function (key, opt) {
-            const sheet = contractSheet;
-            const selection = sheet.getSelections();
-            const row = selection[0].row;
-            const select = contractTree.nodes[row];
-            return select;
-        },
-        disabled: function (key, opt) {
-            const sheet = contractSheet;
-            const selection = sheet.getSelections();
-            const row = selection[0].row;
-            const select = contractTree.nodes[row];
-            console.log(select);
-            return !(select && select.level > 1 && !select.c_code);
-        }
-    };
-    if (permission_edit) {
-        contractContextMenuOptions.items.pasteBlock = {
-            name: '粘贴整块',
-            icon: 'fa-clipboard',
-            disabled: function (key, opt) {
-                //const block = treeOperationObj.block || [];
-                const copyInfo = JSON.parse(getLocalCache(copyBlockTag));
-                return !(copyInfo && copyInfo.block && copyInfo.block.length > 0);
-            },
-            callback: function (key, opt) {
-                //const block = treeOperationObj.block || [];
-                const copyInfo = JSON.parse(getLocalCache(copyBlockTag));
-                if (copyInfo.block.length > 0) {
-                    contractTreeSpreadObj.pasteBlock(contractSpread, copyInfo);
-                } else {
-                    document.execCommand('paste');
-                }
-            },
-            visible: function (key, opt) {
-                return permission_edit;
-            }
-        };
-        contractContextMenuOptions.items.sprBlock = '----';
-    }
+    // contractContextMenuOptions.items.copyBlock = {
+    //     name: '复制整块(只复制节点)',
+    //     icon: 'fa-files-o',
+    //     callback: function (key, opt) {
+    //         const copyBlockList = [];
+    //         const sheet = contractSheet;
+    //         const sel = sheet.getSelections()[0];
+    //         let iRow = sel.row;
+    //         const pid = sheet.zh_tree.nodes[iRow].contract_pid;
+    //         while (iRow < sel.row + sel.rowCount) {
+    //             const node = sheet.zh_tree.nodes[iRow];
+    //             if (node.contract_pid !== pid) {
+    //                 toastr.error('仅可同时选中同层节点');
+    //                 return;
+    //             }
+    //             const posterity = sheet.zh_tree.getPosterity(node);
+    //             iRow += posterity.length + 1;
+    //             posterity.unshift(node);
+    //             const copyData = sheet.zh_tree.getDefaultData(posterity);
+    //             const newCopyData = _.filter(_.cloneDeep(copyData), function (d) {
+    //                 return d.c_code === undefined;
+    //             });
+    //             const newCopyBlock = [];
+    //             for (const p of newCopyData) {
+    //                 const children = _.filter(newCopyData, function (item) {
+    //                     return item.contract_pid === p.contract_id;
+    //                 });
+    //                 if (children.length > 0) {
+    //                     let i = 1;
+    //                     for (const c of children) {
+    //                         c.order = i;
+    //                         i++;
+    //                     }
+    //                 } else {
+    //                     p.is_leaf = 1;
+    //                 }
+    //                 if (_.findIndex(newCopyBlock, { contract_id: p.contract_id }) === -1) {
+    //                     newCopyBlock.push(p);
+    //                 }
+    //             }
+    //             copyBlockList.push(newCopyBlock);
+    //         }
+    //         setLocalCache(copyBlockTag, JSON.stringify({ block: copyBlockList }));
+    //     },
+    //     visible: function (key, opt) {
+    //         const sheet = contractSheet;
+    //         const selection = sheet.getSelections();
+    //         const row = selection[0].row;
+    //         const select = contractTree.nodes[row];
+    //         return select;
+    //     },
+    //     disabled: function (key, opt) {
+    //         const sheet = contractSheet;
+    //         const selection = sheet.getSelections();
+    //         const row = selection[0].row;
+    //         const select = contractTree.nodes[row];
+    //         console.log(select);
+    //         return !(select && select.level > 1 && !select.c_code);
+    //     }
+    // };
+    // if (permission_edit) {
+    //     contractContextMenuOptions.items.pasteBlock = {
+    //         name: '粘贴整块',
+    //         icon: 'fa-clipboard',
+    //         disabled: function (key, opt) {
+    //             //const block = treeOperationObj.block || [];
+    //             const copyInfo = JSON.parse(getLocalCache(copyBlockTag));
+    //             return !(copyInfo && copyInfo.block && copyInfo.block.length > 0);
+    //         },
+    //         callback: function (key, opt) {
+    //             //const block = treeOperationObj.block || [];
+    //             const copyInfo = JSON.parse(getLocalCache(copyBlockTag));
+    //             if (copyInfo.block.length > 0) {
+    //                 contractTreeSpreadObj.pasteBlock(contractSpread, copyInfo);
+    //             } else {
+    //                 document.execCommand('paste');
+    //             }
+    //         },
+    //         visible: function (key, opt) {
+    //             return permission_edit;
+    //         }
+    //     };
+    //     contractContextMenuOptions.items.sprBlock = '----';
+    // }
     if (permission_edit) {
         contractContextMenuOptions.items.batchInsert = {
             name: '批量插入',
@@ -1132,8 +1132,8 @@ $(document).ready(function() {
                 return !(valid && first && first.level > 1);
             }
         };
+        contractContextMenuOptions.items.sprTag = '----';
     }
-    contractContextMenuOptions.items.sprTag = '----';
     contractContextMenuOptions.items.showLast = {
         name: '显示至最底层',
         callback: function (key, opt, menu, e) {

+ 179 - 0
app/public/js/financial_index.js

@@ -0,0 +1,179 @@
+$(document).ready(function() {
+    autoFlashHeight();
+    const projectTreeObj = (function(setting){
+        const ProjectTree = createDragTree(setting.treeSetting);
+        ProjectTree.loadDatas(setting.source);
+        const TableObj = $(setting.table);
+        let tenderTreeShowLevel;
+
+        const Utils = {
+            getRowTdHtml: function (node, tree) {
+                const html = [];
+                // 名称
+                html.push('<td width="20%" class="in-' + node.tree_level + '">');
+                if (node.is_folder) {
+                    if (node.children.length > 0) {
+                        html.push('<span onselectstart="return false" style="{-moz-user-select:none}" class="fold-switch mr-1" title="收起" id="' + node.id + '"><i class="fa fa-minus-square-o"></i></span> <i class="fa fa-folder-o"></i> ', node.name);
+                    } else {
+                        html.push('<i class="fa fa-folder-o"></i> ', node.name);
+                    }
+                } else {
+                    html.push(`<span class="text-muted mr-2">${tree.isLastSibling(node) ? '└' : '├'}</span>`);
+                    html.push('<a href="/financial/' + node.id + '/' + node.toUrl + '">', node.name, '</a>');
+                }
+                html.push('</td>');
+                // 创建时间
+                if (node.is_folder) {
+                    html.push(`<td class="text-center"></td>`);
+                } else {
+                    html.push(`<td class="text-center">${moment(node.create_time).format('YYYY-MM-DD')}</td>`);
+                }
+                if (is_admin) {
+                    if (!node.is_folder) {
+                        html.push(`<td class="text-center"><a href="javascript:void(0);" data-spid="${node.id}" class="btn btn-outline-primary btn-sm get-audits">成员管理</a></td>`);
+                    } else {
+                        html.push(`<td class="text-center"></td>`);
+                    }
+                }
+                return html.join('');
+            },
+            getNodeTrHtml: function (node, tree) {
+                const html = [];
+                html.push(`<tr tree_id="${node.id}" draggable="true">`);
+                html.push(Utils.getRowTdHtml(node, tree));
+                html.push(`</tr>`);
+                return html.join('');
+            },
+            reloadTable: function () {
+                const html = [];
+                for (const node of ProjectTree.nodes) {
+                    html.push(Utils.getNodeTrHtml(node, ProjectTree));
+                }
+                TableObj.html(html.join(''));
+            },
+            getSelectNode: function() {
+                const selectId = $('tr.table-active').attr('tree_id');
+                return selectId ? ProjectTree.getItems(selectId) : null;
+            },
+            getSelectNodeId: function() {
+                const selectId = $('tr.table-active').attr('tree_id');
+                return selectId || setting.treeSetting.rootId;
+            },
+            refreshTreeTable: function(result) {
+                ProjectTree.loadDatas(result);
+                if (ProjectTree.nodes.length > 0 && $('#no-project').length > 0) window.location.reload();
+                Utils.reloadTable();
+            },
+            refreshRow: function(result) {
+                const refreshData = ProjectTree.loadPostData(result);
+                if (!refreshData.update) return;
+                for (const u of refreshData.update) {
+                    $(`tr[tree_id=${u.id}]`).html(Utils.getRowTdHtml(u, ProjectTree));
+                }
+            },
+            expandByLevel: function(level){
+                ProjectTree.expandByLevel(level);
+                for (const node of ProjectTree.nodes) {
+                    const tr = $(`tr[tree_id=${node.id}]`);
+                    if (node.expanded) {
+                        $('.fold-switch', tr).html(`<i class="fa fa-minus-square-o"></i>`);
+                    } else {
+                        $('.fold-switch', tr).html(`<i class="fa fa-plus-square-o"></i>`);
+                    }
+                    if (node.visible) {
+                        tr.show();
+                    } else {
+                        tr.hide();
+                    }
+                }
+            }
+        };
+        Utils.reloadTable();
+        $('body').on('click', 'tr[tree_id]', function() {
+            if ($(this).hasClass('table-active')) {
+                $(this).removeClass('table-active');
+            } else {
+                $('tr[tree_id].table-active').removeClass('table-active');
+                $(this).addClass('table-active');
+            }
+            // Utils.refreshAddButton();
+        });
+        $('body').on('dragstart', 'tr[tree_id]', function(e) {
+            Utils.dragNode = ProjectTree.getItems(e.target.getAttribute('tree_id'));
+        });
+        $('body').on('drop', 'tr[tree_id]', function(e) {
+            Utils.dropNode = ProjectTree.getItems(e.currentTarget.getAttribute('tree_id'));
+            // Utils.dropTo();
+        });
+        $('body').on('dragover', 'tr[tree_id]', function(e) {
+            const parent = ProjectTree.getItems(e.currentTarget.getAttribute('tree_id'));
+            return !parent || !parent.is_folder || parent.tree_level > 3 || parent.id === Utils.dragNode.id;
+        });
+        $('body').on('click', '.fold-switch', function() {
+            const id = this.getAttribute('id');
+            const node = ProjectTree.getItems(id);
+            ProjectTree.setExpanded(node, !node.expanded);
+            const posterity = ProjectTree.getPosterity(node);
+            if (node.expanded) {
+                $(this).html(`<i class="fa fa-minus-square-o"></i>`);
+            } else {
+                $(this).html(`<i class="fa fa-plus-square-o"></i>`);
+            }
+            for (const p of posterity) {
+                if (p.visible) {
+                    $(`tr[tree_id=${p.id}]`).show();
+                } else {
+                    $(`tr[tree_id=${p.id}]`).hide();
+                }
+            }
+        });
+
+        const getChildrenLevel = function (node) {
+            let iLevel = node.tree_level || 1;
+            if (node.children && node.children.length > 0) {
+                for (const c of node.children) {
+                    iLevel = Math.max(iLevel, getChildrenLevel(c));
+                }
+            }
+            return iLevel;
+        };
+        tenderTreeShowLevel = $.cs_showLevel({
+            selector: '#show-level',
+            levels: [
+                {
+                    type: 'sort', count: 5, visible_count: function () {
+                        return ProjectTree.children.map(getChildrenLevel).reduce((x, y) => { return Math.max(x, y); }, 0) - 1;
+                    }
+                },
+                {
+                    type: 'last', title: '最底层', visible: function () {
+                        const count = ProjectTree.children.map(getChildrenLevel).reduce((x, y) => { return Math.max(x, y); }, 0) - 1;
+                        return count > 0;
+                    }
+                },
+            ],
+            showLevel: function (tag) {
+                switch (tag) {
+                    case "1":
+                    case "2":
+                    case "3":
+                    case "4":
+                    case "5":
+                        Utils.expandByLevel(parseInt(tag));
+                        break;
+                    case "last":
+                        Utils.expandByLevel(20);
+                        break;
+                    default: return;
+                }
+            }
+        });
+        tenderTreeShowLevel.initShowLevel();
+        tenderTreeShowLevel.refreshMenuVisible();
+        return { ProjectTree, TableObj, ...Utils };
+    })({
+        treeSetting: { id: 'id', pid: 'tree_pid', level: 'tree_level', order: 'tree_order', rootId: '-1' },
+        source: projectList,
+        table: '#projectList',
+    });
+});

+ 929 - 0
app/public/js/financial_pay.js

@@ -0,0 +1,929 @@
+let auditUtils;
+$(function () {
+    autoFlashHeight();
+
+    $('#tid_select').change(function () {
+        const tid = parseInt($(this).val()) || 0;
+        setSelectValue('tid', tid);
+    });
+
+    $('#status_select .to-log-link').click(function () {
+        const status = parseInt($(this).data('val')) || null;
+        setSelectValue('status', status);
+    });
+
+    $('#used_select .to-log-link').click(function () {
+        const used = $(this).data('val') || null;
+        setSelectValue('used', used);
+    });
+
+    function setSelectValue(select, value) {
+        const routes = [];
+        const tid = select === 'tid' ? value : $('#tid_select').val();
+        if (tid) {
+            routes.push('tid=' + tid);
+        }
+        const status = select === 'status' ? value : $('#status_selected').data('value');
+        if (status) {
+            routes.push('status=' + status);
+        }
+        const used = select === 'used' ? value : $('#used_selected').data('value');
+        if (used) {
+            routes.push('used=' + used);
+        }
+        window.location.href = '/financial/' + spid + '/pay' + (routes.length ? '?' + routes.join('&') : '');
+    }
+
+    let timer = null
+    let oldSearchVal = null
+    $('#liucheng').on('input propertychange', '.gr-search', function(e) {
+        oldSearchVal = e.target.value;
+        timer && clearTimeout(timer);
+        timer = setTimeout(() => {
+            const newVal = $(this).val();
+            const code = $(this).attr('data-code');
+            let html = '';
+            if (newVal && newVal === oldSearchVal) {
+                accountList.filter(item => item && (item.name.indexOf(newVal) !== -1 || (item.mobile && item.mobile.indexOf(newVal) !== -1))).forEach(item => {
+                    html += `<dd class="border-bottom p-2 mb-0 " data-id="${item.id}" >
+                        <p class="mb-0 d-flex"><span class="text-primary">${item.name}</span><span
+                                class="ml-auto">${item.mobile || ''}</span></p>
+                        <span class="text-muted">${item.role || ''}</span>
+                    </dd>`
+                });
+                $('#' + code + '_dropdownMenu .book-list').empty();
+                $('#' + code + '_dropdownMenu .book-list').append(html);
+            } else {
+                if (!$('#' + code + '_dropdownMenu .acc-btn').length) {
+                    accountGroup.forEach((group, idx) => {
+                        if (!group) return;
+                        html += `<dt><a href="javascript: void(0);" class="acc-btn" data-groupid="${idx}" data-type="hide"><i class="fa fa-plus-square"></i>
+                        </a> ${group.groupName}</dt>
+                        <div class="dd-content" data-toggleid="${idx}">`;
+                        group.groupList.forEach(item => {
+                            html += `<dd class="border-bottom p-2 mb-0 " data-id="${item.id}" >
+                                <p class="mb-0 d-flex"><span class="text-primary">${item.name}</span><span
+                                        class="ml-auto">${item.mobile || ''}</span></p>
+                                <span class="text-muted">${item.role || ''}</span>
+                            </dd>`;
+                        });
+                        html += '</div>';
+                    });
+                    $('#' + code + '_dropdownMenu .book-list').empty();
+                    $('#' + code + '_dropdownMenu .book-list').append(html);
+                }
+            }
+        }, 400);
+    });
+
+    $('#liucheng').on('show.bs.modal', function (e) {
+        $('#shenpi-tender-list tr').removeClass('bg-warning');
+        if (tenders.length > 0) {
+            $('#shenpi-tender-list tr').eq(0).addClass('bg-warning');
+            auditUtils. makeReportListHtml(tenders[0]);
+            auditUtils.makeShenpiListHtml(tenders[0]);
+        }
+    });
+
+    $('#shenpi-tender-list').on('click', '.change-tender', function () {
+        if ($(this).hasClass('bg-warning')) {
+            return;
+        }
+        $('#shenpi-tender-list tr').removeClass('bg-warning');
+        $(this).parents('tr').addClass('bg-warning');
+        const tid = parseInt($(this).parents('tr').data('tid')) || 0;
+        const tender = tenders.find(t => t.id === tid);
+        if (!tender) {
+            toastr.error('请选择标段');
+            return;
+        }
+        auditUtils.makeReportListHtml(tender);
+        auditUtils.makeShenpiListHtml(tender);
+    });
+    auditUtils = {
+        makeReportListHtml: function (flow) {
+            let addHtml = '';
+            $('#select-all-ptAudits').prop('checked', false);
+            for (const pl of flow.permissionList) {
+                addHtml += `<tr>
+                                <td class="text-center"><input type="checkbox" class="select-ptAudit" data-id="${pl.id}"></td><td>${pl.name}</td><td>${pl.company}</td>
+                                <td class="text-center"><input type="checkbox" class="save-report" data-id="${pl.id}" ${pl.is_report ? 'checked' : ''}></td><td class="text-center"><a href="javascript:void(0);" class="text-danger remove-audit" data-id="${pl.id}">移除</a></td>
+                            </tr>`;
+            }
+            $('#report-list').html(addHtml);
+        },
+        makeShenpiListHtml: function (flow) {
+            let addhtml = '<ul class="list-unstyled">\n';
+            addhtml += this.getgdsplHtml(flow, 'financial');
+            addhtml += '</ul>\n';
+            $('#shenpi-list').html(addhtml);
+        },
+        getAuditHtml: function(audit, is_report = 0) {
+            return '<span class="d-inline-block"><span class="badge badge-light">'+ audit.name +' <span class="dropdown">\n' +
+                '                                                            <a href="javascript:void(0);" class="btn-sm text-danger px-1" title="移除" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"><i class="fa fa-remove"></i></a>\n' +
+                '                                                            <div class="dropdown-menu">\n' +
+                '                                                                <a class="dropdown-item" href="javascript:void(0);">确认移除' + ( is_report ? '填报人' : '审批人') + '?</a>\n' +
+                '                                                                <div class="dropdown-divider"></div>\n' +
+                '                                                                <div class="px-2 py-1 text-center">\n' +
+                '                                                                    <button class="remove-audit btn btn-sm btn-danger" data-id="' + (is_report ? audit.id : audit.audit_id) + '">移除</button>\n' +
+                '                                                                    <button class="btn btn-sm btn-secondary">取消</button>\n' +
+                '                                                                </div>\n' +
+                '                                                            </div>\n' +
+                '                                                        </span> ' +
+                '                                            </span></span>\n'
+        },
+        getAuditTypeHtml: function(code, type) {
+            const html = [];
+            html.push(`<span class="d-inline-block"><select class="form-control form-control-sm audit-type-key" data-type="${type}">`);
+            for (const t of auditType.types) {
+                if (t.valid && t.valid.indexOf(code) < 0) continue;
+                html.push(`<option value="${t.value}" ${t.value === type ? 'selected' : ''}>${t.name}</option>`);
+            }
+            html.push('</select></span> ');
+            return html.join('');
+        },
+        getSelectAuditHtml: function (code, is_report = 0) {
+            let divhtml = '';
+            accountGroup.forEach((group, idx) => {
+                let didivhtml = '';
+                if(group) {
+                    group.groupList.forEach(item => {
+                        didivhtml += '<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';
+                }
+            });
+            const html =
+                '                                            <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' +
+                '                                                        ' + (is_report ? '添加填报人' : '选择审批人') + '\n' +
+                '                                                    </button>\n' +
+                '                                                    <div class="dropdown-menu dropdown-menu-right" id="' + code + '_dropdownMenu" aria-labelledby="' + code + '_dropdownMenuButton" style="width:220px">\n' +
+                '                                                        <div class="mb-2 p-2"><input class="form-control form-control-sm gr-search"\n' +
+                '                                                                                     placeholder="姓名/手机 检索" autocomplete="off" data-code="' + code + '"></div>\n' +
+                '                                                        <dl class="list-unstyled book-list">\n' + divhtml +
+                '                                                        </dl>\n' +
+                '                                                    </div>\n' +
+                '                                                </div>\n' +
+                '                                            </span>\n';
+            return html;
+        },
+        // 以下i从1开始
+        getAuditGroupInnerHtml: function(code, auditGroup, i) {
+            const html = [];
+            const type = auditGroup.length > 0 ? auditGroup[0].audit_type : auditType.key.common;
+            html.push(`<span class="col-auto">${transFormToChinese(i)}审</span><span class="col-10 spr-span">`);
+            html.push(this.getAuditTypeHtml(code, type));
+            for (const audit of auditGroup) {
+                html.push(this.getAuditHtml(audit));
+            }
+            if (type !== auditType.key.common || auditGroup.length === 0) {
+                html.push(this.getSelectAuditHtml(code));
+            }
+            if (type === auditType.key.union && auditGroup.length > 0) {
+                html.push(`<button class="btn btn-sm btn-outline-primary" sp_type="${code}" audit_order="${i}" name="union-set">协同设置</button>`);
+            }
+            html.push('</span>');
+            return html.join('');
+        },
+        getAuditGroupHtml: function (code, auditGroup, i) {
+            return `<li class="d-flex justify-content-start align-items-center mb-3">${this.getAuditGroupInnerHtml(code, auditGroup, i)}</li>`;
+        },
+        getgdsplHtml(flow, this_code) {
+            let addhtml = '';
+            if (flow.auditGroupList.length !== 0) {
+                for(const [i, auditGroup] of flow.auditGroupList.entries()) {
+                    addhtml += this.getAuditGroupHtml(this_code, auditGroup, i + 1);
+                }
+                const addGroupHtml = this_code === 'change' && (!flow.groupList || (flow.groupList && flow.groupList.length === 0)) ?
+                    `<span class="pl-3"><a href="javascript:void(0);" class="show-spzsave" data-code="${this_code}"><i class="fa fa-save"></i> 存为审批组</a></span>\n` : '';
+                addhtml += '<li>\n' +
+                    '                                            <span class="pl-3"><a href="javascript:void(0);" class="add-audit" ><i class="fa fa-plus"></i> 添加流程</a></span>\n' + addGroupHtml +
+                    '                                        </li>';
+            } else {
+                addhtml += this.getAuditGroupHtml(this_code, [], 1);
+            }
+            return addhtml;
+        },
+        // 以下i从0开始
+        addAudit: function (tender, user, i) {
+            const flow = tender;
+            if (!flow.auditGroupList) flow.auditGroupList = [];
+            if (!flow.auditGroupList[i]) flow.auditGroupList[i] = [];
+            flow.auditGroupList[i].push(user);
+            return flow.auditGroupList[i];
+        },
+        removeAudit: function (tender, audit_id, i) {
+            const flow = tender;
+            if (flow.auditGroupList[i].length === 1) {
+                flow.auditGroupList.splice(i, 1);
+                return null;
+            }
+            flow.auditGroupList[i].splice(flow.auditGroupList[i].findIndex(x => { return x.audit_id === audit_id; }), 1);
+            return flow.auditGroupList[i];
+        },
+        setAuditType: function (tender, audit_type, i) {
+            const flow = tender;
+            if (!flow || !flow.auditGroupList || !flow.auditGroupList[i]) return;
+            flow.auditGroupList[i].forEach(x => { x.audit_type = audit_type});
+            return flow.auditGroupList[i];
+        }
+    };
+
+    // 选中填报人
+    $('body').on('click', 'div[id="report_audit_dropdownMenu"] dl dd', function () {
+        const tid = parseInt($('#shenpi-tender-list tr.bg-warning').data('tid'));
+        const tender = tenders.find(t => t.id === tid);
+        if (!tender) {
+            toastr.error('请选择标段');
+            return;
+        }
+        const id = parseInt($(this).data('id'));
+        if (!id) return;
+        if (!isNaN(id) && id !== 0) {
+            postData('/financial/'+ spid + '/pay/save', {type: 'add-tender-audit', id: id, tid: tender.id }, function (result) {
+                tender.permissionList = result;
+                auditUtils.makeReportListHtml(tender);
+            })
+        }
+    });
+
+    // 移除填报人
+    $('body').on('click', '#report-list .remove-audit', function () {
+        const tid = parseInt($('#shenpi-tender-list tr.bg-warning').data('tid'));
+        const tender = tenders.find(t => t.id === tid);
+        if (!tender) {
+            toastr.error('请选择标段');
+            return;
+        }
+        const id = parseInt($(this).data('id'));
+        deleteAfterHint(function () {
+            postData('/financial/'+ spid + '/pay/save', {type: 'del-tender-audit', id, tid: tender.id }, function (result) {
+                tender.permissionList = result.permissionList;
+                tender.auditGroupList = result.auditGroupList;
+                auditUtils.makeReportListHtml(tender);
+                auditUtils.makeShenpiListHtml(tender);
+            });
+        }, '确认删除该标段用户?');
+    });
+
+    // 勾选是否为填报人
+    $('body').on('click', '#report-list .save-report', function () {
+        const tid = parseInt($('#shenpi-tender-list tr.bg-warning').data('tid'));
+        const tender = tenders.find(t => t.id === tid);
+        if (!tender) {
+            toastr.error('请选择标段');
+            return;
+        }
+        const id = parseInt($(this).data('id'));
+        const isReport = $(this).prop('checked');
+        const permission = tender.permissionList.find(p => p.id === id);
+        if (!permission) {
+            toastr.error('该用户不存在');
+            return;
+        }
+        postData('/financial/'+ spid + '/pay/save', {type: 'save-permission', updateData: { id, is_report: isReport } }, function (result) {
+            permission.is_report = isReport;
+        });
+    });
+
+    $('#select-all-ptAudits').click(function () {
+        $('#report-list .select-ptAudit').prop('checked', $(this).prop('checked'));
+    });
+
+    $('#batch-del-ptAudit').click(function () {
+        const tid = parseInt($('#shenpi-tender-list tr.bg-warning').data('tid'));
+        const tender = tenders.find(t => t.id === tid);
+        if (!tender) {
+            toastr.error('请选择标段');
+            return;
+        }
+        const ids = [];
+        $('#report-list .select-ptAudit:checked').each(function () {
+            ids.push(parseInt($(this).data('id')));
+        });
+        if (ids.length === 0) {
+            toastr.warning('请勾选要删除的用户');
+            return;
+        }
+        deleteAfterHint(function () {
+            postData('/financial/'+ spid + '/pay/save', {type: 'del-tender-audit', id: ids, tid: tender.id }, function (result) {
+                tender.permissionList = result.permissionList;
+                tender.auditGroupList = result.auditGroupList;
+                auditUtils.makeReportListHtml(tender);
+                auditUtils.makeShenpiListHtml(tender);
+            });
+        }, '确认删除已勾选的标段用户?');
+    });
+
+    $('#batch-other-ptAudit').click(function () {
+        const cur_tenderid = parseInt($('#shenpi-tender-list tr.bg-warning').data('tid'));
+        const tender = tenders.find(t => t.id === cur_tenderid);
+        if (!tender) {
+            toastr.error('请选择标段');
+            return;
+        }
+        const ids = [];
+        $('#report-list .select-ptAudit:checked').each(function () {
+            ids.push(parseInt($(this).data('id')));
+        });
+        if (ids.length === 0) {
+            toastr.warning('请勾选要同步的用户');
+            return;
+        }
+        const num = $('#shenpi-tender-list input:checked').length;
+        if (num === 0 || (num === 1 && parseInt($('#shenpi-tender-list input:checked').eq(0).parents('tr').data('tid')) === cur_tenderid)) {
+            toastr.warning('请选择需要设置审批同步的标段');
+            return;
+        }
+        const tenderList = [];
+        for (let i = 0; i < num; i++) {
+            const tid = parseInt($('#shenpi-tender-list input:checked').eq(i).parents('tr').data('tid'));
+            if (tid !== cur_tenderid) {
+                tenderList.push(tid);
+            }
+        }
+        const data = {
+            type: 'copy-tender-audit',
+            id: ids,
+            this_tid: tender.id,
+        }
+        data.tidList = tenderList.join(',');
+        postData('/financial/'+ spid + '/pay/save',  data, function (result) {
+            toastr.success('已同步到其它勾选标段');
+            for (let i = 0; i < num; i++) {
+                const tid = parseInt($('#shenpi-tender-list input:checked').eq(i).parents('tr').data('tid'));
+                if (tid !== cur_tenderid) {
+                    const other_tender = tenders.find(t => t.id === tid);
+                    const permissionList = _.filter(result.otherPermissionList, { tid: tid });
+                    other_tender.permissionList = permissionList;
+                }
+            }
+        });
+    });
+
+    // 选中审批人
+    $('body').on('click', '#shenpi-list div[id$="_dropdownMenu"] dl dd', function () {
+        const tid = parseInt($('#shenpi-tender-list tr.bg-warning').data('tid'));
+        const tender = tenders.find(t => t.id === tid);
+        if (!tender) {
+            toastr.error('请选择标段');
+            return;
+        }
+        const id = parseInt($(this).data('id'));
+        if (!id) return;
+
+        let this_code = 'financial';
+        const user = _.find(accountList, function (item) {
+            return item.id === id;
+        });
+        // 判断是否已存在审批人
+        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: sp_status.gdspl,
+            code: sp_type[this_code],
+            audit_id: id,
+            type: 'add',
+            audit_type: parseInt($(this).parents('li').find('select[class*="audit-type-key"]')[0].value),
+            audit_order: $(this).parents('li').index() + 1,
+        };
+        const _self = $(this);
+        postData('/financial/' + spid + '/pay/save', { type: 'add-shenpi-audit', shenpi: prop, tid }, function (result) {
+            const data = result.shenpi;
+            const auditGroup = auditUtils.addAudit(tender, { audit_id: data.audit_id, name: user.name, audit_type: data.audit_type, audit_order: data.audit_order }, prop.audit_order - 1);
+            if (_self.parents('ul').find('.add-audit').length === 0) {
+                _self.parents('ul').append('<li>\n' +
+                    '                                            <span class="pl-3"><a href="javascript:void(0);" class="add-audit" ><i class="fa fa-plus"></i> 添加流程</a></span>\n' +
+                    '                                        </li>');
+            }
+            _self.parents('li').html(auditUtils.getAuditGroupInnerHtml(this_code, auditGroup, prop.audit_order));
+            tender.permissionList = result.permissionList;
+            auditUtils.makeReportListHtml(tender);
+        });
+    });
+
+    // 移除审批人
+    $('body').on('click', '#shenpi-list .remove-audit', function () {
+        const tid = parseInt($('#shenpi-tender-list tr.bg-warning').data('tid'));
+        const tender = tenders.find(t => t.id === tid);
+        if (!tender) {
+            toastr.error('请选择标段');
+            return;
+        }
+        const id = parseInt($(this).data('id'));
+        const this_code = 'financial';
+        const prop = {
+            status: sp_status.gdspl,
+            code: sp_type[this_code],
+            audit_id: id,
+            type: 'del',
+        };
+        const _self = $(this);
+        postData('/tender/' + tid + '/shenpi/audit/save', prop, function (data) {
+            const index = _self.parents('li').index();
+            const auditGroup = auditUtils.removeAudit(tender, id, index);
+            if (auditGroup) {
+                _self.parents('li').html(auditUtils.getAuditGroupInnerHtml(this_code, auditGroup, index + 1));
+            } else {
+                const _selflc = _self.parents('#shenpi-list');
+                _self.parents('li').remove();
+                const aid_num = parseInt(_selflc.children('ul').find('li.d-flex').length);
+                if (aid_num === 0) {
+                    _selflc.children('ul').html(auditUtils.getAuditGroupHtml(this_code, [], 1));
+                } else {
+                    for (let i = 0; i < aid_num; i++) {
+                        _selflc.find('li.d-flex').eq(i).find('.col-auto').text(transFormToChinese(i+1) + '审');
+                    }
+                }
+            }
+        })
+    });
+
+    $('body').on('click', '#shenpi-list .add-audit', function () {
+        const num = $(this).parents('ul').children('li').length;
+        const this_code = 'financial';
+        const addhtml = auditUtils.getAuditGroupHtml(this_code, [], num);
+        $(this).parents('ul').append(addhtml);
+        $(this).parents('li').remove();
+    });
+
+    // 设置会签、或签
+    $('body').on('change', 'select[class*="audit-type-key"]', function() {
+        const tid = parseInt($('#shenpi-tender-list tr.bg-warning').data('tid'));
+        const tender = tenders.find(t => t.id === tid);
+        if (!tender) {
+            toastr.error('请选择标段');
+            return;
+        }
+        const removes = $(this).parents('.d-flex').find('.remove-audit');
+        if (removes.length === 0) return;
+
+        const this_status = sp_status.gdspl;
+        const this_code = 'financial';
+        const ids = [];
+        const liParent = $(this).parents('li');
+        removes.each((i, r) => { ids.push(parseInt(r.getAttribute('data-id'))); });
+        const prop = {
+            status: this_status,
+            code: sp_type[this_code],
+            audit_id: ids,
+            audit_type: parseInt(this.value),
+            type: 'audit-type',
+        };
+        if (prop.audit_type === auditType.key.common && ids.length > 1) {
+            toastr.warning('设置个人审批前请先删除多余的审批人');
+            this.value = this.getAttribute('data-type');
+            return;
+        }
+        const _self = this;
+        postData('/tender/'+ tid +'/shenpi/audit/save', prop, function () {
+            _self.setAttribute('data-type', _self.value);
+            const auditGroup = auditUtils.setAuditType(tender, prop.audit_type, liParent.index());
+            liParent.html(auditUtils.getAuditGroupInnerHtml(this_code, auditGroup, liParent.index() + 1));
+        });
+    });
+
+    $('#set-other-tenders').on('click', function () {
+        const cur_tenderid = parseInt($('#shenpi-tender-list tr.bg-warning').data('tid'));
+        const tender = tenders.find(t => t.id === cur_tenderid);
+        if (!tender) {
+            toastr.error('请选择标段');
+            return;
+        }
+        if (tender.auditGroupList.length === 0) {
+            toastr.warning('请先设置审批流程再同步到其它标段');
+            return;
+        }
+        const this_code = 'financial';
+        const num = $('#shenpi-tender-list input:checked').length;
+        if (num === 0 || (num === 1 && parseInt($('#shenpi-tender-list input:checked').eq(0).parents('tr').data('tid')) === cur_tenderid)) {
+            toastr.warning('请选择需要设置审批同步的标段');
+            return;
+        }
+        const data = {
+            type: 'copy-shenpi-audit',
+            status: sp_status.gdspl,
+            code: this_code,
+            this_tid: cur_tenderid,
+        };
+        // 二维数组变一维
+        data.auditList = [];
+        for (const auditGroup of tender.auditGroupList) {
+            data.auditList.push(...auditGroup);
+        }
+        const tenderList = [];
+        for (let i = 0; i < num; i++) {
+            const tid = parseInt($('#shenpi-tender-list input:checked').eq(i).parents('tr').data('tid'));
+            if (tid !== cur_tenderid) {
+                tenderList.push(tid);
+            }
+        }
+        data.tidList = tenderList.join(',');
+        console.log(data);
+        postData('/financial/' + spid + '/pay/save', data, function (result) {
+            toastr.success('已同步到其它勾选标段');
+            for (let i = 0; i < num; i++) {
+                const tid = parseInt($('#shenpi-tender-list input:checked').eq(i).parents('tr').data('tid'));
+                if (tid !== cur_tenderid) {
+                    const other_tender = tenders.find(t => t.id === tid);
+                    other_tender.auditGroupList = tender.auditGroupList;
+                    const permissionList = _.filter(result.otherPermissionList, { tid: tid });
+                    other_tender.permissionList = permissionList;
+                }
+            }
+        });
+    });
+
+    // 添加到成员中
+    $('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
+    });
+
+    $('#add-pay').on('show.bs.modal', function () {
+        let t = null;
+        if (tenders.length > 0) {
+            if (is_admin) {
+                t = tenders[0];
+            } else {
+                const filterTender = tenders.filter(t => _.includes(fptAuditTids, t.id));
+                if (filterTender.length > 0) {
+                    t = filterTender[0];
+                }
+            }
+        }
+        $('#add-pay-tender').val(t ? t.id : '');
+        changeTender(t);
+    });
+
+    $('#add-pay-tender').on('change', function () {
+        const tender = tenders.find(t => t.id === parseInt($(this).val()));
+        changeTender(tender);
+    });
+
+    $('#add-pay-btn').on('click', function () {
+        const tid = $('#add-pay-tender').val();
+        const tender = tenders.find(t => t.id === parseInt(tid));
+        if (!tender) {
+            toastr.error('请选择支付标段');
+            return;
+        }
+        const code = $('#add-pay-code').val();
+        if (!code) {
+            toastr.error('请先去标段属性,填写合同编号');
+            return;
+        }
+        const prop = {
+            tid: tender.id,
+            code: code,
+            used: $('#add-pay-used').val(),
+        };
+        postData('/financial/'+ spid + '/pay/save', { type: 'add-pay', updateData: prop }, function (result) {
+            window.location.href = '/financial/' + spid + '/pay/' + result.id + '/detail';
+        });
+    });
+
+    $('body').on('click', '#pay-list .del-pay-btn', function () {
+        const fpid = $(this).data('id');
+        deleteAfterHint(function () {
+            postData('/financial/'+ spid + '/pay/save', {type: 'del-pay', postData: { node: fpid }}, function (result) {
+                window.location.reload();
+            })
+        }, '确认删除该资金支付?');
+    });
+
+    $('#pay-list tr').on('click', function () {
+        const tid = parseInt($(this).data('tid'));
+        const tender = tenders.find(t => t.id === tid);
+        if (tid && tender) {
+            $(this).siblings().removeClass('alert-warning');
+            $(this).addClass('alert-warning');
+            $('#show-pay-account').show();
+            $('#payaccount input[name="tid"]').val(tender.id);
+        } else {
+            $(this).siblings().removeClass('alert-warning');
+            $('#show-pay-account').hide();
+            $('#payaccount input[name="tid"]').val('');
+        }
+    });
+
+    $('#payaccount').on('show.bs.modal', function () {
+        const tid = parseInt($('#payaccount input[name="tid"]').val());
+        const tender = tenders.find(t => t.id === tid);
+        $('#payaccount .modal-title').text('付款账号()');
+        $('#payaccount input[name="name"]').val('');
+        $('#payaccount input[name="bank"]').val('');
+        $('#payaccount input[name="bank_account"]').val('');
+        $('#payaccount input[name="contact"]').val('');
+        $('#payaccount input[name="phone"]').val('');
+        if (tid && tender) {
+            $('#payaccount .modal-title').text('付款账号(' + tender.name + ')');
+            $('#payaccount input[name="name"]').val(tender.pt.name);
+            $('#payaccount input[name="bank"]').val(tender.pt.bank);
+            $('#payaccount input[name="bank_account"]').val(tender.pt.bank_account);
+            $('#payaccount input[name="contact"]').val(tender.pt.contact);
+            $('#payaccount input[name="phone"]').val(tender.pt.phone);
+            if (is_admin || (fptAuditTids && _.includes(fptAuditTids, tid))) {
+                $('#payaccount table input').attr('readonly', false);
+                $('#get-form-tender').show();
+                $('#set-pay-btn').show();
+                $('#payaccount input[name="id"]').val(tender.pt.id);
+            } else {
+                $('#payaccount table input').attr('readonly', true);
+                $('#get-form-tender').hide();
+                $('#set-pay-btn').hide();
+                $('#payaccount input[name="id"]').val('');
+                $('#payaccount input[name="tid"]').val('');
+            }
+        }
+    });
+
+    $('#get-form-tender').on('click', function () {
+        const tid = $('#payaccount input[name="tid"]').val();
+        const tender = tenders.find(t => t.id === parseInt(tid));
+        if (!tid || !tender) {
+            toastr.error('标段不存在');
+            return;
+        }
+        $('#payaccount input[name="name"]').val(tender.pay_account.name);
+        $('#payaccount input[name="bank"]').val(tender.pay_account.bank);
+        $('#payaccount input[name="bank_account"]').val(tender.pay_account.account);
+        $('#payaccount input[name="contact"]').val(tender.pay_account.contact);
+        $('#payaccount input[name="phone"]').val(tender.pay_account.phone);
+        toastr.success('已同步');
+    });
+
+    $('#set-pay-btn').on('click', function () {
+        const tid = $('#payaccount input[name="tid"]').val();
+        const tender = tenders.find(t => t.id === parseInt(tid));
+        if (!tid || !tender) {
+            toastr.error('标段不存在');
+            return;
+        }
+        if (is_admin || (fptAuditTids && _.includes(fptAuditTids, tender.id))) {
+            const data = {
+                id: parseInt($('#payaccount input[name="id"]').val()),
+                tid: tender.id,
+                name: $('#payaccount input[name="name"]').val(),
+                bank: $('#payaccount input[name="bank"]').val(),
+                bank_account: $('#payaccount input[name="bank_account"]').val(),
+                contact: $('#payaccount input[name="contact"]').val(),
+                phone: $('#payaccount input[name="phone"]').val(),
+            }
+            if (!data.name) {
+                toastr.error('请填写开户名称');
+                return;
+            }
+            if (!data.bank) {
+                toastr.error('请填写开户银行');
+                return;
+            }
+            if (!data.bank_account) {
+                toastr.error('请填写开户账号');
+                return;
+            }
+            postData('/financial/'+ spid + '/pay/save', { type: 'set-pay-tender', updateData: data }, function (result) {
+                toastr.success('保存成功');
+                tender.pt = result;
+            });
+        } else {
+            toastr.error('无权限操作');
+            return;
+        }
+    });
+
+    $('#pay-list tr').eq(0).click();
+
+    function changeTender(tender) {
+        $('#add-pay-tender').val(tender ? tender.id : '');
+        if (!tender) {
+            toastr.warning('请先为项目添加标段再申请支付');
+            return;
+        }
+        if (tender.dealCode) {
+            $('#add-pay-code').val(tender.dealCode + '-' + moment().format('YYYYMMDD') + '-' + makeNum(tender.startNum));
+            $('#add-pay-code').siblings('span').text('');
+        } else {
+            $('#add-pay-code').val('');
+            $('#add-pay-code').siblings('span').html('请先去<a href="/tender/' + tender.id + '" target="_blank">标段属性</a>,填写合同编号');
+        }
+    }
+
+    function makeNum(num) {
+        let str = num.toString();
+        while (str.length < 3) {
+            str = '0' + str;
+        }
+        return str;
+    }
+
+    $('#audit-list').on('click', 'a', function() {
+        const type = $(this).data('target')
+        const auditCard = $(this).parent().parent()
+        if (type === 'show') {
+            $(this).data('target', 'hide')
+            auditCard.find('.fold-card').slideDown('swing', () => {
+                auditCard.find('#end-target').text($(this).data('idx') + '#')
+                auditCard.find('#fold-btn').text('收起历史审核记录')
+            })
+        } else {
+            $(this).data('target', 'show')
+            auditCard.find('.fold-card').slideUp('swing', () => {
+                auditCard.find('#end-target').text('1#')
+                auditCard.find('#fold-btn').text('展开历史审核记录')
+            })
+        }
+    });
+
+    // 获取审批流程
+    $('a[data-target="#sp-list" ]').on('click', function () {
+        const data = {
+            type: 'get-auditors',
+            id: $(this).attr('c-id'),
+        };
+        postData('/financial/'+ spid + '/pay/save', data, function (result) {
+            const { auditHistory, auditors2, user } = result;
+            let auditorsHTML = [];
+            auditors2.forEach((group, idx) => {
+                if (idx === 0) {
+                    auditorsHTML.push(`<li class="list-group-item d-flex justify-content-between align-items-center">
+                        <span class="mr-1"><i class="fa fa fa-play-circle fa-rotate-90"></i></span>
+                    <span class="text-muted">${getGroupAuditHtml(group)}</span>
+                    <span class="badge badge-light badge-pill ml-auto"><small>原报</small></span>
+                    </li>`);
+                } else if(idx === auditors2.length -1 && idx !== 0) {
+                    auditorsHTML.push(`<li class="list-group-item d-flex justify-content-between align-items-center">
+                        <span class="mr-1"><i class="fa fa fa-stop-circle fa-rotate-90"></i></span>
+                    <span class="text-muted">${getGroupAuditHtml(group)}</span>
+                    <div class="d-flex ml-auto">
+                    ${getAuditTypeHtml(group[0].audit_type)}
+                    <span class="badge badge-light badge-pill ml-auto"><small>终审</small></span>
+                    </div>
+                    </li>`);
+                } else {
+                    auditorsHTML.push(`<li class="list-group-item d-flex justify-content-between align-items-center">
+                        <span class="mr-1"><i class="fa fa fa-chevron-circle-down"></i></span>
+                    <span class="text-muted">${getGroupAuditHtml(group)}</span>
+                    <div class="d-flex ml-auto">
+                    ${getAuditTypeHtml(group[0].audit_type)}
+                    <span class="badge badge-light badge-pill"><small>${transFormToChinese(idx)}审</small></span>
+                    </div>
+                    </li>`);
+                }
+            });
+            $('#auditor-list').empty();
+            $('#auditor-list').append(auditorsHTML.join(''));
+
+            let historyHTML = [];
+            auditHistory.forEach((his, idx) => {
+                if (idx === auditHistory.length - 1 && auditHistory.length !== 1) {
+                    historyHTML.push(`<div class="text-right"><a href="javascript: void(0);" id="fold-btn" data-target="show">展开历史审批流程</a></div>`);
+                }
+                historyHTML.push(`<div class="${idx < auditHistory.length - 1 ? 'fold-card' : ''}">`);
+                historyHTML.push(`<div class="text-center text-muted">${idx+1}#</div>`);
+                historyHTML.push(`<ul class="timeline-list list-unstyled mt-2 ${ idx === auditHistory.length - 1 && auditHistory.length !== 1 ? 'last-auditor-list' : '' }">`);
+                his.forEach((group, index) => {
+                    if (index === 0) {
+                        historyHTML.push(`<li class="timeline-list-item pb-2">
+                                            <div class="timeline-item-date">
+                                                ${group.beginYear}
+                                                <span>${group.beginDate}</span>
+                                                <span>${group.beginTime}</span>
+                                            </div>
+                                            <div class="timeline-item-tail"></div>
+                                            <div class="timeline-item-icon bg-success text-light"><i class="fa fa-caret-down"></i></div>
+                                            <div class="timeline-item-content">
+                                                <div class="py-1">
+                                                    <span class="text-black-50">原报</span>
+                                                    <span class="pull-right text-success">${idx !== 0 ? '重新' : '' }上报审批</span>
+                                                </div>
+                                                <div class="card">
+                                                    <div class="card-body px-3 py-0">
+                                                        <div class="card-text p-2 py-3 row">
+                                                            <div class="col">
+                                                                <span class="h6">${user.name}</span>
+                                                                <span class="text-muted ml-1">${user.role}</span>
+                                                            </div>
+                                                            <div class="col">
+                                                                <span class="pull-right text-success"><i class="fa fa-check-circle"></i></span>
+                                                            </div>
+                                                        </div>
+                                                    </div>
+                                                </div>
+                                            </div>
+                                        </li>`);
+                    }
+                    historyHTML.push(`<li class="timeline-list-item pb-2 ${ group.status === auditConst.status.uncheck && idx === auditHistory.length - 1 && auditHistory.length !== 1 ? 'is_uncheck' : ''}">`);
+                    if (group.endYear) {
+                        historyHTML.push(`<div class="timeline-item-date">${group.endYear}<span>${group.endDate}</span><span>${group.endTime}</span></div>`);
+                    }
+                    if (index < his.length - 1) {
+                        historyHTML.push('<div class="timeline-item-tail"></div>');
+                    }
+                    if (group.status === auditConst.status.checked) {
+                        historyHTML.push('<div class="timeline-item-icon bg-success text-light"><i class="fa fa-check"></i></div>');
+                    } else if (group.status === auditConst.status.checkNo) {
+                        historyHTML.push('<div class="timeline-item-icon bg-warning text-light"><i class="fa fa-level-up"></i></div>');
+                    } else if (group.status === auditConst.status.checking) {
+                        historyHTML.push('<div class="timeline-item-icon bg-warning text-light"><i class="fa fa-ellipsis-h"></i></div>');
+                    } else {
+                        historyHTML.push('<div class="timeline-item-icon bg-secondary text-light"></div>');
+                    }
+
+                    historyHTML.push('<div class="timeline-item-content">');
+                    historyHTML.push('<div class="py-1">');
+                    const statuStr = group.status !== auditConst.status.uncheck ?
+                        `<span class="pull-right ${auditConst.statusClass[group.status]}">${auditConst.statusString[group.status]}</span>` : '';
+                    historyHTML.push(`
+                    <span class="text-black-50">
+                    ${ group.audit_order === 0 ? '原报' : !group.is_final ? group.audit_order + '审' : '终审' } ${getAuditTypeText(group.audit_type)}
+                    </span>
+                    ${statuStr}`);
+                    historyHTML.push('</div>');
+                    historyHTML.push('<div class="card"><div class="card-body px-3 py-0">');
+                    for (const [i, auditor] of group.auditors.entries()) {
+                        historyHTML.push(`<div class="card-text p-2 py-3 row ${ ( i > 0 ? 'border-top' : '') }">`);
+                        historyHTML.push(`<div class="col"><span class="h6">${auditor.name}</span><span class="text-muted ml-1">${auditor.role}</span></div>`);
+                        historyHTML.push('<div class="col">');
+                        if (auditor.status === auditConst.status.checked) {
+                            historyHTML.push('<span class="pull-right text-success"><i class="fa fa-check-circle"></i></span>');
+                        } else if (auditor.status === auditConst.status.checkNo) {
+                            historyHTML.push('<span class="pull-right text-warning"><i class="fa fa-share-square fa-rotate-270"></i></span>');
+                        } else if (auditor.status === auditConst.status.checking) {
+                            historyHTML.push('<span class="pull-right text-warning"><i class="fa fa-commenting"></i></span>');
+                        }
+                        historyHTML.push('</div>');
+                        if (auditor.opinion) {
+                            historyHTML.push(`<div class="col-12 py-1 bg-light"><i class="fa fa-commenting-o mr-1"></i>${auditor.opinion}</div>`);
+                        }
+                        historyHTML.push('</div>');
+                    }
+                    historyHTML.push('</div></div>');
+                    historyHTML.push('</div>');
+                    historyHTML.push('</li>');
+                });
+                historyHTML.push('</div>');
+                historyHTML.push('</ul>');
+            });
+            $('#audit-list').empty();
+            $('#audit-list').append(historyHTML.join(''));
+        });
+    });
+
+    $.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();
+        }
+    });
+});
+const getGroupAuditHtml = function (group) {
+    return group.map(u => { return `<small class="d-inline-block text-dark mx-1" title="${u.role}" data-auditorId="${u.aid}">${u.name}</small>`; }).join('');
+};
+
+const getAuditTypeHtml = function (type) {
+    if (type === auditType.key.common) return '';
+    return `<div class="li-subscript"><span class="badge badge-pill badge-${auditType.info[type].class} p-1 badge-bg-small"><small>${auditType.info[type].short}</small></span></div>`;
+};
+
+const getAuditTypeText = function (type) {
+    if (type === auditType.key.common) return '';
+    return `<span class="text-${auditType.info[type].class}">${auditType.info[type].long}</span>`;
+};

+ 733 - 0
app/public/js/financial_pay_detail.js

@@ -0,0 +1,733 @@
+$(function () {
+    autoFlashHeight();
+
+    handleFileList(fileList);
+
+    $('#file-ok').click(function () {
+        const files = Array.from($('#file-modal')[0].files)
+        const valiData = files.map(v => {
+            const ext = v.name.substring(v.name.lastIndexOf('.') + 1)
+            return {
+                size: v.size,
+                ext
+            }
+        });
+        if (validateFiles(valiData)) {
+            if (files.length) {
+                const formData = new FormData();
+                files.forEach(file => {
+                    formData.append('name', file.name);
+                    formData.append('size', file.size);
+                    formData.append('file', file);
+                })
+                postDataWithFile(preUrl + '/file/upload', formData, function (result) {
+                    handleFileList(result);
+                    $('#file-cancel').click();
+                });
+            }
+        }
+    })
+    function handleFileList(files = []) {
+        $('#file-content').empty();
+        // const { uncheck, checkNo } = auditConst.status
+        const newFiles = files.map(file => {
+            let showDel = false;
+            if (file.uid === cur_uid) {
+                if (financialPay.status === auditConst.status.checked) {
+                    showDel = false
+                } else {
+                    showDel = true
+                }
+            }
+            return {...file, showDel}
+        })
+        let html = financialPay.filePermission ? `<tr><td colspan="5"><a href="#addfujian" data-toggle="modal" class="btn btn-primary btn-sm" data-placement="bottom" title="">上传附件</a></td></tr>` : '';
+        newFiles.forEach((file, idx) => {
+            if (file.showDel) {
+                html += `<tr><td>${idx + 1}</td><td><a href="${file.filepath}" target="_blank">${file.filename}</a></td><td>${file.username}</td><td>${moment(file.upload_time).format('YYYY-MM-DD HH:mm:ss')}</td><td><a href="/financial/${file.spid}/pay/${file.fpid}/file/${file.id}/download" class="mr-2"><i class="fa fa-download"></i></a><a href="javascript: void(0);" class="text-danger file-del" data-id="${file.id}"><i class="fa fa-remove"></i></a></td></tr>`
+            } else {
+                html += `<tr><td width="70">${idx + 1}</td><td><a href="${file.filepath}" target="_blank">${file.filename}</a></td><td>${file.username}</td><td>${moment(file.upload_time).format('YYYY-MM-DD HH:mm:ss')}</td><td><a href="/financial/${file.spid}/pay/${file.fpid}/file/${file.id}/download" class="mr-2"><i class="fa fa-download"></i></a></td></tr>`
+            }
+        })
+        $('#file-content').append(html);
+    }
+
+    $('#file-content').on('click', 'a', function () {
+        if ($(this).hasClass('file-del')) {
+            const id = $(this).data('id');
+            postData(preUrl + '/file/delete', {id}, (result) => {
+                handleFileList(result);
+            })
+        }
+    });
+
+    // 回车提交
+    $('#pay-table input').on('keypress', function () {
+        if(window.event.keyCode === 13) {
+            $(this).blur();
+        }
+    });
+
+    $('#pay-table input').blur(function () {
+        const val_name = $(this).data('name');
+        let val = _.trim($(this).val()) !== '' ? _.trim($(this).val()) : null;
+        switch(val_name) {
+            default:
+                if(val && val.length > 255) {
+                    toastr.error('超出字段范围,请缩减');
+                    $(this).val(financialPay[val_name]);
+                    return false;
+                }
+                break;
+        }
+        updatePayMsg(val_name, val, $(this));
+    });
+
+    $('#pay-table textarea').blur(function () {
+        const val_name = $(this).data('name');
+        let val = _.trim($(this).val()) !== '' ? _.trim($(this).val()) : null;
+        updatePayMsg(val_name, val, $(this));
+    });
+
+    $('#pay-table select').change(function () {
+        const val_name = $(this).attr('data-name');
+        let val = _.trim($(this).val()) !== '' ? _.trim($(this).val()) : null;
+        updatePayMsg(val_name, val, $(this));
+    });
+
+    function updatePayMsg(val_name, val, _self) {
+        if(financialPay[val_name] !== val) {
+            postData(preUrl + '/save', { type: 'pay_save', name: val_name, val}, function (result) {
+                financialPay[val_name] = val;
+                _self.val(financialPay[val_name]);
+            }, function () {
+                _self.val(financialPay[val_name]);
+            })
+        } else {
+            $(this).val(financialPay[val_name]);
+        }
+    }
+
+    const payContractSpread = SpreadJsObj.createNewSpread($('#pay-contract-spread')[0]);
+    const payContractSpreadSheet = payContractSpread.getActiveSheet();
+
+    const payContractSpreadSetting = {
+        cols: [
+            {title: '合同编号', colSpan: '1', rowSpan: '2', field: 'c_code', hAlign: 0, width: 110, formatter: '@', readOnly: true, backColor2: 'backColor.fromContract'},
+            {title: '合同名称', colSpan: '1', rowSpan: '2', field: 'name', hAlign: 0, width: 130, formatter: '@', readOnly: true, backColor2: 'backColor.fromContract'},
+            {title: '合同金额', colSpan: '1', rowSpan: '2', field: 'total_price', hAlign: 2, width: 80, type: 'Number', readOnly: 'readOnly.isEdit'},
+            {title: '收款单位', colSpan: '1', rowSpan: '2', field: 'entity', hAlign: 1, width: 130, formatter: '@', readOnly: 'readOnly.isEdit2'},
+            {title: '收款单位账号', colSpan: '1', rowSpan: '2', field: 'bank_account', hAlign: 1, formatter: '@', width: 130, readOnly: 'readOnly.isEdit2'},
+            {title: '收款单位开户行', colSpan: '1', rowSpan: '2', field: 'bank', hAlign: 1, formatter: '@', width: 130, readOnly: 'readOnly.isEdit2'},
+            {title: '小额支出', colSpan: '1', rowSpan: '2', field: 'small_expenses', cellType: 'checkbox', hAlign: 1, width: 60, readOnly: 'readOnly.isEdit2'},
+            {title: '支付金额', colSpan: '1', rowSpan: '2', field: 'pay_price', hAlign: 2, width: 80, type: 'Number', readOnly: 'readOnly.isEdit2'},
+            {title: '累计支付', colSpan: '1', rowSpan: '2', field: 'accumulate_pay_price', getValue: 'getValue.accumulate_pay_price', hAlign: 2, width: 80, type: 'Number', readOnly: true },
+            {title: '结算金额', colSpan: '1', rowSpan: '2', field: 'settle_price', hAlign: 2, width: 80, type: 'Number', readOnly: 'readOnly.isEdit2'},
+            {title: '累计结算', colSpan: '1', rowSpan: '2', field: 'accumulate_settle_price', getValue: 'getValue.accumulate_settle_price', hAlign: 2, width: 80, type: 'Number', readOnly: true },
+            {title: '未结算', colSpan: '1', rowSpan: '2', field: 'not_pay_price', getValue: 'getValue.not_pay_price', hAlign: 2, width: 80, type: 'Number', readOnly: true },
+            {title: '支付方式', colSpan: '1', rowSpan: '2', field: 'pay_type', hAlign: 2, width: 80, cellType: 'unit', comboItems: payTypeList },
+            {title: '发票', colSpan: '1', rowSpan: '2', field: 'bill', hAlign: 1, cellType: 'checkbox', width: 60, readOnly: 'readOnly.isEdit2' },
+            {title: '附件', colSpan: '1', rowSpan: '2', field: 'attachment', hAlign: 0, width: 60, readOnly: true, cellType: 'imageBtn',
+                normalImg: '#rela-file-icon', hoverImg: '#rela-file-hover', getValue: 'getValue.attachment' },
+        ],
+        emptyRows: 0,
+        headRows: 2,
+        headRowHeight: [25, 25],
+        defaultRowHeight: 21,
+        headerFont: '12px 微软雅黑',
+        font: '12px 微软雅黑',
+        // readOnly: readOnly,
+        localCache: {
+            key: 'pay-contract-spread',
+            colWidth: true,
+        }
+    };
+    payContractSpreadSetting.imageClick = function (data) {
+        makeAttTable(data);
+        $('#pay-contract-file').modal('show');
+    };
+
+    const payContractCol = {
+        getValue: {
+            attachment: function (data) {
+                return data.files ? data.files.length : 0;
+            },
+            accumulate_pay_price: function (data) {
+                return !data.cid ? data.pay_price : data.accumulate_pay_price;
+            },
+            accumulate_settle_price: function (data) {
+                return !data.cid ? data.settle_price : data.accumulate_settle_price;
+            },
+            not_pay_price: function (data) {
+                return ZhCalc.sub(data.total_price || 0, data.accumulate_settle_price || 0);
+            },
+        },
+        readOnly: {
+            isEdit: function (data) {
+                return data.c_code || readOnly;
+            },
+            isEdit2: function (data) {
+                return readOnly;
+            },
+        },
+        backColor: {
+            fromContract: function (data) {
+                if (!data.cid) {
+                    return '#f8f9fa';
+                }
+            },
+        }
+    }
+
+    const payContractSpreadObj = {
+        deletePress: function (sheet) {
+            return;
+        },
+        valueChanged: function (e, info) {
+            // 防止ctrl+z撤销数据
+            SpreadJsObj.reLoadRowData(info.sheet, info.row);
+        },
+    };
+
+    if (!readOnly) {
+        payContractSpreadObj.add = function () {
+            postData(preUrl + '/save', { type: 'contract_white_add' }, function (data) {
+                contractList.push(data);
+                SpreadJsObj.loadSheetData(payContractSpreadSheet, SpreadJsObj.DataType.Data, contractList);
+            });
+        };
+        payContractSpreadObj.del = function (sheet) {
+            const selection = sheet.getSelections();
+            const row = selection[0].row, count = selection[0].rowCount;
+            const sortData = sheet.zh_data;
+            const ids = [];
+            for (let iRow = 0; iRow < count; iRow++) {
+                if (sortData[iRow + row]) {
+                    ids.push(sortData[iRow + row].id);
+                }
+            }
+            if (ids.length > 0) {
+                postData(preUrl + '/save', {type: 'contract_del', ids}, function (result) {
+                    contractList = result.contractList;
+                    $('#pay-total-price').val(result.tp || 0);
+                    SpreadJsObj.loadSheetData(payContractSpreadSheet, SpreadJsObj.DataType.Data, contractList);
+                });
+            }
+        };
+        payContractSpreadObj.editEnded = function (e, info) {
+            if (info.sheet.zh_setting) {
+                const type = SpreadJsObj.getSelectObject(info.sheet) ? 'contract_update' : false;
+                if (!type) {
+                    toastr.error('该数据不存在');
+                    SpreadJsObj.reLoadRowData(info.sheet, info.row);
+                    return;
+                }
+                const select = type === 'contract_update' ? SpreadJsObj.getSelectObject(info.sheet) : {};
+                const col = info.sheet.zh_setting.cols[info.col];
+                if (col.field === 'small_expenses' || col.field === 'bill' || col.field === 'attachment') {
+                    return;
+                }
+                // 未改变值则不提交
+                let validText = col.type === 'Number' ? (is_numeric(info.editingText) ? parseFloat(info.editingText) : 0) : (info.editingText ? trimInvalidChar(info.editingText) : '');
+                const orgValue = type === 'contract_update' ? select[col.field] : '';
+                if (orgValue == validText || ((!orgValue || orgValue === '') && (validText === ''))) {
+                    SpreadJsObj.reLoadRowData(info.sheet, info.row);
+                    return;
+                }
+                if (col.field === 'accumulate_pay_price' || col.field === 'accumulate_settle_price' || col.field === 'not_pay_price') {
+                    SpreadJsObj.reLoadRowData(info.sheet, info.row);
+                    return;
+                }
+                // 判断部分值是否输入的是数字判断和数据计算
+                if (col.type === 'Number') {
+                    if (isNaN(validText)) {
+                        toastr.error('不能输入其它非数字类型字符');
+                        SpreadJsObj.reLoadRowData(info.sheet, info.row);
+                        return;
+                    }
+                }
+                if (col.field === 'pay_price') {
+                    select.accumulate_pay_price = ZhCalc.add(ZhCalc.sub(select.accumulate_pay_price, select.pay_price), validText);
+                }
+                if (col.field === 'settle_price') {
+                    select.accumulate_settle_price = ZhCalc.add(ZhCalc.sub(select.accumulate_settle_price, select.settle_price), validText);
+                }
+                select[col.field] = validText;
+                const uData = {
+                    id: select.id,
+                };
+                uData[col.field] = validText;
+                console.log(select, type);
+                delete select.waitingLoading;
+
+                // 更新至服务器
+                postData(preUrl + '/save', { type, updateData: uData }, function (result) {
+                    if(type === 'contract_update') {
+                        contractList.splice(info.row, 1, select);
+                        SpreadJsObj.reLoadRowData(info.sheet, info.row);
+                        $('#pay-total-price').val(result.tp || 0);
+                    }
+                }, function () {
+                    select[col.field] = orgValue;
+                    SpreadJsObj.reLoadRowData(info.sheet, info.row);
+                });
+            }
+        };
+        payContractSpreadObj.clipboardPasted  = function(e, info, cellRange) {
+            if (info.sheet.getColumnCount() > info.sheet.zh_setting.cols.length) {
+                info.sheet.setColumnCount(info.sheet.zh_setting.cols.length);
+            }
+            const hint = {
+                cellError: {type: 'error', msg: '粘贴内容超出了表格范围'},
+                numberExpr: {type: 'error', msg: '不能粘贴其它非数字类型字符'},
+            };
+            if (info.sheet.zh_setting) {
+                const sortData = info.sheet.zh_data || [];
+                const range = info.cellRange;
+                const data = [];
+                let haveNew = false;
+                for (let iRow = 0; iRow < range.rowCount; iRow++) {
+                    let bPaste = true;
+                    const curRow = range.row + iRow;
+                    // const materialData = JSON.parse(JSON.stringify(sortData[curRow]));
+                    const cLData = curRow >= sortData.length ? {} : {id: sortData[curRow].id};
+                    haveNew = curRow >= sortData.length ? curRow : false;
+                    const hintRow = range.rowCount > 1 ? curRow : '';
+                    let sameCol = 0;
+                    for (let iCol = 0; iCol < range.colCount; iCol++) {
+                        const curCol = range.col + iCol;
+                        const colSetting = info.sheet.zh_setting.cols[curCol];
+                        if (!colSetting) continue;
+                        // cLData[colSetting.field] = trimInvalidChar(info.sheet.getText(curRow, curCol));
+
+                        let validText = info.sheet.getText(curRow, curCol);
+                        validText = colSetting.type === 'Number' ? (is_numeric(validText) ? parseFloat(validText) : 0) : (validText ? trimInvalidChar(validText) : '');
+                        const orgValue = curRow >= sortData.length ? '' : sortData[curRow][colSetting.field];
+                        if (orgValue == validText || ((!orgValue || orgValue === '') && (validText === ''))) {
+                            sameCol++;
+                            if (range.colCount === sameCol) {
+                                bPaste = false;
+                            }
+                            continue;
+                        }
+
+                        if (colSetting.type === 'Number') {
+                            if (isNaN(validText)) {
+                                // toastMessageUniq(getPasteHint(hint.numberExpr, hintRow));
+                                toastMessageUniq(hint.numberExpr);
+                                bPaste = false;
+                                continue;
+                            }
+                        }
+                        cLData[colSetting.field] = validText;
+
+                    }
+                    if (bPaste) {
+                        data.push(cLData);
+                        // rowData.push(curRow);
+                    } else {
+                        SpreadJsObj.reLoadRowData(info.sheet, curRow);
+                    }
+                }
+                if (data.length === 0) {
+                    SpreadJsObj.reLoadRowData(info.sheet, info.cellRange.row, info.cellRange.rowCount);
+                    return;
+                }
+                console.log(data);
+                // 更新至服务器
+                postData(preUrl + '/save', { type:'contract_paste', updateData: data }, function (result) {
+                    contractList = result.contractList;
+                    if (haveNew) {
+                        SpreadJsObj.initSheet(payContractSpreadSheet, payContractSpreadSetting);
+                    }
+                    SpreadJsObj.loadSheetData(payContractSpreadSheet, SpreadJsObj.DataType.Data, contractList);
+                    if (haveNew) {
+                        payContractSpreadSheet.setSelection(haveNew, 1, 1, 1);
+                        payContractSpreadSheet.getParent().focus();
+                        payContractSpreadSheet.showRow(haveNew, spreadNS.VerticalPosition.center);
+                    }
+                    $('#pay-total-price').val(result.tp || 0);
+                }, function () {
+                    SpreadJsObj.reLoadRowData(info.sheet, info.cellRange.row, info.cellRange.rowCount);
+                    return;
+                });
+            }
+        };
+        payContractSpreadObj.buttonClicked = function (e, info) {
+            if (info.sheet.zh_setting) {
+                const select = SpreadJsObj.getSelectObject(info.sheet);
+                const col = info.sheet.zh_setting.cols[info.col];
+                if (!select) {
+                    toastr.error('请添加合同金额再勾选');
+                    if (info.sheet.isEditing()) {
+                        info.sheet.endEdit(true);
+                    }
+                    return;
+                } else if (col.field === 'small_expenses' || col.field === 'bill') {
+                    if (info.sheet.isEditing()) {
+                        info.sheet.endEdit(true);
+                    }
+                    const is_select = info.sheet.getValue(info.row, info.col) ? 0 : 1;
+                    if (select[col.field] !== is_select) {
+                        const uData = { id: select.id };
+                        select[col.field] = is_select;
+                        uData[col.field] = select[col.field];
+                        console.log(uData);
+                        postData(preUrl + '/save', { type: 'contract_update', updateData: uData }, function (result) {
+                            contractList.splice(info.row, 1, select);
+                            SpreadJsObj.reLoadRowData(info.sheet, info.row);
+                        }, function () {
+                            select[col.field] = info.sheet.getValue(info.row, info.col) ? 1 : 0;
+                            SpreadJsObj.reLoadRowData(info.sheet, info.row);
+                        });
+                    }
+                }
+            }
+        };
+        payContractSpread.bind(spreadNS.Events.EditEnded, payContractSpreadObj.editEnded);
+        payContractSpread.bind(spreadNS.Events.ButtonClicked, payContractSpreadObj.buttonClicked);
+        payContractSpread.bind(spreadNS.Events.ClipboardPasted, payContractSpreadObj.clipboardPasted);
+        payContractSpread.bind(spreadNS.Events.ValueChanged, payContractSpreadObj.valueChanged);
+        SpreadJsObj.addDeleteBind(payContractSpread, payContractSpreadObj.deletePress);
+
+        // 右键菜单
+        $.contextMenu({
+            selector: '#pay-contract-spread',
+            build: function ($trigger, e) {
+                const target = SpreadJsObj.safeRightClickSelection($trigger, e, payContractSpread);
+                return target.hitTestType === GC.Spread.Sheets.SheetArea.viewport || target.hitTestType === GC.Spread.Sheets.SheetArea.rowHeader;
+            },
+            items: {
+                'createAdd': {
+                    name: '添加',
+                    icon: 'fa-sign-in',
+                    callback: function (key, opt) {
+                        payContractSpreadObj.add(payContractSpreadSheet);
+                    },
+                },
+                'createAdd2': {
+                    name: '添加合同',
+                    icon: 'fa-sign-in',
+                    callback: function (key, opt) {
+                        $('#add-deal').modal('show');
+                    },
+                },
+                'delete': {
+                    name: '删除',
+                    icon: 'fa-remove',
+                    callback: function (key, opt) {
+                        payContractSpreadObj.del(payContractSpreadSheet);
+                    },
+                    disabled: function (key, opt) {
+                        if (payContractSpreadSheet.zh_data) {
+                            const selection = payContractSpreadSheet.getSelections();
+                            // return changeSpreadSheet.zh_data.length < selection[0].row + selection[0].rowCount;
+                            return payContractSpreadSheet.getRowCount() < selection[0].row + selection[0].rowCount;
+                        } else {
+                            return true;
+                        }
+                    }
+                },
+            }
+        });
+
+        $('#add-white-contract').click(function () {
+            payContractSpreadObj.add(payContractSpreadSheet);
+        });
+        let contracts = [];
+        let contractTrees = [];
+        const sqTreeSetting = {
+            id: 'contract_id',
+            pid: 'contract_pid',
+            order: 'order',
+            level: 'level',
+            rootId: -1,
+            keys: ['id', 'tid', 'spid'],
+        };
+        const sqTree = createNewPathTree('base', sqTreeSetting);
+        $('#add-deal').on('show.bs.modal', function () {
+            $('#contract-tree').val('0');
+            $('#contract-keyword').val('');
+            $('#select-all-contract').prop('checked', false);
+            postData(preUrl + '/save', { type: 'contract_list' }, function (result) {
+                contracts = result.contracts;
+                contractTrees = result.contractTrees;
+                sqTree.loadDatas(contractTrees);
+                const level2Tree = _.filter(sqTree.nodes, function (item) {
+                    return item.level === 2;
+                });
+                makeContractListHtml(contracts);
+                let html2 = '<option value="0">全部</option>';
+                for (const t of level2Tree) {
+                    html2 += `<option value="${t.contract_id}">${t.name}</option>`;
+                }
+                $('#contract-tree').html(html2);
+            });
+        });
+
+        function makeContractListHtml(list) {
+            let html = '';
+            for (const c of list) {
+                const tree = _.find(contractTrees, function (item) {
+                    return item.contract_id === parseInt(c.full_path.split('-')[1] || 0);
+                });
+                html += `<tr class="text-center">
+                                    <td><input type="checkbox" data-contract-id="${c.id}" ${_.findIndex(contractList, { cid: c.id }) !== -1 ? 'checked disabled' : ''}></td>
+                                    <td>${tree ? tree.name : ''}</td>
+                                    <td class="text-left">${c.c_code}</td>
+                                    <td class="text-left">${c.name}</td>
+                                    <td class="text-right">${c.total_price}</td>
+                                    <td>${c.entity}</td>
+                                    <td>${c.bank_account}</td>
+                                </tr>`;
+            }
+            $('#contract-list').html(html);
+        }
+
+        $('body').on('change', '#contract-tree', function () {
+            searchContracts();
+        });
+
+        $('#search-contract-btn').click(function () {
+            searchContracts();
+        });
+
+        // 回车提交
+        $('#contract-keyword').on('keypress', function () {
+            if(window.event.keyCode === 13) {
+                searchContracts();
+            }
+        });
+
+        function searchContracts() {
+            $('#select-all-contract').prop('checked', false);
+            const keyword = $('#contract-keyword').val();
+            const selectTree = $('#contract-tree').val();
+            const list = _.filter(contracts, function (item) {
+                return (selectTree !== '0' ? _.includes(item.full_path, '-' + selectTree + '-') : true) && (keyword !== '' ? (item.c_code.indexOf(keyword) > -1 || item.name.indexOf(keyword) > -1) : true);
+            });
+            makeContractListHtml(list);
+        }
+
+        $('#select-all-contract').click(function () {
+            $('#contract-list input:not(:disabled)').prop('checked', $(this).prop('checked'));
+        });
+
+        $('#add-contract-btn').click(function () {
+            const selects = [];
+            $('#contract-list input:checked:not(:disabled)').each(function () {
+                selects.push($(this).data('contract-id'));
+            });
+            if (selects.length === 0) {
+                toastr.warning('请选择合同再添加');
+                return;
+            }
+            console.log(selects);
+            postData(preUrl + '/save', { type: 'contract_add', contract_ids: selects }, function (result) {
+                $('#add-deal').modal('hide');
+                contractList = result.contractList;
+                SpreadJsObj.loadSheetData(payContractSpreadSheet, SpreadJsObj.DataType.Data, contractList);
+                // $('#pay-total-price').val(result.tp || 0);
+            });
+        });
+    }
+    SpreadJsObj.initSpreadSettingEvents(payContractSpreadSetting, payContractCol);
+    SpreadJsObj.initSheet(payContractSpreadSheet, payContractSpreadSetting);
+    postData(preUrl + '/save', { type: 'pay_contract_list' }, function (result) {
+        contractList = result;
+        SpreadJsObj.loadSheetData(payContractSpreadSheet, SpreadJsObj.DataType.Data, result);
+    });
+
+    // 上传附件
+    $('#pay-contract-file input[type="file"]').change(function () {
+        const files = Array.from(this.files);
+        const valiData = files.map(v => {
+            const ext = v.name.substring(v.name.lastIndexOf('.') + 1)
+            return {
+                size: v.size,
+                ext
+            }
+        });
+        const fpcid = $('#file-contract-id').val();
+        if (!$('#pay-contract-file input[name="type"]:checked').val()) {
+            toastr.error('请选择资料类型');
+            return false;
+        }
+        const fpcInfo = _.find(contractList, { id: parseInt(fpcid) });
+        if (!fpcInfo) {
+            toastr.warning('不存在该资金支付明细');
+            $('#pay-contract-file input[type="file"]').val('');
+            return;
+        }
+        if (validateFiles(valiData)) {
+            if (files.length) {
+                const formData = new FormData()
+                formData.append('type', $('#pay-contract-file input[name="type"]:checked').val());
+                formData.append('fpcid', fpcInfo.id);
+                files.forEach(file => {
+                    formData.append('name', file.name)
+                    formData.append('size', file.size)
+                    formData.append('file', file)
+                })
+                postDataWithFile(preUrl + '/file/upload', formData, function (result) {
+                    fpcInfo.files = result;
+                    makeAttTable(fpcInfo);
+                    SpreadJsObj.reLoadRowData(payContractSpreadSheet, contractList.indexOf(fpcInfo));
+                });
+            }
+        }
+        $('#transfer-file input[type="file"]').val('');
+    });
+
+    $('body').on('click', '#pay-contract-file .file-del', function () {
+        const fpcid = $('#file-contract-id').val();
+        const fpcInfo = _.find(contractList, { id: parseInt(fpcid) });
+        if (!fpcInfo) {
+            toastr.warning('不存在该资金支付明细');
+            return;
+        }
+        const fid = $(this).data('id');
+        deleteAfterHint(function () {
+            postData(preUrl + '/file/delete', { fpcid: fpcInfo.id, id: fid }, (result) => {
+                fpcInfo.files = result;
+                makeAttTable(fpcInfo);
+                SpreadJsObj.reLoadRowData(payContractSpreadSheet, contractList.indexOf(fpcInfo));
+            });
+        }, '确认删除该文件?');
+    });
+
+    function makeAttTable(payNode) {
+        $('#file-contract-id').val(payNode.id);
+        const files = payNode.files;
+        let filesHtml = '';
+        const newFiles = files.map(file => {
+            let showDel = false;
+            if (file.uid === cur_uid) {
+                if (file.type === 1 && financialPay.readOnly) {
+                    showDel = false;
+                } else {
+                    showDel = true;
+                }
+            }
+            return {...file, showDel}
+        });
+        newFiles.forEach((file, idx) => {
+            filesHtml += `<tr class="text-center">
+                                        <td>${idx + 1}</td><td class="text-left"><a href="${file.filepath}" target="_blank">${file.filename}</a></td>
+                                        <td>${file.username}</td>
+                                        <td>${file.type === 1 ? (financialPay.status !== auditConst.status.checked && file.uid === cur_uid ? '<input type="text" data-id="' + file.id + '" class="file-bill form-control form-control-sm" value="' + file.bill + '" />' : file.bill)  : '' }</td>
+                                        <td>${moment(file.upload_time).format('YYYY-MM-DD HH:mm:ss')}</td>
+                                        <td>
+                                            <div class="btn-group-table">
+                                                ${file.viewpath ? `<a href="${file.viewpath}" target="_blank" class="mr-1"><i class="fa fa-eye fa-fw"></i></a>` : ''}
+                                                <a href="/financial/${payNode.spid}/pay/${payNode.fpid}/file/${file.id}/download" class="mr-1"><i class="fa fa-download fa-fw"></i></a>
+                                                ${file.showDel ? `<a href="javascript: void(0);" class="text-danger file-del mr-1" data-id="${file.id}"><i class="fa fa-trash-o fa-fw text-danger"></i></a>` : ''}
+                                            </div>
+                                        </td>
+                                    </tr>`;
+        });
+        $('#contract-files').html(filesHtml);
+    }
+
+    $('body').on('change', '#contract-files .file-bill', function () {
+        const bill = $(this).val();
+        const fid = $(this).data('id');
+        saveBills(bill, fid);
+    });
+
+    function saveBills(bill, fid) {
+        const fpcid = $('#file-contract-id').val();
+        const fpcInfo = _.find(contractList, { id: parseInt(fpcid) });
+        if (!fpcInfo) {
+            toastr.warning('不存在该资金支付明细');
+            return;
+        }
+        postData(preUrl + '/save', { type: 'file_bill', id: fid, bill }, function (result) {
+            const fileInfo = _.find(fpcInfo.files, { id: fid });
+            fileInfo.bill = bill;
+        });
+    }
+
+    $.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();
+        }
+    });
+});
+function checkStartFrom() {
+    // 判断发票是否已上传
+    if (!contractList || contractList.length === 0) {
+        toastr.error('请添加合同再上报');
+        return false;
+    }
+    let notBills = [];
+    for (const c of contractList) {
+        if (c.bill && _.findIndex(c.files, { type: 1 }) === -1) {
+            notBills.push(c);
+        }
+    }
+    if (notBills.length > 0) {
+        for (const c of notBills) {
+            toastr.error(`合同${c.c_code}未上传发票`);
+        }
+        return false;
+    }
+    return true;
+}
+// texterea换行
+function auditCheck(i) {
+    // const inlineRadio1 = $('#inlineRadio1:checked').val()
+    // const inlineRadio2 = $('#inlineRadio2:checked').val()
+    const opinion = $('textarea[name="opinion"]').eq(i).val().replace(/\r\n/g, '<br/>').replace(/\n/g, '<br/>').replace(/\s/g, ' ');
+    $('textarea[name="opinion"]').eq(i).val(opinion);
+    // if (i === 1) {
+    //     if (!inlineRadio1 && !inlineRadio2) {
+    //         if (!$('#warning-text').length) {
+    //             $('#reject-process').prepend('<p id="warning-text" style="color: red; margin: 0;">请选择退回流程</p>');
+    //         }
+    //         return false;
+    //     }
+    //     if ($('#warning-text').length) $('#warning-text').remove()
+    // }
+
+    return true;
+}
+/**
+ * 校验文件大小、格式
+ * @param {Array} files 文件数组
+ */
+function validateFiles(files) {
+    if (files.length > 10) {
+        toastr.error('至多同时上传10个文件');
+        return false
+    }
+    return files.every(file => {
+        if (file.size > 1024 * 1024 * 30) {
+            toastr.error('文件大小限制为30MB');
+            return false
+        }
+        if (whiteList.indexOf('.' + file.ext.toLowerCase()) === -1) {
+            toastr.error('请上传正确的格式文件');
+            return false
+        }
+        return true
+    })
+}
+const is_numeric = (value) => {
+    if (typeof(value) === 'object') {
+        return false;
+    } else {
+        return !Number.isNaN(Number(value)) && value.toString().trim() !== '';
+    }
+};

+ 186 - 0
app/public/js/financial_transfer.js

@@ -0,0 +1,186 @@
+$(function () {
+    autoFlashHeight();
+
+    $('#add-transfer-btn').on('click', function () {
+        if ($('#transfer-date').val() === '') {
+            toastr.error('请选择日期');
+            return false;
+        }
+        $('#add-transfer-form').submit();
+    });
+
+    $('body').on('click', '#transfer-list .del-transfer-btn', function () {
+        const trid = $(this).data('id');
+        deleteAfterHint(function () {
+            postData(window.location.pathname + '/update', {postType: 'del-transfer', postData: { node: trid }}, function (result) {
+                window.location.reload();
+            })
+        }, '确认删除该资金划拨并移除所有附件?');
+    });
+
+    $('body').on('click', '.open-transfer-files', function () {
+        const trid = $(this).attr('data-trid');
+        if (!trid) {
+            toastr.error('获取资金划拨信息失败');
+            return;
+        }
+        const trInfo = _.find(transferList, { id: parseInt(trid) });
+        if (!trInfo) {
+            toastr.error('获取资金划拨信息失败');
+            return;
+        }
+        if (trInfo.uid === user_id || financialPermission.transfer_file) {
+            $('#transfer-file .upload-permission').show();
+        } else {
+            $('#transfer-file .upload-permission').hide();
+        }
+        $('#transfer-file').modal('show');
+        $('#transfer-file input[name="trid"]').val(trid);
+        openFinancialTransferFiles(trInfo);
+    });
+
+    // 上传附件
+    $('#transfer-file input[type="file"]').change(function () {
+        const files = Array.from(this.files);
+        const valiData = files.map(v => {
+            const ext = v.name.substring(v.name.lastIndexOf('.') + 1)
+            return {
+                size: v.size,
+                ext
+            }
+        });
+        const trid = $('#transfer-file input[name="trid"]').val();
+        const trInfo = _.find(transferList, { id: parseInt(trid) });
+        if (!trInfo) {
+            toastr.warning('不存在该资金划拨');
+            $('#transfer-file input[type="file"]').val('');
+            return;
+        }
+        if (validateFiles(valiData)) {
+            if (files.length) {
+                const formData = new FormData()
+                files.forEach(file => {
+                    formData.append('name', file.name)
+                    formData.append('size', file.size)
+                    formData.append('file', file)
+                })
+                postDataWithFile('/financial/' + spid + '/transfer/' + trInfo.id + '/file/upload', formData, function (result) {
+                    trInfo.files = result;
+                    openFinancialTransferFiles(trInfo);
+                    $('#transfer-list tr[data-id="' + trInfo.id + '"] .file-num').text(trInfo.files.length ? trInfo.files.length : '');
+                });
+            }
+        }
+        $('#transfer-file input[type="file"]').val('');
+    });
+
+    $('body').on('click', '#transfer-file .file-del', function () {
+        const trid = $('#transfer-file input[name="trid"]').val();
+        const trInfo = _.find(transferList, { id: parseInt(trid) });
+        if (!trInfo) {
+            toastr.warning('不存在该资金划拨');
+            return;
+        }
+        const fid = $(this).data('id');
+        deleteAfterHint(function () {
+            postData('/financial/' + spid + '/transfer/' + trInfo.id + '/file/delete', { id: fid }, function (result) {
+                trInfo.files = result;
+                openFinancialTransferFiles(trInfo);
+                $('#transfer-list tr[data-id="' + trInfo.id + '"] .file-num').text(trInfo.files.length ? trInfo.files.length : '');
+            });
+        }, '确认删除该文件?');
+    });
+
+    function openFinancialTransferFiles(trInfo, _this = '#transfer-file table tbody') {
+        const files = trInfo.files;
+        let filesHtml = '';
+        const newFiles = files.map(file => {
+            let showDel = false;
+            if (file.uid === user_id) {
+                showDel = true
+            }
+            return {...file, showDel}
+        })
+        newFiles.forEach((file, idx) => {
+            filesHtml += `<tr class="text-center">
+                                        <td>${idx + 1}</td><td class="text-left"><a href="${file.filepath}" target="_blank">${file.filename}</a></td><td>${file.username}</td><td>${moment(file.upload_time).format('YYYY-MM-DD HH:mm:ss')}</td>
+                                        <td>
+                                            <div class="btn-group-table">
+                                                ${file.viewpath ? `<a href="${file.viewpath}" target="_blank" class="mr-1"><i class="fa fa-eye fa-fw"></i></a>` : ''}
+                                                <a href="/financial/${spid}/transfer/${trInfo.id}/file/${file.id}/download" class="mr-1"><i class="fa fa-download fa-fw"></i></a>
+                                                ${file.showDel ? `<a href="javascript: void(0);" class="text-danger file-del mr-1" data-id="${file.id}"><i class="fa fa-trash-o fa-fw text-danger"></i></a>` : ''}
+                                            </div>
+                                        </td>
+                                    </tr>`;
+        });
+        $(_this).html(filesHtml);
+    }
+
+    $('body').on('click', '#transfer-list .edit-remark', function () {
+        const trid = $(this).data('id');
+        const trInfo = _.find(transferList, { id: trid });
+        if (!trInfo) {
+            toastr.error('获取资金划拨信息失败');
+            return;
+        }
+        $('#edit-remark-modal .transfer-time').text(trInfo.t_time);
+        $('#edit-remark-modal').modal('show');
+        $('#edit-remark-modal input[name="ftid"]').val(trid);
+        $('#edit-remark-modal textarea[name="remark"]').val(trInfo.remark);
+    });
+
+    $('#save-remark-btn').click(function () {
+        const data = {
+            id: parseInt($('#edit-remark-modal input[name="ftid"]').val()),
+            remark: $('#edit-remark-modal textarea[name="remark"]').val(),
+        }
+        const trInfo = _.find(transferList, { id: data.id });
+        if (!trInfo) {
+            toastr.error('获取资金划拨信息失败');
+            return;
+        }
+        postData(window.location.pathname + '/update', {postType: 'save-transfer', postData: data}, function (result) {
+            trInfo.remark = data.remark;
+            $('#edit-remark-modal').modal('hide');
+            $('#transfer-list tr[data-id="' + trInfo.id + '"] .show-remark').html(trInfo.remark);
+        });
+    });
+
+    $.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();
+        }
+    });
+})
+/**
+ * 校验文件大小、格式
+ * @param {Array} files 文件数组
+ */
+function validateFiles(files) {
+    if (files.length > 10) {
+        toastr.error('至多同时上传10个文件');
+        return false
+    }
+    return files.every(file => {
+        if (file.size > 1024 * 1024 * 30) {
+            toastr.error('文件大小限制为30MB');
+            return false
+        }
+        if (whiteList.indexOf('.' + file.ext.toLowerCase()) === -1) {
+            toastr.error('请上传正确的格式文件');
+            return false
+        }
+        return true
+    })
+}

+ 188 - 0
app/public/js/financial_transfer_tender.js

@@ -0,0 +1,188 @@
+$(function () {
+    autoFlashHeight();
+
+    $('#add-tender-btn').click(function () {
+        const addTenders = [];
+        $('#tenders tr').each(function () {
+            if ($(this).find('input').is(':checked') && $(this).find('select').val().length > 0) {
+                addTenders.push({
+                    tid: $(this).find('input').val(),
+                    sorder: $(this).find('select').val(),
+                })
+            }
+        });
+        if (addTenders.length === 0) {
+            toastr.warning('请选择添加标段及期数');
+            return false;
+        }
+        postData(window.location.pathname + '/update', {postType: 'add-tender', postData: { tenders: addTenders }}, function (result) {
+            window.location.reload();
+        });
+    });
+
+    $('body').on('click', '#tender-list .del-tender-btn', function () {
+        const ftid = $(this).data('id');
+        deleteAfterHint(function () {
+            postData(window.location.pathname + '/update', {postType: 'del-tender', postData: { node: ftid }}, function (result) {
+                window.location.reload();
+            })
+        }, '确认删除该标段并移除所有附件?');
+    });
+
+    $('#lock-transfer-btn').click(function () {
+        postData('/financial/'+ spid + '/transfer/update', {postType: 'lock-transfer', postData: { node: trid, lock: 1 }}, function (result) {
+            window.location.reload();
+        });
+    });
+
+    $('#unlock-transfer-btn').click(function () {
+        postData('/financial/'+ spid + '/transfer/update', {postType: 'lock-transfer', postData: { node: trid, lock: 0 }}, function (result) {
+            window.location.reload();
+        });
+    });
+
+    $('#tender-list input[type="text"]').change(function () {
+        const ftid = $(this).data('ftid');
+        const value = $(this).val() || 0;
+        postData(window.location.pathname + '/update', {postType: 'update-hb_tp', postData: { node: ftid, hb_tp: parseFloat(value) }}, function (result) {
+            window.location.reload();
+        });
+    });
+
+    $('body').on('click', '.open-tender-files', function () {
+        const ftid = $(this).attr('data-ftid');
+        if (!ftid) {
+            toastr.error('获取资金划拨标段信息失败');
+            return;
+        }
+        const ftInfo = _.find(tenderList, { id: parseInt(ftid) });
+        if (!ftInfo) {
+            toastr.error('获取资金划拨信息失败');
+            return;
+        }
+        if (ftInfo.uid === user_id || financialPermission.transfer_file) {
+            $('#tender-file .upload-permission').show();
+        } else {
+            $('#tender-file .upload-permission').hide();
+        }
+        $('#tender-file').modal('show');
+        $('#tender-file input[name="ftid"]').val(ftid);
+        openFinancialTransferTenderFiles(ftInfo);
+    });
+
+    // 上传附件
+    $('#tender-file input[type="file"]').change(function () {
+        const files = Array.from(this.files);
+        const valiData = files.map(v => {
+            const ext = v.name.substring(v.name.lastIndexOf('.') + 1)
+            return {
+                size: v.size,
+                ext
+            }
+        });
+        const ftid = $('#tender-file input[name="ftid"]').val();
+        const ftInfo = _.find(tenderList, { id: parseInt(ftid) });
+        if (!ftInfo) {
+            toastr.warning('不存在该资金划拨标段');
+            $('#tender-file input[type="file"]').val('');
+            return;
+        }
+        if (validateFiles(valiData)) {
+            if (files.length) {
+                const formData = new FormData()
+                files.forEach(file => {
+                    formData.append('name', file.name)
+                    formData.append('size', file.size)
+                    formData.append('file', file)
+                })
+                postDataWithFile('/financial/' + spid + '/transfer/' + trid + '/tender/' + ftInfo.id + '/file/upload', formData, function (result) {
+                    ftInfo.files = result;
+                    openFinancialTransferTenderFiles(ftInfo);
+                    $('#tender-list tr[data-id="' + ftInfo.id + '"] .file-num').text(ftInfo.files.length ? ftInfo.files.length : '');
+                });
+            }
+        }
+        $('#tender-file input[type="file"]').val('');
+    });
+
+    $('body').on('click', '#tender-file .file-del', function () {
+        const ftid = $('#tender-file input[name="ftid"]').val();
+        const ftInfo = _.find(tenderList, { id: parseInt(ftid) });
+        if (!ftInfo) {
+            toastr.warning('不存在该资金划拨标段');
+            return;
+        }
+        const fid = $(this).data('id');
+        deleteAfterHint(function () {
+            postData('/financial/' + spid + '/transfer/' + trid + '/tender/' + ftInfo.id + '/file/delete', { id: fid }, function (result) {
+                ftInfo.files = result;
+                openFinancialTransferTenderFiles(ftInfo);
+                $('#tender-list tr[data-id="' + ftInfo.id + '"] .file-num').text(ftInfo.files.length ? ftInfo.files.length : '');
+            });
+        }, '确认删除该文件?');
+    });
+
+    function openFinancialTransferTenderFiles(ftInfo, _this = '#tender-file table tbody') {
+        const files = ftInfo.files;
+        let filesHtml = '';
+        const newFiles = files.map(file => {
+            let showDel = false;
+            if (file.uid === user_id) {
+                showDel = true
+            }
+            return {...file, showDel}
+        })
+        newFiles.forEach((file, idx) => {
+            filesHtml += `<tr class="text-center">
+                                        <td>${idx + 1}</td><td class="text-left"><a href="${file.filepath}" target="_blank">${file.filename}</a></td><td>${file.username}</td><td>${moment(file.upload_time).format('YYYY-MM-DD HH:mm:ss')}</td>
+                                        <td>
+                                            <div class="btn-group-table">
+                                                ${file.viewpath ? `<a href="${file.viewpath}" target="_blank" class="mr-1"><i class="fa fa-eye fa-fw"></i></a>` : ''}
+                                                <a href="/financial/${spid}/transfer/${trid}/tender/${ftInfo.id}/file/${file.id}/download" class="mr-1"><i class="fa fa-download fa-fw"></i></a>
+                                                ${file.showDel ? `<a href="javascript: void(0);" class="text-danger file-del mr-1" data-id="${file.id}"><i class="fa fa-trash-o fa-fw text-danger"></i></a>` : ''}
+                                            </div>
+                                        </td>
+                                    </tr>`;
+        });
+        $(_this).html(filesHtml);
+    }
+
+    $.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();
+        }
+    });
+})
+/**
+ * 校验文件大小、格式
+ * @param {Array} files 文件数组
+ */
+function validateFiles(files) {
+    if (files.length > 10) {
+        toastr.error('至多同时上传10个文件');
+        return false
+    }
+    return files.every(file => {
+        if (file.size > 1024 * 1024 * 30) {
+            toastr.error('文件大小限制为30MB');
+            return false
+        }
+        if (whiteList.indexOf('.' + file.ext.toLowerCase()) === -1) {
+            toastr.error('请上传正确的格式文件');
+            return false
+        }
+        return true
+    })
+}
+

+ 6 - 0
app/public/js/measure_compare.js

@@ -500,4 +500,10 @@ $(document).ready(() => {
         gclSpread.refresh();
         leafXmjSpread.refresh();
     });
+    $('#select-qi-all').click(function() {
+        const check = this.checked;
+        $('input', 'tr[stage-id]').each((i, x) => {
+            x.checked = check;
+        })
+    });
 });

+ 3 - 2
app/public/js/shares/batch_import.js

@@ -96,6 +96,7 @@ const BatchImportStageGcl = function (setting) {
                     {title: '编号', field: 'code', hAlign: 0, width: 180, formatter: '@', cellType: 'tree', readOnly: true },
                     {title: '名称/引用标段', field: 'name', hAlign: 0, width: 180, formatter: '@', readOnly: true },
                     {title: '可选期', field: 'stage', hAlign: 1, width: 60, formatter: '@'},
+                    {title: '指定项目节', field: 'match_code', hAlign: 1, width: 70, formatter: '@', readOnly: true},
                     {title: '状态', field: 'status', hAlign: 1, width: 60, formatter: '@', readOnly: true},
                     {title: '错误信息', field: 'error', hAlign: 1, width: 60, formatter: '@', readOnly: true},
                 ],
@@ -128,8 +129,8 @@ const BatchImportStageGcl = function (setting) {
                 $('#bi-download-error').hide();
             }
         },
-        importStageGcl: async function (node, cover, ignore, loadChange) {
-            const updateData = { lid: node.lid, type: 'stage', cover, ignore, loadChange, tenders: [] };
+        importStageGcl: async function (node, cover, ignore, change) {
+            const updateData = { lid: node.lid, type: 'stage', cover, ignore, change, tenders: [] };
             for (const tender of node.children) {
                 updateData.tenders.push({ tid: tender.tid, name: tender.name, stageCount: tender.stageCount, stage: tender.stage, match_code: tender.match_code });
             }

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

@@ -65,6 +65,9 @@ const SpreadJsObj = {
             if (col.foreColor && Object.prototype.toString.apply(col.foreColor) === "[object String]") {
                 col.foreColor = getEvent(col.foreColor);
             }
+            if (col.backColor2 && Object.prototype.toString.apply(col.backColor2) === "[object String]") {
+                col.backColor2 = getEvent(col.backColor2);
+            }
         }
     },
 
@@ -489,6 +492,13 @@ const SpreadJsObj = {
         if (sheet.zh_setting.selectedBackColor && sels && sels[0].row === row ) {
             return sheet.zh_setting.selectedBackColor;
         } else {
+            if (col.backColor2) {
+                if (Object.prototype.toString.apply(col.backColor2) === "[object Function]") {
+                    backColor = col.backColor2(data, backColor);
+                } else {
+                    backColor = col.backColor2 || backColor;
+                }
+            }
             if (sheet.zh_setting.tree.getColor && Object.prototype.toString.apply(sheet.zh_setting.tree.getColor) === "[object Function]") {
                 backColor = sheet.zh_setting.tree.getColor(sheet, data, row, col, backColor);
             }

+ 44 - 13
app/public/js/stage.js

@@ -201,14 +201,16 @@ $(document).ready(() => {
             {title: '|金额', colSpan: '|1', rowSpan: '|1', field: 'total_price', hAlign: 2, width: 60, type: 'Number'},
             {title: '本期合同计量|数量', colSpan: '2|1', rowSpan: '1|1', field: 'contract_qty', hAlign: 2, width: 60, type: 'Number'},
             {title: '|金额', colSpan: '|1', rowSpan: '|1', field: 'contract_tp', hAlign: 2, width: 60, type: 'Number'},
-            {title: '本期数量变更|数量', colSpan: '2|1', rowSpan: '1|1', field: 'qc_qty', hAlign: 2, width: 60, type: 'Number'},
+            {title: '本期数量变更|数量', colSpan: '3|1', rowSpan: '1|1', field: 'qc_qty', hAlign: 2, width: 60, type: 'Number'},
             {title: '|金额', colSpan: '|1', rowSpan: '|1', field: 'qc_tp', hAlign: 2, width: 60, type: 'Number'},
+            {title: '|不计价', colSpan: '|1', rowSpan: '|1', field: 'qc_minus_qty', hAlign: 2, width: 60, type: 'Number'},
             {title: '本期完成计量|数量', colSpan: '2|1', rowSpan: '1|1', field: 'gather_qty', hAlign: 2, width: 60, type: 'Number'},
             {title: '|金额', colSpan: '|1', rowSpan: '|1', field: 'gather_tp', hAlign: 2, width: 60, type: 'Number'},
             {title: '截止本期合同计量|数量', colSpan: '2|1', rowSpan: '1|1', field: 'end_contract_qty', hAlign: 2, width: 60, type: 'Number'},
             {title: '|金额', colSpan: '|1', rowSpan: '|1', field: 'end_contract_tp', hAlign: 2, width: 60, type: 'Number'},
-            {title: '截止本期数量变更|数量', colSpan: '2|1', rowSpan: '1|1', field: 'end_qc_qty', hAlign: 2, width: 60, type: 'Number'},
+            {title: '截止本期数量变更|数量', colSpan: '3|1', rowSpan: '1|1', field: 'end_qc_qty', hAlign: 2, width: 60, type: 'Number'},
             {title: '|金额', colSpan: '|1', rowSpan: '|1', field: 'end_qc_tp', hAlign: 2, width: 60, type: 'Number'},
+            {title: '|不计价', colSpan: '|1', rowSpan: '|1', field: 'end_qc_minus_qty', hAlign: 2, width: 60, type: 'Number'},
             {title: '截止本期完成计量|数量', colSpan: '3|1', rowSpan: '1|1', field: 'end_gather_qty', hAlign: 2, width: 60, type: 'Number'},
             {title: '|金额', colSpan: '|1', rowSpan: '|1', field: 'end_gather_tp', hAlign: 2, width: 60, type: 'Number'},
             {title: '|完成率(%)', colSpan: '|1', rowSpan: '|1', field: 'end_final_1_percent', hAlign: 2, width: 80, type: 'Number'},
@@ -220,6 +222,7 @@ $(document).ready(() => {
             {title: '本期批注', colSpan: '1', rowSpan: '2', field: 'postil', hAlign: 0, width: 100, formatter: '@', cellType: 'autoTip'},
             {title: '图(册)号', colSpan: '1', rowSpan: '2', field: 'drawing_code', hAlign: 0, width: 80, formatter: '@'},
             {title: '备注', colSpan: '1', rowSpan: '2', field: 'memo', hAlign: 0, width: 100, formatter: '@', cellType: 'ellipsisAutoTip'},
+            {title: '位置', colSpan: '1', rowSpan: '2', field: 'position', hAlign: 0, width: 80, formatter: '@'},
         ],
         headRows: 2,
         headRowHeight: [25, 25],
@@ -1437,10 +1440,10 @@ $(document).ready(() => {
                         code: node.code, b_code: node.b_code, name: node.name, unit: node.unit,
                         unit_price: node.unit_price, quantity: node.quantity, total_price: node.total_price,
                         contract_qty: node.contract_qty, contract_tp: node.contract_tp,
-                        qc_qty: node.qc_qty, qc_tp: node.qc_tp,
+                        qc_qty: node.qc_qty, qc_tp: node.qc_tp, qc_minus_qty: node.qc_minus_qty,
                         gather_qty: node.gather_qty, gather_tp: node.gather_tp,
                         end_contract_qty: node.end_contract_qty, end_contract_tp: node.end_contract_tp,
-                        end_qc_qty: node.end_qc_qty, end_qc_tp: node.end_qc_tp,
+                        end_qc_qty: node.end_qc_qty, end_qc_tp: node.end_qc_tp, end_qc_minus_qty: node.end_qc_minus_qty,
                         end_gather_qty: node.end_gather_qty, end_gather_tp: node.end_gather_tp, end_final_1_percent: node.end_final_1_percent,
                         deal_dgn_qty1: node.deal_dgn_qty1, deal_dgn_qty2: node.deal_dgn_qty2,
                         c_dgn_qty1: node.c_dgn_qty1, c_dgn_qty2: node.c_dgn_qty2,
@@ -1460,7 +1463,8 @@ $(document).ready(() => {
                                     quantity: p.quantity,
                                     contract_qty: p.contract_qty, qc_qty: p.qc_qty, gather_qty: p.gather_qty,
                                     end_contract_qty: p.end_contract_qty, end_qc_qty: p.end_qc_qty, end_gather_qty: p.end_gather_qty,
-                                    drawing_code: p.drawing_code, memo: p.memo, postil: p.postil,
+                                    qc_minus_qty: p.qc_minus_qty, end_qc_minus_qty: p.end_qc_minus_qty,
+                                    drawing_code: p.drawing_code, memo: p.memo, postil: p.postil, position: p.position,
                                 });
                             }
                         }
@@ -1648,7 +1652,7 @@ $(document).ready(() => {
                     const sheet = spSpread.getActiveSheet();
                     const node = SpreadJsObj.getSelectObject(slSpread.getActiveSheet());
 
-                    const data = { updateType: 'update', updateData: [] };
+                    const data = { updateType: 'batchUpdate', updateData: [] };
                     const posterity = stageTree.getPosterity(node);
                     posterity.unshift(node);
                     for (const p of posterity) {
@@ -1668,18 +1672,23 @@ $(document).ready(() => {
                         return;
                     }
 
-                    if (updateStage.length > 0) {
-                        if (updateStage.length > 1000) {
+                    if (data.updateData.length > 0) {
+                        if (data.updateData.length > 1000) {
                             toastr.warning('提交的数据太大,仅提交1000条数据');
-                            updateStage.length = 1000;
+                            data.updateData.length = 1000;
                         }
                         postData(window.location.pathname + '/update', {pos: data}, function (result) {
-                            const nodes = stageTree.loadPostStageData(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.loadStageLedgerUpdateData(result, nodes);
+                                detail.loadStagePosUpdateData(result, nodes);
                             } else {
-                                stageIm.loadUpdateLedgerData(result, nodes);
+                                stageIm.loadUpdatePosData(result, nodes);
                             }
                             stageTreeSpreadObj.loadExprToInput(sheet);
                         });
@@ -1872,7 +1881,29 @@ $(document).ready(() => {
                     return contractExpr;
                 }
             },
-            importSpr: '---',
+            dealLoadSpr: '---',
+            exportDealData: {
+                name: '导出本期合同计量',
+                callback: function (key, opt, menu, e) {
+                    window.open(window.location.pathname + '/cpd');
+                },
+            },
+            importDealData: {
+                name: '导入本期合同计量',
+                callback: function (key, opt, menu, e) {
+                    BaseImportFile.show({
+                        validList: ['.cpd'],
+                        url: window.location.pathname + '/cpd/load',
+                        afterImport: function() {
+                            window.location.reload();
+                        }
+                    });
+                },
+                disabled: function(key, opt) {
+                    return readOnly || stage.revising || stage.status !== auditConst.status.uncheck;
+                },
+            },
+            sumLoadSpr: '---',
             importStageGcl: {
                 name: '导入(其他标段)工程量清单计量数据',
                 icon: 'fa-link',

+ 0 - 1
app/public/js/tender_list_info.js

@@ -172,7 +172,6 @@ const tenderListSpec = (function(){
     }
     function getTenderTreeHeaderHtml() {
         colSetCache = generateColSetCache();
-        console.log(colSetCache);
         const html = [];
         html.push('<table class="table table-hover table-bordered">');
         html.push('<thead style="position: fixed;left:56px;top: 34px;">', '<tr>');

+ 31 - 0
app/router.js

@@ -53,6 +53,10 @@ module.exports = app => {
     const constructionCheck = app.middlewares.constructionCheck();
     // 合同管理中间件
     const contractCheck = app.middlewares.contractCheck();
+    // 资金监管中间件
+    const financialCheck = app.middlewares.financialCheck();
+    const financialPayCheck = app.middlewares.financialPayCheck();
+    const financialPayAuditCheck = app.middlewares.financialPayAuditCheck();
     // 登入登出相关
     app.get('/login', 'loginController.index');
     app.get('/login/:code', 'loginController.index');
@@ -345,6 +349,8 @@ module.exports = app => {
     app.post('/tender/:id/measure/stage/:order/im-file/upload', sessionAuth, tenderCheck, uncheckTenderCheck, stageCheck, 'stageController.uploadImFile');
     app.post('/tender/:id/measure/stage/:order/im-file/del', sessionAuth, tenderCheck, uncheckTenderCheck, stageCheck, 'stageController.deleteImFile');
     app.get('/tender/:id/measure/stage/:order/im-file/download', sessionAuth, tenderCheck, uncheckTenderCheck, stageCheck, 'stageController.downloadImFile');
+    app.get('/tender/:id/measure/stage/:order/cpd', sessionAuth, tenderCheck, uncheckTenderCheck, stageCheck, 'stageController.exportStageData');
+    app.post('/tender/:id/measure/stage/:order/cpd/load', sessionAuth, tenderCheck, uncheckTenderCheck, stageCheck, 'stageController.importStageData');
 
     // 暂存计量
     app.post('/tender/:id/measure/stage/:order/stash/list', sessionAuth, tenderCheck, stageCheck, 'stageController.stashList');
@@ -947,4 +953,29 @@ module.exports = app => {
     app.post('/contract/:stid/detail/:type/:cid/pay/:cpid/file/upload', sessionAuth, contractCheck, 'contractController.uploadFile');
     app.post('/contract/:stid/detail/:type/:cid/pay/:cpid/file/delete', sessionAuth, contractCheck, 'contractController.deleteFile');
     app.get('/contract/:stid/detail/:type/:cid/pay/:cpid/file/:fid/download', sessionAuth, contractCheck, 'contractController.downloadFile');
+
+    // 资金监管
+    app.get('/financial', sessionAuth, 'financialController.index');
+    app.post('/financial/:spid/audit/save', sessionAuth, financialCheck, 'financialController.auditSave');
+    app.get('/financial/:spid/transfer', sessionAuth, financialCheck, 'financialController.transfer');
+    app.post('/financial/:spid/transfer/add', sessionAuth, financialCheck, 'financialController.transferAdd');
+    app.post('/financial/:spid/transfer/update', sessionAuth, financialCheck, 'financialController.transferUpdate');
+    app.post('/financial/:spid/transfer/:id/file/upload', sessionAuth, financialCheck, 'financialController.transferUploadFile');
+    app.post('/financial/:spid/transfer/:id/file/delete', sessionAuth, financialCheck, 'financialController.transferDeleteFile');
+    app.get('/financial/:spid/transfer/:id/file/:fid/download', sessionAuth, financialCheck, 'financialController.transferDownloadFile');
+    app.get('/financial/:spid/transfer/:id/tender', sessionAuth, financialCheck, 'financialController.transferTender');
+    app.post('/financial/:spid/transfer/:id/tender/update', sessionAuth, financialCheck, 'financialController.transferTenderUpdate');
+    app.post('/financial/:spid/transfer/:trid/tender/:id/file/upload', sessionAuth, financialCheck, 'financialController.transferTenderUploadFile');
+    app.post('/financial/:spid/transfer/:trid/tender/:id/file/delete', sessionAuth, financialCheck, 'financialController.transferTenderDeleteFile');
+    app.get('/financial/:spid/transfer/:trid/tender/:id/file/:fid/download', sessionAuth, financialCheck, 'financialController.transferTenderDownloadFile');
+    app.get('/financial/:spid/pay', sessionAuth, financialCheck, 'financialController.pay');
+    app.post('/financial/:spid/pay/save', sessionAuth, financialCheck, 'financialController.paySave');
+    app.get('/financial/:spid/pay/:fpid/detail', sessionAuth, financialCheck, financialPayCheck, financialPayAuditCheck, 'financialController.payDetail');
+    app.post('/financial/:spid/pay/:fpid/save', sessionAuth, financialCheck, financialPayCheck, 'financialController.payDetailSave');
+    app.post('/financial/:spid/pay/:fpid/file/upload', sessionAuth, financialCheck, financialPayCheck, financialPayAuditCheck, 'financialController.payUploadFile');
+    app.post('/financial/:spid/pay/:fpid/file/delete', sessionAuth, financialCheck, financialPayCheck, financialPayAuditCheck, 'financialController.payDeleteFile');
+    app.get('/financial/:spid/pay/:fpid/file/:fid/download', sessionAuth, financialCheck, financialPayCheck, financialPayAuditCheck, 'financialController.payDownloadFile');
+    app.post('/financial/:spid/pay/:fpid/audit/start', sessionAuth, financialCheck, financialPayCheck, financialPayAuditCheck, 'financialController.startPayAudit');
+    app.post('/financial/:spid/pay/:fpid/audit/check', sessionAuth, financialCheck, financialPayCheck, 'financialController.checkPayAudit');
+    // app.get('/financial/:spid/summary', sessionAuth, financialCheck, 'financialController.summary');
 };

+ 36 - 36
app/service/change.js

@@ -363,31 +363,30 @@ module.exports = app => {
                     case 5: // 待上报(所有的)PS:取未上报,退回,修订的变更令
                         sql =
                             'SELECT a.* FROM ?? AS a WHERE' +
-                            ' ((a.status != ? AND a.cid IN (SELECT b.cid FROM ?? AS b WHERE b.uid = ? AND a.times = b.times GROUP BY b.cid))' +
-                            ' OR (a.status = ? AND a.cid IN (SELECT b.cid FROM ?? AS b WHERE b.uid = ? AND a.times - 1 = b.times GROUP BY b.cid)))' +
-                            // 'a.cid IN (SELECT b.cid FROM ?? AS b WHERE b.uid = ? GROUP BY b.cid)' +
-                            ' AND (a.status = ? OR a.status = ? OR a.status = ?) AND a.tid = ?' + stateSql;
+                            ' (a.status = ? OR a.status = ? OR a.status = ?) AND a.tid = ?' + stateSql +
+                            (this.ctx.session.sessionUser.is_admin ? '' : ' AND ((a.status != ? AND a.cid IN (SELECT b.cid FROM ?? AS b WHERE b.uid = ? AND a.times = b.times GROUP BY b.cid))' +
+                            ' OR (a.status = ? AND a.cid IN (SELECT b.cid FROM ?? AS b WHERE b.uid = ? AND a.times - 1 = b.times GROUP BY b.cid)))');
                         sqlParam = [
                             this.tableName,
                             audit.change.status.uncheck,
+                            audit.change.status.checkNo,
+                            audit.change.status.revise,
+                            tenderId,
+                            audit.change.status.uncheck,
                             this.ctx.service.changeAudit.tableName,
                             this.ctx.session.sessionUser.accountId,
                             audit.change.status.checkNo,
                             this.ctx.service.changeAudit.tableName,
                             this.ctx.session.sessionUser.accountId,
-                            audit.change.status.uncheck,
-                            audit.change.status.checkNo,
-                            audit.change.status.revise,
-                            tenderId,
                         ];
                         break;
                     case 2: // 进行中(所有的)
                     case 4: // 终止(所有的)
                         sql =
                             'SELECT a.* FROM ?? AS a WHERE ' +
-                            'a.cid IN (SELECT b.cid FROM ?? AS b WHERE b.uid = ? AND a.times = b.times GROUP BY b.cid) AND ' +
-                            'a.status = ? AND a.tid = ?' + stateSql;
-                        sqlParam = [this.tableName, this.ctx.service.changeAudit.tableName, this.ctx.session.sessionUser.accountId, status, tenderId];
+                            'a.status = ? AND a.tid = ?' + stateSql +
+                            (this.ctx.session.sessionUser.is_admin ? '' : ' AND a.cid IN (SELECT b.cid FROM ?? AS b WHERE b.uid = ? AND a.times = b.times GROUP BY b.cid)');
+                        sqlParam = [this.tableName, status, tenderId, this.ctx.service.changeAudit.tableName, this.ctx.session.sessionUser.accountId];
                         break;
                     case 3: // 已完成(所有的)
                         sql = 'SELECT a.* FROM ?? AS a WHERE a.status = ? AND a.tid = ?' + stateSql;
@@ -464,22 +463,21 @@ module.exports = app => {
                 case 5: // 待上报(所有的)PS:取未上报,退回,修订的变更令
                     const sql2 =
                         'SELECT count(*) AS count FROM ?? AS a WHERE' +
-                        ' ((a.status != ? AND a.cid IN (SELECT b.cid FROM ?? AS b WHERE b.uid = ? AND a.times = b.times GROUP BY b.cid))' +
-                        ' OR (a.status = ? AND a.cid IN (SELECT b.cid FROM ?? AS b WHERE b.uid = ? AND a.times - 1 = b.times GROUP BY b.cid)))' +
-                        // 'a.cid IN (SELECT b.cid FROM ?? AS b WHERE b.uid = ? AND a.times = b.times GROUP BY b.cid) ' +
-                        ' AND (a.status = ? OR a.status = ? OR a.status = ?) AND a.tid = ?' + stateSql;
+                        ' (a.status = ? OR a.status = ? OR a.status = ?) AND a.tid = ?' + stateSql +
+                        (this.ctx.session.sessionUser.is_admin ? '' : ' AND ((a.status != ? AND a.cid IN (SELECT b.cid FROM ?? AS b WHERE b.uid = ? AND a.times = b.times GROUP BY b.cid))' +
+                            ' OR (a.status = ? AND a.cid IN (SELECT b.cid FROM ?? AS b WHERE b.uid = ? AND a.times - 1 = b.times GROUP BY b.cid)))');
                     const sqlParam2 = [
                         this.tableName,
                         audit.change.status.uncheck,
+                        audit.change.status.checkNo,
+                        audit.change.status.revise,
+                        tenderId,
+                        audit.change.status.uncheck,
                         this.ctx.service.changeAudit.tableName,
                         this.ctx.session.sessionUser.accountId,
                         audit.change.status.checkNo,
                         this.ctx.service.changeAudit.tableName,
                         this.ctx.session.sessionUser.accountId,
-                        audit.change.status.uncheck,
-                        audit.change.status.checkNo,
-                        audit.change.status.revise,
-                        tenderId,
                     ];
                     const result2 = await this.db.query(sql2, sqlParam2);
                     return result2[0].count;
@@ -487,8 +485,9 @@ module.exports = app => {
                 case 4: // 终止(所有的)
                     const sql3 =
                         'SELECT count(*) AS count FROM ?? AS a WHERE ' +
-                        'a.cid IN (SELECT b.cid FROM ?? AS b WHERE b.uid = ? AND a.times = b.times GROUP BY b.cid) AND a.status = ? AND a.tid = ?' + stateSql;
-                    const sqlParam3 = [this.tableName, this.ctx.service.changeAudit.tableName, this.ctx.session.sessionUser.accountId, status, tenderId];
+                        'a.status = ? AND a.tid = ?' + stateSql +
+                        (this.ctx.session.sessionUser.is_admin ? '' : ' AND a.cid IN (SELECT b.cid FROM ?? AS b WHERE b.uid = ? AND a.times = b.times GROUP BY b.cid)');
+                    const sqlParam3 = [this.tableName, status, tenderId, this.ctx.service.changeAudit.tableName, this.ctx.session.sessionUser.accountId];
                     const result3 = await this.db.query(sql3, sqlParam3);
                     return result3[0].count;
                 case 3: // 已完成(所有的)
@@ -501,9 +500,10 @@ module.exports = app => {
             }
         }
 
-        async getTp(tenderId, status) {
+        async getTp(tenderId, status, state) {
+            const stateSql = state ? ' AND a.state = ' + state : '';
             if ((this.ctx.tender.isTourist || this.ctx.session.sessionUser.is_admin) && status === 0) {
-                const sql5 = 'SELECT SUM(cast (total_price as decimal(18,6))) AS total_price FROM ?? WHERE tid = ?';
+                const sql5 = 'SELECT SUM(cast (total_price as decimal(18,6))) AS total_price FROM ?? AS a WHERE a.tid = ?' + stateSql;
                 const sqlParam5 = [this.tableName, tenderId];
                 const result5 = await this.db.query(sql5, sqlParam5);
                 return result5[0].total_price ? result5[0].total_price : 0;
@@ -514,7 +514,7 @@ module.exports = app => {
                         'SELECT SUM(cast (a.total_price as decimal(18,6))) AS total_price FROM ?? AS a WHERE a.tid = ? AND ' +
                         '(a.uid = ? OR (a.status != ? AND a.cid IN (SELECT b.cid FROM ?? AS b WHERE b.uid = ? AND a.times = b.times GROUP BY b.cid))' +
                         ' OR (a.status = ? AND a.cid IN (SELECT b.cid FROM ?? AS b WHERE b.uid = ? AND a.times - 1 = b.times GROUP BY b.cid))' +
-                        ' OR a.status = ? )';
+                        ' OR a.status = ? )' + stateSql;
                     const sqlParam = [
                         this.tableName,
                         tenderId,
@@ -530,29 +530,28 @@ module.exports = app => {
                     const result = await this.db.query(sql, sqlParam);
                     return result[0].total_price ? result[0].total_price : 0;
                 case 1: // 待处理(你的)
-                    const sql6 = 'SELECT SUM(cast (a.total_price as decimal(18,6))) AS total_price FROM ?? as a WHERE cid in(SELECT b.cid FROM ?? as b WHERE tid = ? AND uid = ? AND status = ?)';
+                    const sql6 = 'SELECT SUM(cast (a.total_price as decimal(18,6))) AS total_price FROM ?? as a WHERE cid in(SELECT b.cid FROM ?? as b WHERE tid = ? AND uid = ? AND status = ?)' + stateSql;
                     const sqlParam6 = [this.tableName, this.ctx.service.changeAudit.tableName, tenderId, this.ctx.session.sessionUser.accountId, audit.change.status.checking];
                     const result6 = await this.db.query(sql6, sqlParam6);
                     return result6[0].total_price ? result6[0].total_price : 0;
                 case 5: // 待上报(所有的)PS:取未上报,退回,修订的变更令
                     const sql2 =
                         'SELECT SUM(cast (a.total_price as decimal(18,6))) AS total_price FROM ?? AS a WHERE' +
-                        ' ((a.status != ? AND a.cid IN (SELECT b.cid FROM ?? AS b WHERE b.uid = ? AND a.times = b.times GROUP BY b.cid))' +
-                        ' OR (a.status = ? AND a.cid IN (SELECT b.cid FROM ?? AS b WHERE b.uid = ? AND a.times - 1 = b.times GROUP BY b.cid)))' +
-                        // 'a.cid IN (SELECT b.cid FROM ?? AS b WHERE b.uid = ? AND a.times = b.times GROUP BY b.cid) ' +
-                        ' AND (a.status = ? OR a.status = ? OR a.status = ?) AND a.tid = ?';
+                        ' (a.status = ? OR a.status = ? OR a.status = ?) AND a.tid = ?' + stateSql +
+                        (this.ctx.session.sessionUser.is_admin ? '' : ' AND ((a.status != ? AND a.cid IN (SELECT b.cid FROM ?? AS b WHERE b.uid = ? AND a.times = b.times GROUP BY b.cid))' +
+                            ' OR (a.status = ? AND a.cid IN (SELECT b.cid FROM ?? AS b WHERE b.uid = ? AND a.times - 1 = b.times GROUP BY b.cid)))');
                     const sqlParam2 = [
                         this.tableName,
                         audit.change.status.uncheck,
+                        audit.change.status.checkNo,
+                        audit.change.status.revise,
+                        tenderId,
+                        audit.change.status.uncheck,
                         this.ctx.service.changeAudit.tableName,
                         this.ctx.session.sessionUser.accountId,
                         audit.change.status.checkNo,
                         this.ctx.service.changeAudit.tableName,
                         this.ctx.session.sessionUser.accountId,
-                        audit.change.status.uncheck,
-                        audit.change.status.checkNo,
-                        audit.change.status.revise,
-                        tenderId,
                     ];
                     const result2 = await this.db.query(sql2, sqlParam2);
                     return result2[0].total_price ? result2[0].total_price : 0;
@@ -560,12 +559,13 @@ module.exports = app => {
                 case 4: // 终止(所有的)
                     const sql3 =
                         'SELECT SUM(cast (a.total_price as decimal(18,6))) AS total_price FROM ?? AS a WHERE ' +
-                        'a.cid IN (SELECT b.cid FROM ?? AS b WHERE b.uid = ? AND a.times = b.times GROUP BY b.cid) AND a.status = ? AND a.tid = ?';
-                    const sqlParam3 = [this.tableName, this.ctx.service.changeAudit.tableName, this.ctx.session.sessionUser.accountId, status, tenderId];
+                        'a.status = ? AND a.tid = ?' + stateSql +
+                        (this.ctx.session.sessionUser.is_admin ? '' : ' AND a.cid IN (SELECT b.cid FROM ?? AS b WHERE b.uid = ? AND a.times = b.times GROUP BY b.cid)');
+                    const sqlParam3 = [this.tableName, status, tenderId, this.ctx.service.changeAudit.tableName, this.ctx.session.sessionUser.accountId];
                     const result3 = await this.db.query(sql3, sqlParam3);
                     return result3[0].total_price ? result3[0].total_price : 0;
                 case 3: // 已完成(所有的)
-                    const sql4 = 'SELECT SUM(cast (total_price as decimal(18,6))) AS total_price FROM ?? WHERE status = ? AND tid = ?';
+                    const sql4 = 'SELECT SUM(cast (total_price as decimal(18,6))) AS total_price FROM ?? AS a WHERE a.status = ? AND a.tid = ?' + stateSql;
                     const sqlParam4 = [this.tableName, status, tenderId];
                     const result4 = await this.db.query(sql4, sqlParam4);
                     return result4[0].total_price ? result4[0].total_price : 0;

+ 1 - 1
app/service/construction_unit.js

@@ -119,7 +119,7 @@ module.exports = app => {
         }
 
         async getReportData(pid) {
-            const data = this.getAllDataByCondition({ where: { pid } });
+            const data = await this.getAllDataByCondition({ where: { pid } });
             data.forEach(x => {
                 x.type_str = accountGroup.group[x.type];
             });

+ 33 - 0
app/service/extra_pay.js

@@ -0,0 +1,33 @@
+'use strict';
+
+/**
+ *
+ *
+ * @author Mai
+ * @date
+ * @version
+ */
+
+const payConst = require('../const/deal_pay');
+const auditConst = require('../const/audit');
+
+module.exports = app => {
+    class ExtraPay extends app.BaseService {
+        /**
+         * 构造函数
+         *
+         * @param {Object} ctx - egg全局变量
+         * @return {void}
+         */
+        constructor(ctx) {
+            super(ctx);
+            this.tableName = 'extra_pay';
+        }
+
+        async AddExtraPay() {
+
+        }
+    }
+
+    return ExtraPay;
+};

+ 108 - 0
app/service/financial_audit.js

@@ -0,0 +1,108 @@
+'use strict';
+
+/**
+ * Created by EllisRan on 2020/3/3.
+ */
+
+const BaseService = require('../base/base_service');
+
+module.exports = app => {
+
+    class FinancialAudit extends BaseService {
+
+        /**
+         * 构造函数
+         *
+         * @param {Object} ctx - egg全局变量
+         * @return {void}
+         */
+        constructor(ctx) {
+            super(ctx);
+            this.tableName = 'financial_audit';
+            this.dataId = 'id';
+        }
+
+        async getList(spid) {
+            const list = await this.db.select(this.tableName, { where: { spid }, orders: [['id', 'desc']] });
+            for (const l of list) {
+                const accountInfo = await this.ctx.service.projectAccount.getDataById(l.uid);
+                l.name = accountInfo.name;
+                l.role = accountInfo.role;
+                l.company = accountInfo.company;
+                l.mobile = accountInfo.mobile;
+            }
+            return list;
+        }
+
+        async saveAudits(spid, accountList, transaction = null) {
+            // 判断是否已存在该用户,存在则不插入
+            const pauditList = await this.getAllDataByCondition({ where: { spid } });
+            const pushData = [];
+            for (const a of this._.uniqBy(accountList, 'id')) {
+                if (this._.findIndex(pauditList, { uid: a.id }) === -1) {
+                    const data = {
+                        spid,
+                        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);
+            }
+            return false;
+        }
+
+        async delAudit(id) {
+            return await this.db.delete(this.tableName, { id });
+        }
+
+        async updatePermission(updateData) {
+            if (!updateData.id) {
+                return false;
+            }
+            return await this.db.update(this.tableName, updateData);
+        }
+
+        async checkPermission(spid, uid) {
+            if (this.ctx.session.sessionUser.is_admin) {
+                return true;
+            }
+            let flag = false;
+            const info = await this.getDataByCondition({ spid, uid });
+            if (info) {
+                flag = true;
+            }
+            return flag;
+        }
+
+        async getPermission(spid, uid) {
+            const permission = {
+                transfer_show: false,
+                transfer_add: false,
+                transfer_file: false,
+                pay_show: false,
+                pay_file: false,
+            };
+            if (this.ctx.session.sessionUser.is_admin) {
+                permission.transfer_show = true;
+                permission.transfer_add = true;
+                permission.transfer_file = true;
+                permission.pay_show = true;
+                permission.pay_file = true;
+            } else {
+                const auditInfo = await this.getDataByCondition({ spid, uid });
+                if (auditInfo) {
+                    permission.transfer_show = auditInfo.permission_transfer_show === 1;
+                    permission.transfer_add = auditInfo.permission_transfer_add === 1;
+                    permission.transfer_file = auditInfo.permission_transfer_file === 1;
+                    permission.pay_show = auditInfo.permission_pay_show === 1;
+                    permission.pay_file = auditInfo.permission_pay_file === 1;
+                }
+            }
+            return permission;
+        }
+    }
+    return FinancialAudit;
+};

+ 289 - 0
app/service/financial_pay.js

@@ -0,0 +1,289 @@
+'use strict';
+
+/**
+ * Created by EllisRan on 2020/3/3.
+ */
+
+const BaseService = require('../base/base_service');
+const auditConst = require('../const/audit').financial;
+const auditType = require('../const/audit').auditType;
+const shenpiConst = require('../const/shenpi');
+
+module.exports = app => {
+
+    class FinancialPay extends BaseService {
+
+        /**
+         * 构造函数
+         *
+         * @param {Object} ctx - egg全局变量
+         * @return {void}
+         */
+        constructor(ctx) {
+            super(ctx);
+            this.tableName = 'financial_pay';
+        }
+
+        async getOnePay(id) {
+            const pay = await this.getDataById(id);
+            const tender = await this.ctx.service.tender.getDataById(pay.tid);
+            pay.tenderName = tender.name;
+            return pay;
+        }
+
+        /**
+         * 获取列表
+         * @param {int} spid - 项目id
+         * @param {int} status - 状态
+         * @param {int} hadlimit - 分页
+         * @return {object} list - 列表
+         */
+        async getListByStatus(spid, status = 0, tid = null, used = null) {
+            let addSql = '';
+            if (tid) {
+                addSql += ' AND a.tid in (' + this.ctx.helper.getInArrStrSqlFilter(tid) + ')';
+            }
+            if (used) {
+                addSql += ' AND a.used = "' + used + '"';
+            }
+            let sql = '';
+            let sqlParam = '';
+            switch (status) {
+                case 0: // 所有
+                    sql = 'SELECT a.* FROM ?? As a WHERE a.spid = ?' + addSql;
+                    sqlParam = [this.tableName, spid];
+                    break;
+                case 1: // 待处理(你的)
+                    sql = 'SELECT a.* FROM ?? as a WHERE a.spid = ?' + addSql + ' AND (a.id in(SELECT b.fpid FROM ?? as b WHERE b.spid = ? AND b.aid = ? AND b.status = ?) OR (a.uid = ? AND (a.status = ? OR a.status = ?)))';
+                    sqlParam = [this.tableName, spid, this.ctx.service.financialPayAudit.tableName, spid, this.ctx.session.sessionUser.accountId, auditConst.status.checking, this.ctx.session.sessionUser.accountId, auditConst.status.uncheck, auditConst.status.checkNo];
+                    break;
+                case 5: // 待上报(所有的)PS:取未上报,退回,修订的变更令
+                    sql =
+                        'SELECT a.* FROM ?? as a WHERE a.spid = ?' + addSql + ' AND (a.status = ? OR a.status = ?)';
+                    sqlParam = [
+                        this.tableName,
+                        spid,
+                        auditConst.status.uncheck,
+                        auditConst.status.checkNo,
+                    ];
+                    break;
+                case 2: // 进行中(所有的)
+                case 4: // 终止(所有的)
+                    sql = 'SELECT a.* FROM ?? as a WHERE ' +
+                        'a.status = ? AND a.spid = ?' + addSql + ' AND (a.uid = ? OR ' +
+                        'a.id IN (SELECT b.fpid FROM ?? AS b WHERE b.aid = ? AND a.times = b.times GROUP BY b.fpid))';
+                    sqlParam = [this.tableName, status, spid, this.ctx.session.sessionUser.accountId,
+                        this.ctx.service.financialPayAudit.tableName, this.ctx.session.sessionUser.accountId];
+                    break;
+                case 3: // 已完成(所有的)
+                    sql = 'SELECT a.* FROM ?? as a WHERE a.status = ? AND a.spid = ?' + addSql;
+                    sqlParam = [this.tableName, status, spid];
+                    break;
+                default:
+                    break;
+            }
+            sql += ' ORDER BY a.create_time DESC';
+            const list = await this.db.query(sql, sqlParam);
+            return list;
+        }
+
+        /**
+         * 获取支付个数
+         * @param {int} spid - 项目id
+         * @param {int} status - 状态
+         * @return {void}
+         */
+        async getCountByStatus(spid, status = 0, tid = null, used = null) {
+            let addSql = '';
+            if (tid) {
+                addSql += ' AND a.tid in (' + this.ctx.helper.getInArrStrSqlFilter(tid) + ')';
+            }
+            if (used) {
+                addSql += ' AND a.used = "' + used + '"';
+            }
+            switch (status) {
+                case 0: // 所有
+                    const sql5 = 'SELECT count(*) AS count FROM ?? As a WHERE a.spid = ?' + addSql;
+                    const sqlParam5 = [this.tableName, spid];
+                    const result5 = await this.db.query(sql5, sqlParam5);
+                    return result5[0].count;
+                case 1: // 待处理(你的)
+                    const sql6 = 'SELECT count(*) AS count FROM ?? as a WHERE a.spid = ?' + addSql + ' AND (a.id in(SELECT b.fpid FROM ?? as b WHERE b.spid = ? AND b.aid = ? AND b.status = ?) OR (a.uid = ? AND (a.status = ? OR a.status = ?)))';
+                    const sqlParam6 = [this.tableName, spid, this.ctx.service.financialPayAudit.tableName, spid, this.ctx.session.sessionUser.accountId, auditConst.status.checking, this.ctx.session.sessionUser.accountId, auditConst.status.uncheck, auditConst.status.checkNo];
+                    const result6 = await this.db.query(sql6, sqlParam6);
+                    return result6[0].count;
+                case 5: // 待上报(所有的)PS:取未上报,退回的变更立项
+                    const sql2 =
+                        'SELECT count(*) AS count FROM ?? as a WHERE ' +
+                        // 'a.id IN (SELECT b.cpid FROM ?? AS b WHERE b.aid = ? AND a.times = b.times GROUP BY b.cpid) ' +
+                        'a.spid = ?' + addSql + ' AND (a.status = ? OR a.status = ?)';
+                    const sqlParam2 = [
+                        this.tableName,
+                        spid,
+                        auditConst.status.uncheck,
+                        auditConst.status.checkNo,
+                    ];
+                    const result2 = await this.db.query(sql2, sqlParam2);
+                    return result2[0].count;
+                case 2: // 进行中(所有的)
+                case 4: // 终止(所有的)
+                    const sql3 =
+                        'SELECT count(*) AS count FROM ?? as a WHERE ' +
+                        'a.status = ? AND a.spid = ?' + addSql + ' AND (a.uid = ? OR a.id IN (SELECT b.fpid FROM ?? AS b WHERE b.aid = ? AND a.times = b.times GROUP BY b.fpid))';
+                    const sqlParam3 = [this.tableName, status, spid, this.ctx.session.sessionUser.accountId, this.ctx.service.financialPayAudit.tableName, this.ctx.session.sessionUser.accountId];
+                    const result3 = await this.db.query(sql3, sqlParam3);
+                    return result3[0].count;
+                case 3: // 已完成(所有的)
+                    const sql4 = 'SELECT count(*) AS count FROM ?? as a WHERE a.status = ? AND a.spid = ?' + addSql;
+                    const sqlParam4 = [this.tableName, status, spid];
+                    const result4 = await this.db.query(sql4, sqlParam4);
+                    return result4[0].count;
+                default:
+                    break;
+            }
+        }
+
+        async addPay(spid, data) {
+            if (!data.tid || !data.code || !data.used) {
+                throw '参数错误';
+            }
+            const times = new Date();
+            const transaction = await this.db.beginTransaction();
+            try {
+                const info = await this.ctx.service.financialPay.getDataByCondition({ spid, tid: data.tid, code: data.code });
+                if (info) {
+                    throw '支付编号已存在';
+                }
+                const insertData = {
+                    spid,
+                    tid: data.tid,
+                    code: data.code,
+                    used: data.used,
+                    uid: this.ctx.session.sessionUser.accountId,
+                    status: auditConst.status.uncheck,
+                    times: 1,
+                    create_time: times,
+                };
+                const payTender = await this.ctx.service.financialPayTender.getDataByCondition({ spid, tid: data.tid });
+                if (payTender) {
+                    insertData.entity = payTender.name;
+                    insertData.bank = payTender.bank;
+                    insertData.bank_account = payTender.bank_account;
+                }
+                const result = await transaction.insert(this.tableName, insertData);
+                // 添加审批流程
+                const auditList = await this.ctx.service.shenpiAudit.getAuditList(data.tid, shenpiConst.sp_other_type.financial, shenpiConst.sp_status.gdspl);
+                const insertAuditDatas = [];
+                for (const audit of auditList) {
+                    insertAuditDatas.push({
+                        spid,
+                        tid: data.tid,
+                        fpid: result.insertId,
+                        aid: audit.audit_id,
+                        order: audit.audit_order,
+                        times: 1,
+                        status: auditConst.status.uncheck,
+                        audit_type: audit.audit_type,
+                        audit_order: audit.audit_order,
+                    });
+                }
+                if (insertAuditDatas.length > 0) await transaction.insert(this.ctx.service.financialPayAudit.tableName, insertAuditDatas);
+                await transaction.commit();
+                return { id: result.insertId };
+            } catch (e) {
+                await transaction.rollback();
+                throw e;
+            }
+        }
+
+        async delPay(fpid) {
+            if (!fpid) {
+                throw '参数有误';
+            }
+            const node = await this.getDataById(fpid);
+            if (!node) {
+                throw '资金支付不存在';
+            }
+            const transaction = await this.db.beginTransaction();
+            try {
+                await transaction.delete(this.tableName, { id: node.id });
+                // 删除合同附件
+                const attList = await this.ctx.service.financialPayAtt.getAllDataByCondition({ where: { fpid } });
+                await this.ctx.helper.delFiles(attList);
+                await transaction.delete(this.ctx.service.financialPayAtt.tableName, { fpid });
+                await transaction.delete(this.ctx.service.financialPayAudit.tableName, { fpid });
+                await transaction.delete(this.ctx.service.financialPayContract.tableName, { fpid });
+                await transaction.commit();
+            } catch (err) {
+                console.log(err);
+                await transaction.rollback();
+                throw err;
+            }
+            return true;
+        }
+
+        async savePay(fpid, postData) {
+            // 初始化事务
+            const transaction = await this.db.beginTransaction();
+            let result = false;
+            try {
+                const updateData = {
+                    id: fpid,
+                };
+                updateData[postData.name] = postData.val;
+                await transaction.update(this.tableName, updateData);
+                await transaction.commit();
+                result = true;
+            } catch (error) {
+                await transaction.rollback();
+                result = false;
+            }
+            return result;
+        }
+
+        async loadFinancialPayAuditViewData(financialPay) {
+            const times = financialPay.status === auditConst.status.checkNo ? financialPay.times - 1 : financialPay.times;
+
+            if (!financialPay.user) financialPay.user = await this.ctx.service.projectAccount.getAccountInfoById(financialPay.uid);
+            financialPay.auditHistory = await this.ctx.service.financialPayAudit.getAuditorHistory(financialPay.id, times);
+            // 获取审批流程中左边列表
+            if ((financialPay.status === auditConst.status.checkNo || financialPay.status === auditConst.status.revise) && financialPay.uid !== this.ctx.session.sessionUser.accountId && !this.ctx.session.sessionUser.is_admin) {
+                const auditors = await this.ctx.service.financialPayAudit.getAuditors(financialPay.id, times); // 全部参与的审批人
+                const auditorGroups = this.ctx.helper.groupAuditors(auditors, 'order', true);
+                financialPay.auditors2 = this.ctx.helper.groupAuditorsUniq(auditorGroups);
+                financialPay.auditors2.unshift([{
+                    aid: financialPay.user.id, order: 0, times: financialPay.times - 1, audit_order: 0, audit_type: auditType.key.common,
+                    name: financialPay.user.name, role: financialPay.user.role, company: financialPay.user.company,
+                }]);
+            } else {
+                financialPay.auditors2 = financialPay.userGroups;
+            }
+            if (financialPay.status === auditConst.status.uncheck || financialPay.status === auditConst.status.checkNo) {
+                financialPay.auditorList = await this.ctx.service.financialPayAudit.getAuditors(financialPay.id, financialPay.times);
+            }
+        }
+
+        async loadPayUser(pay) {
+            const status = auditConst.status;
+            const accountId = this.ctx.session.sessionUser.accountId;
+            pay.permission = await this.ctx.service.financialAudit.getPermission(this.ctx.subProject.id, accountId);
+            pay.user = await this.ctx.service.projectAccount.getAccountInfoById(pay.uid);
+            pay.auditors = await this.ctx.service.financialPayAudit.getAuditors(pay.id, pay.times); // 全部参与的审批人
+            pay.auditorIds = this._.map(pay.auditors, 'aid');
+            pay.curAuditors = pay.auditors.filter(x => { return x.status === status.checking; }); // 当前流程中审批中的审批人
+            pay.curAuditorIds = this._.map(pay.curAuditors, 'aid');
+            pay.flowAuditors = pay.curAuditors.length > 0 ? pay.auditors.filter(x => { return x.order === pay.curAuditors[0].order; }) : []; // 当前流程中参与的审批人(包含会签时,审批通过的人)
+            pay.flowAuditorIds = this._.map(pay.flowAuditors, 'aid');
+            pay.nextAuditors = pay.curAuditors.length > 0 ? pay.auditors.filter(x => { return x.order === pay.curAuditors[0].order + 1; }) : [];
+            pay.nextAuditorIds = this._.map(pay.nextAuditors, 'aid');
+            pay.auditorGroups = this.ctx.helper.groupAuditors(pay.auditors, 'order', true);
+            pay.userGroups = this.ctx.helper.groupAuditorsUniq(pay.auditorGroups);
+            pay.userGroups.unshift([{
+                aid: pay.user.id, order: 0, times: pay.times, audit_order: 0, audit_type: auditType.key.common,
+                name: pay.user.name, role: pay.user.role, company: pay.user.company,
+            }]);
+            pay.finalAuditorIds = pay.userGroups[pay.userGroups.length - 1].map(x => { return x.aid; });
+        }
+    }
+    return FinancialPay;
+};

+ 65 - 0
app/service/financial_pay_att.js

@@ -0,0 +1,65 @@
+'use strict';
+
+/**
+ * Created by EllisRan on 2020/3/3.
+ */
+
+const BaseService = require('../base/base_service');
+
+module.exports = app => {
+
+    class FinancialPayAtt extends BaseService {
+
+        /**
+         * 构造函数
+         *
+         * @param {Object} ctx - egg全局变量
+         * @return {void}
+         */
+        constructor(ctx) {
+            super(ctx);
+            this.tableName = 'financial_pay_attachment';
+            this.dataId = 'id';
+        }
+
+        async getAtt(fpid, fpcid = null) {
+            const fpcidSql = fpcid ? ' AND fpcid = ' + fpcid : ' AND fpcid IS NULL';
+            const sql = 'SELECT a.*, b.name as username FROM ?? as a LEFT JOIN ?? as b ON a.`uid` = b.`id` WHERE a.`fpid` = ?' + fpcidSql + ' ORDER BY `upload_time` DESC';
+            const sqlParam = [this.tableName, this.ctx.service.projectAccount.tableName, fpid];
+            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 = `/financial/${item.spid}/pay/${item.fpid}/file/${item.id}/download`;
+                } else {
+                    item.filepath = this.ctx.app.config.fujianOssPath + item.filepath;
+                    item.viewpath = item.filepath;
+                }
+                return item;
+            });
+        }
+
+        async updateBill(id, bill) {
+            return await this.db.update(this.tableName, { id, bill });
+        }
+
+        /**
+         * 存储上传的文件信息至数据库
+         * @param {Array} payload 载荷
+         * @return {Promise<void>} 数据库插入执行实例
+         */
+        async saveFileMsgToDb(payload) {
+            return await this.db.insert(this.tableName, payload);
+        }
+
+        /**
+         * 删除附件
+         * @param {Number} id - 附件id
+         * @return {void}
+         */
+        async delete(id) {
+            return await this.deleteById(id);
+        }
+    }
+    return FinancialPayAtt;
+};

+ 535 - 0
app/service/financial_pay_audit.js

@@ -0,0 +1,535 @@
+'use strict';
+
+/**
+ *
+ *
+ * @author Mai
+ * @date 2019/2/27
+ * @version
+ */
+
+const auditConst = require('../const/audit').financial;
+const auditType = require('../const/audit').auditType;
+const shenpiConst = require('../const/shenpi');
+const pushType = require('../const/audit').pushType;
+
+module.exports = app => {
+    class FinancialPayAudit extends app.BaseService {
+        /**
+         * 构造函数
+         *
+         * @param {Object} ctx - egg全局变量
+         * @return {void}
+         */
+        constructor(ctx) {
+            super(ctx);
+            this.tableName = 'financial_pay_audit';
+        }
+
+        async getAuditorGroup(fpId, times) {
+            const auditors = await this.getAuditors(fpId, times); // 全部参与的审批人
+            return this.ctx.helper.groupAuditors(auditors, 'order', true);
+        }
+
+        async getUserGroup(fpId, times) {
+            const group = await this.getAuditorGroup(fpId, times);
+            const sql =
+                'SELECT pa.`id` As aid, pa.`name`, pa.`company`, pa.`role`, ? As times, ? As fpid, 0 As `order`, 1 As audit_type, 0 As audit_order' +
+                '  FROM ' + this.ctx.service.financialPay.tableName + ' As s' +
+                '  LEFT JOIN ' + this.ctx.service.projectAccount.tableName + ' As pa' +
+                '  ON s.uid = pa.id' +
+                '  WHERE s.id = ?';
+            const sqlParam = [times, fpId, fpId];
+            const user = await this.db.queryOne(sql, sqlParam);
+            user.audit_order = 0;
+            group.unshift([ user ]);
+            return group;
+        }
+
+        async getUniqUserGroup(fpId, times) {
+            const group = await this.getAuditorGroup(fpId, times);
+            const sql =
+                'SELECT pa.`id` As aid, pa.`name`, pa.`company`, pa.`role`, ? As times, ? As fpid, 0 As `order`, 1 As audit_type, 0 As audit_order' +
+                '  FROM ' + this.ctx.service.financialPay.tableName + ' As s' +
+                '  LEFT JOIN ' + this.ctx.service.projectAccount.tableName + ' As pa' +
+                '  ON s.uid = pa.id' +
+                '  WHERE s.id = ?';
+            const sqlParam = [times, fpId, fpId];
+            const user = await this.db.queryOne(sql, sqlParam);
+            user.audit_order = 0;
+            group.unshift([ user ]);
+            return this.ctx.helper.groupAuditorsUniq(group);
+        }
+
+        async getAuditorHistory(fpId, times, reverse = false) {
+            const history = [];
+            if (times >= 1) {
+                for (let i = 1; i <= times; i++) {
+                    const auditors = await this.getAuditors(fpId, i);
+                    const group = this.ctx.helper.groupAuditors(auditors);
+                    const historyGroup = [];
+                    // 找出group里audit_order最大值
+                    const max_info = group.length > 0 ? this._.maxBy(group, function(item) {
+                        return item && item[0] && item[0].audit_order;
+                    }) : null;
+                    const max_order = max_info ? max_info[0].audit_order : -1;
+                    for (const g of group) {
+                        const his = {
+                            beginYear: '', beginDate: '', beginTime: '', endYear: '', endDate: '', endTime: '', begin_time: null, end_time: null,
+                            audit_type: g[0].audit_type, audit_order: g[0].audit_order,
+                            auditors: g,
+                        };
+                        if (his.audit_type === auditType.key.common) {
+                            his.name = g[0].name;
+                        } else {
+                            his.name = this.ctx.helper.transFormToChinese(his.audit_order) + '审';
+                        }
+                        his.is_final = his.audit_order === max_order;
+                        if (g[0].begin_time) {
+                            his.begin_time = g[0].begin_time;
+                            const beginTime = this.ctx.moment(g[0].begin_time);
+                            his.beginYear = beginTime.format('YYYY');
+                            his.beginDate = beginTime.format('MM-DD');
+                            his.beginTime = beginTime.format('HH:mm:ss');
+                        }
+                        let end_time;
+                        g.forEach(x => {
+                            if (x.status === auditConst.status.checkSkip) return;
+                            if (!his.status || x.status === auditConst.status.checking) his.status = x.status;
+                            if (x.end_time && (!end_time || x.end_time > end_time)) {
+                                end_time = x.end_time;
+                                if (his.status !== auditConst.status.checking) his.status = x.status;
+                            }
+                        });
+                        if (end_time) {
+                            his.end_time = end_time;
+                            const endTime = this.ctx.moment(end_time);
+                            his.endYear = endTime.format('YYYY');
+                            his.endDate = endTime.format('MM-DD');
+                            his.endTime = endTime.format('HH:mm:ss');
+                        }
+                        historyGroup.push(his);
+                    }
+                    if (reverse) {
+                        history.push(historyGroup.reverse());
+                    } else {
+                        history.push(historyGroup);
+                    }
+                }
+            }
+            return history;
+        }
+
+        async getUniqAuditor(fpId, times) {
+            const auditors = await this.getAuditors(fpId, times); // 全部参与的审批人
+            const result = [];
+            auditors.forEach(x => {
+                if (result.findIndex(r => { return x.aid === r.aid && x.audit_order === r.audit_order; }) < 0) {
+                    result.push(x);
+                }
+            });
+            return result;
+        }
+
+        /**
+         * 获取 审核列表信息
+         *
+         * @param {Number} cpId - 变更立项id
+         * @param {Number} times - 第几次审批
+         * @return {Promise<*>}
+         */
+        async getAuditors(fpId, times = 1, order_sort = 'asc', noYB = false) {
+            // const sql = 'SELECT la.`aid`, pa.`name`, pa.`company`, pa.`role`, pa.`mobile`, pa.`telephone`, la.`times`, la.`order`, la.`status`, la.`opinion`, la.`begin_time`, la.`end_time`, g.`sort` ' +
+            //     'FROM ?? AS la, ?? AS pa, (SELECT t1.`aid`,(@i:=@i+1) as `sort` FROM (SELECT t.`aid`, t.`order` FROM (select `aid`, `order` from ?? WHERE `cpid` = ? AND `times` = ? ORDER BY `order` LIMIT 200) t GROUP BY t.`aid` ORDER BY t.`order`) t1, (select @i:=0) as it) as g ' +
+            //     'WHERE la.`cpid` = ? and la.`times` = ? and la.`aid` = pa.`id` and g.`aid` = la.`aid` order by la.`order`';
+            // const sqlParam = [this.tableName, this.ctx.service.projectAccount.tableName, this.tableName, cpId, times, cpId, times];
+            // const result = await this.db.query(sql, sqlParam);
+            // const sql2 = 'SELECT COUNT(a.`aid`) as num FROM (SELECT `aid` FROM ?? WHERE `cpid` = ? AND `times` = ? GROUP BY `aid`) as a';
+            // const sqlParam2 = [this.tableName, cpId, times];
+            // const count = await this.db.queryOne(sql2, sqlParam2);
+            // for (const i in result) {
+            //     result[i].max_sort = count.num;
+            // }
+            const ybSql = noYB ? ' AND la.audit_order != 0' : '';
+            const sql = 'SELECT la.id, la.aid, la.times, la.order, la.status, la.opinion, la.begin_time, la.end_time, la.audit_type, la.audit_order,' +
+                '    pa.name, pa.company, pa.role, pa.mobile, pa.telephone' +
+                `  FROM ${this.tableName} la LEFT JOIN ${this.ctx.service.projectAccount.tableName} pa ON la.aid = pa.id` +
+                '  WHERE la.fpid = ? AND la.times = ?' + ybSql +
+                '  ORDER BY la.order ' + order_sort;
+            const sqlParam = [fpId, times];
+            const result = await this.db.query(sql, sqlParam);
+            const max_sort = this._.max(result.map(x => { return x.audit_order; }));
+            for (const i in result) {
+                result[i].max_sort = max_sort;
+            }
+            return result;
+        }
+
+        async getCurAuditors(fpId, times = 1) {
+            const sql =
+                'SELECT la.`aid`, pa.`name`, pa.`company`, pa.`role`, pa.`mobile`, pa.`telephone`, la.`times`, la.`order`, la.`status`, la.`opinion`, la.`begin_time`, la.`end_time`, la.audit_type, la.audit_order ' +
+                '  FROM ?? AS la Left Join ?? AS pa On la.`aid` = pa.`id`' +
+                '  WHERE la.`fpid` = ? and la.`status` = ? and la.`times` = ?';
+            const sqlParam = [this.tableName, this.ctx.service.projectAccount.tableName, fpId, auditConst.status.checking, times];
+            return await this.db.query(sql, sqlParam);
+        }
+
+        /**
+         * 获取审核人流程列表
+         *
+         * @param auditorId
+         * @return {Promise<*>}
+         */
+        async getAuditGroupByList(fpId, times, transaction = false) {
+            const sql = 'SELECT la.`aid`, pa.`name`, pa.`company`, pa.`role`, la.`times`, la.`fpid`, la.`aid`, la.`order`, la.`status`, la.audit_type, la.audit_order ' +
+                '  FROM ?? AS la Left Join ?? AS pa On la.`aid` = pa.`id`' +
+                '  WHERE la.`fpid` = ? and la.`times` = ? GROUP BY la.`aid` ORDER BY la.`order`';
+            const sqlParam = [this.tableName, this.ctx.service.projectAccount.tableName, fpId, times];
+            return transaction !== false ? await transaction.query(sql, sqlParam) : await this.db.query(sql, sqlParam);
+        }
+
+        async getAuditorsByStatus(fpId, status, times = 1) {
+            let auditor = [];
+            let sql = '';
+            let sqlParam = '';
+            let cur;
+            switch (status) {
+                case auditConst.status.checking :
+                case auditConst.status.checked :
+                case auditConst.status.cancelRevise :
+                    cur = await this.db.queryOne('SELECT * From ?? where fpid = ? AND times = ? AND status = ? ORDER By times DESC, `order` DESC', [this.tableName, fpId, times, status]);
+                    if (!cur) return [];
+
+                    sql = 'SELECT la.`aid`, pa.`name`, pa.`company`, pa.`role`, la.`times`, la.`fpid`, la.`order`, la.`status`, la.`audit_order`, la.`audit_type` ' +
+                        '  FROM ?? AS la Left Join ?? AS pa On la.`aid` = pa.`id` ' +
+                        '  WHERE la.`fpid` = ? and la.`order` = ? and times = ?';
+                    sqlParam = [this.tableName, this.ctx.service.projectAccount.tableName, fpId, cur.order, times];
+                    auditor = await this.db.query(sql, sqlParam);
+                    break;
+                case auditConst.status.checkNo :
+                    cur = await this.db.queryOne('SELECT * From ?? where fpid = ? AND times = ? AND status = ? ORDER By times DESC, `order` DESC', [this.tableName, fpId, parseInt(times) - 1, status]);
+                    if (!cur) return [];
+
+                    sql = 'SELECT la.`aid`, pa.`name`, pa.`company`, pa.`role`, la.`times`, la.`fpid`, la.`order`, la.`status`, la.`audit_order`, la.`audit_type` ' +
+                        '  FROM ?? AS la Left Join ?? AS pa On la.`aid` = pa.`id` ' +
+                        '  WHERE la.`fpid` = ? and la.`order` = ? and la.`times` = ?';
+                    sqlParam = [this.tableName, this.ctx.service.projectAccount.tableName, fpId, cur.order, parseInt(times) - 1];
+                    auditor = await this.db.query(sql, sqlParam);
+                    break;
+                case auditConst.status.uncheck:
+                default:
+                    break;
+            }
+            return auditor;
+        }
+
+        /**
+         * 获取审核人流程列表(包括原报)
+         * @param {Number} materialId 调差id
+         * @param {Number} times 审核次数
+         * @return {Promise<Array>} 查询结果集(包括原报)
+         */
+        async getAuditorsWithOwner(fpId, times = 1) {
+            const result = await this.getAuditGroupByList(fpId, times);
+            const sql =
+                'SELECT pa.`id` As aid, pa.`name`, pa.`company`, pa.`role`, ? As times, ? As fpid, 0 As `order`' +
+                '  FROM ' +
+                this.ctx.service.financialPay.tableName +
+                ' As s' +
+                '  LEFT JOIN ' +
+                this.ctx.service.projectAccount.tableName +
+                ' As pa' +
+                '  ON s.uid = pa.id' +
+                '  WHERE s.id = ?';
+            const sqlParam = [times, fpId, fpId];
+            const user = await this.db.queryOne(sql, sqlParam);
+            result.unshift(user);
+            return result;
+        }
+
+        async updateNewAuditList(financialPay, newList) {
+            const transaction = await this.db.beginTransaction();
+            try {
+                await this.updateNewAuditors(financialPay, newList, transaction);
+                await transaction.commit();
+            } catch (err) {
+                await transaction.rollback();
+                throw err;
+            }
+        }
+
+        async updateNewAuditors(financialPay, newList, transaction) {
+            // 先删除旧的审批流,再添加新的
+            await transaction.delete(this.tableName, { fpid: financialPay.id, times: financialPay.times });
+
+            const newAuditors = [];
+            for (const auditor of newList) {
+                newAuditors.push({
+                    spid: financialPay.spid, tid: financialPay.tid, fpid: financialPay.id, aid: auditor.audit_id,
+                    times: financialPay.times, order: auditor.audit_order, status: auditConst.status.uncheck,
+                    audit_type: auditor.audit_type, audit_order: auditor.audit_order,
+                });
+            }
+            if (newAuditors.length > 0) await transaction.insert(this.tableName, newAuditors);
+        }
+
+        /**
+         * 开始审批
+         * @param {Number} cpId - 方案id
+         * @param {Number} times - 第几次审批
+         * @return {Promise<boolean>}
+         */
+        async start(fpId, times = 1) {
+            const audits = await this.getAllDataByCondition({where: {fpid: fpId, times, order: 1}});
+            if (audits.length === 0) {
+                throw '请联系管理员添加审批人';
+            }
+
+            const transaction = await this.db.beginTransaction();
+            try {
+                const begin_time = new Date();
+                const updateData = audits.map(x => {
+                    return {id: x.id, status: auditConst.status.checking, begin_time};
+                });
+                await transaction.updateRows(this.tableName, updateData);
+                await transaction.update(this.ctx.service.financialPay.tableName, {
+                    id: fpId, status: auditConst.status.checking,
+                    entities: await this.ctx.service.financialPayContract.getEntities(fpId),
+                });
+                // todo 更新标段tender状态 ?
+                await transaction.commit();
+            } catch (err) {
+                await transaction.rollback();
+                throw err;
+            }
+            return true;
+        }
+
+        /**
+         * 审批
+         * @param {Object} fp - 资金支付信息
+         * @param {auditConst.status.checked|auditConst.status.checkNo} checkType - 审批结果
+         * @return {Promise<void>}
+         */
+        async check(fp, checkData) {
+            if (checkData.checkType !== auditConst.status.checked && checkData.checkType !== auditConst.status.checkNo) {
+                throw '提交数据错误';
+            }
+            const pid = this.ctx.session.sessionProject.id;
+            switch (checkData.checkType) {
+                case auditConst.status.checked:
+                    await this._checked(pid, fp, checkData);
+                    break;
+                case auditConst.status.checkNo:
+                    await this._checkNo(pid, fp, checkData);
+                    break;
+                default:
+                    throw '无效审批操作';
+            }
+        }
+
+        async _checked(pid, fp, checkData) {
+            const accountId = this.ctx.session.sessionUser.accountId;
+            const time = new Date();
+
+            // 整理当前流程审核人状态更新
+            const audits = fp.curAuditors;
+            if (audits.length === 0) throw '审核数据错误';
+            const selfAudit = audits.find(x => { return x.aid === accountId; });
+            if (!selfAudit) throw '当前标段您无权审批';
+            // const flowAudits = await this.getAllDataByCondition({ where: { cpid: cpId, times, order: selfAudit.order } });
+            const nextAudits = fp.nextAuditors;
+
+            const transaction = await this.db.beginTransaction();
+            try {
+                // 更新本人审批状态
+                await transaction.update(this.tableName, {
+                    id: selfAudit.id,
+                    status: checkData.checkType,
+                    opinion: checkData.opinion,
+                    end_time: time,
+                });
+                // await this.ctx.service.noticeAgain.stopNoticeAgain(transaction, this.tableName, selfAudit.id);
+                // 获取推送必要信息
+                const noticeContent = await this.getNoticeContent(pid, fp.spid, fp.id, selfAudit.aid, checkData.opinion);
+                // 添加推送
+                const records = [];
+                const auditors = await this.getAuditorsWithOwner(fp.id, fp.times);
+                auditors.forEach(audit => {
+                    records.push({ pid, type: pushType.financial, uid: audit.aid, status: auditConst.status.checked, content: noticeContent });
+                });
+                await transaction.insert('zh_notice', records);
+                if (audits.length === 1 || selfAudit.audit_type !== auditType.key.and) {
+                    // 或签更新他人审批状态
+                    if (selfAudit.audit_type === auditType.key.or) {
+                        const updateOther = [];
+                        for (const audit of audits) {
+                            if (audit.aid === selfAudit.aid) continue;
+                            updateOther.push({
+                                id: audit.id,
+                                status: auditConst.status.checkSkip,
+                                opinion: '',
+                                end_time: time,
+                            });
+                            // await this.ctx.service.noticeAgain.stopNoticeAgain(transaction, this.tableName, audit.id);
+                        }
+                        if (updateOther.length > 0) transaction.updateRows(this.tableName, updateOther);
+                    }
+                    // 无下一审核人表示,审核结束
+                    if (nextAudits.length > 0) {
+                        // 流程至下一审批人
+                        const updateData = nextAudits.map(x => { return { id: x.id, status: auditConst.status.checking, begin_time: time }; });
+                        await transaction.updateRows(this.tableName, updateData);
+
+                        // 同步 期信息
+                        await transaction.update(this.ctx.service.financialPay.tableName, {
+                            id: fp.id, status: auditConst.status.checking,
+                        });
+                    } else {
+                        // 本期结束
+                        // 生成截止本期数据 final数据
+                        // 同步 期信息
+                        await transaction.update(this.ctx.service.financialPay.tableName, {
+                            id: fp.id, status: checkData.checkType,
+                        });
+                    }
+                } else {
+                    // 同步 期信息
+                    await transaction.update(this.ctx.service.financialPay.tableName, {
+                        id: fp.id,
+                        status: auditConst.status.checking,
+                    });
+                }
+                await transaction.commit();
+            } catch (err) {
+                await transaction.rollback();
+                throw err;
+            }
+        }
+
+        async _checkNo(pid, fp, checkData) {
+            const accountId = this.ctx.session.sessionUser.accountId;
+            const time = new Date();
+            const audits = fp.curAuditors;
+            if (!audits) throw '审核数据错误';
+            const selfAudit = audits.find(x => { return x.aid === accountId; });
+            if (!selfAudit) throw '当前标段您无权审批';
+            const auditors = await this.getUniqAuditor(fp.id, fp.times); // 全部参与的审批人
+            const newAuditors = auditors.map(x => {
+                return {
+                    aid: x.aid, spid: fp.spid, tid: fp.tid, fpid: fp.id,
+                    times: fp.times + 1, order: x.audit_order, status: auditConst.status.uncheck,
+                    audit_type: x.audit_type, audit_order: x.audit_order,
+                };
+            });
+            const transaction = await this.db.beginTransaction();
+            try {
+                const updateData = [];
+                audits.forEach(x => {
+                    updateData.push({
+                        id: x.id,
+                        status: x.aid === selfAudit.aid ? checkData.checkType : auditConst.status.checkSkip,
+                        opinion: x.aid === selfAudit.aid ? checkData.opinion : '',
+                        end_time: x.aid === selfAudit.aid ? time : null,
+                    });
+                });
+                await transaction.updateRows(this.tableName, updateData);
+                // 添加到消息推送表
+                const noticeContent = await this.getNoticeContent(pid, fp.spid, fp.id, selfAudit.aid, checkData.opinion);
+                const records = [{ pid, type: pushType.financial, uid: fp.uid, status: auditConst.status.checkNo, content: noticeContent }];
+                auditors.forEach(audit => {
+                    records.push({ pid, type: pushType.financial, uid: audit.aid, status: auditConst.status.checkNo, content: noticeContent });
+                });
+                await transaction.insert(this.ctx.service.noticePush.tableName, records);
+                // 同步期信息
+                await transaction.update(this.ctx.service.financialPay.tableName, {
+                    id: fp.id, status: checkData.checkType,
+                    times: fp.times + 1,
+                    entities: '',
+                });
+                // 拷贝新一次审核流程列表
+                await transaction.insert(this.tableName, newAuditors);
+                await transaction.commit();
+            } catch (err) {
+                await transaction.rollback();
+                throw err;
+            }
+        }
+
+        /**
+         * 获取审核人需要审核的期列表
+         *
+         * @param auditorId
+         * @return {Promise<*>}
+         */
+        async getAuditFinancial(auditorId) {
+            const sql = 'SELECT ma.`aid`, ma.`times`, ma.`order`, ma.`begin_time`, ma.`end_time`, ma.`spid`, ma.`fpid`,' +
+                '    m.`status` As `fpstatus`, m.`code` As `fpcode`,' +
+                '    sp.`name`, sp.`project_id` ' +
+                '  FROM ?? AS ma LEFT JOIN ?? AS m ON  ma.`fpid` = m.`id` LEFT JOIN ?? As sp ON ma.`spid` = sp.`id`' +
+                '  WHERE ((ma.`aid` = ? and ma.`status` = ?) OR (m.`uid` = ? and ma.`status` = ? and m.`status` = ? and ma.`times` = (m.`times`-1)))' +
+                '  ORDER BY ma.`begin_time` DESC';
+            const sqlParam = [this.tableName, this.ctx.service.financialPay.tableName, this.ctx.service.subProject.tableName, auditorId, auditConst.status.checking, auditorId, auditConst.status.checkNo, auditConst.status.checkNo];
+            const result = await this.db.query(sql, sqlParam);
+            // 过滤result中存在重复sid的值, 保留最新的一条
+            const filterResult = [];
+            const fpidArr = [];
+            for (const r of result) {
+                if (fpidArr.indexOf(r.fpid) === -1) {
+                    filterResult.push(r);
+                    fpidArr.push(r.fpid);
+                }
+            }
+            return filterResult;
+        }
+
+        /**
+         * 获取审核人审核的次数
+         *
+         * @param auditorId
+         * @return {Promise<*>}
+         */
+        async getCountByChecked(auditorId) {
+            return await this.db.count(this.tableName, { aid: auditorId, status: [auditConst.status.checked, auditConst.status.checkNo] });
+        }
+
+        /**
+         * 获取最近一次审批结束时间
+         *
+         * @param auditorId
+         * @return {Promise<*>}
+         */
+        async getLastEndTimeByChecked(auditorId) {
+            const sql = 'SELECT `end_time` FROM ?? WHERE `aid` = ? ' +
+                'AND `status` in (' + this.ctx.helper.getInArrStrSqlFilter([auditConst.status.checked, auditConst.status.checkNo]) + ') ORDER BY `end_time` DESC';
+            const sqlParam = [this.tableName, auditorId];
+            const result = await this.db.queryOne(sql, sqlParam);
+            return result ? result.end_time : null;
+        }
+
+        /**
+         * 用于添加推送所需的content内容
+         * @param {Number} pid 项目id
+         * @param {Number} tid 台账id
+         * @param {Number} cpId 方案id
+         * @param {Number} uid 审批人id
+         */
+        async getNoticeContent(pid, spid, fpId, uid, opinion = '') {
+            const noticeSql = 'SELECT * FROM (SELECT ' +
+                '  sp.`id` As `spid`, ma.`fpid`, m.`code`, sp.`name`, pa.`name` As `su_name`, pa.role As `su_role`' +
+                '  FROM (SELECT * FROM ?? WHERE `id` = ? ) As sp' +
+                '  LEFT JOIN ?? As m On sp.`id` = m.`spid` AND m.`id` = ?' +
+                '  LEFT JOIN ?? As ma ON m.`id` = ma.`fpid`' +
+                '  LEFT JOIN ?? As pa ON pa.`id` = ?' +
+                '  WHERE sp.`project_id` = ? ) as new_t GROUP BY new_t.`spid`';
+            const noticeSqlParam = [this.ctx.service.subProject.tableName, spid, this.ctx.service.financialPay.tableName, fpId, this.tableName, this.ctx.service.projectAccount.tableName, uid, pid];
+            const content = await this.db.query(noticeSql, noticeSqlParam);
+            if (content.length) {
+                content[0].opinion = opinion;
+            }
+            return content.length ? JSON.stringify(content[0]) : '';
+        }
+    }
+
+    return FinancialPayAudit;
+};

+ 171 - 0
app/service/financial_pay_contract.js

@@ -0,0 +1,171 @@
+'use strict';
+
+/**
+ * Created by EllisRan on 2020/3/3.
+ */
+
+const BaseService = require('../base/base_service');
+
+module.exports = app => {
+
+    class FinancialPayContract extends BaseService {
+
+        /**
+         * 构造函数
+         *
+         * @param {Object} ctx - egg全局变量
+         * @return {void}
+         */
+        constructor(ctx) {
+            super(ctx);
+            this.tableName = 'financial_pay_contract';
+            this.dataId = 'id';
+        }
+
+        async getContractList(fpid) {
+            const list = await this.getAllDataByCondition({ where: { fpid }, orders: [['id', 'asc']] });
+            for (const l of list) {
+                const allList = l.cid ? await this.getAllDataByCondition({ where: { spid: l.spid, cid: l.cid } }) : [];
+                l.accumulate_pay_price = this._.sumBy(allList, 'pay_price');
+                l.accumulate_settle_price = this._.sumBy(allList, 'settle_price');
+                l.files = await this.ctx.service.financialPayAtt.getAtt(l.fpid, l.id);
+            }
+            return list;
+        }
+
+        async addWhiteContract(fp) {
+            const insertData = {
+                spid: fp.spid,
+                tid: fp.tid,
+                fpid: fp.id,
+            };
+            const result = await this.db.insert(this.tableName, insertData);
+            const result2 = await this.getDataById(result.insertId);
+            result2.files = [];
+            return result2;
+        }
+
+        async addContracts(fp, cids) {
+            const transaction = await this.db.beginTransaction();
+            try {
+                const list = await this.getAllDataByCondition({ where: { fpid: fp.id, cid: cids } });
+                if (list.length > 0) {
+                    throw '合同已存在, 无法重复添加';
+                }
+                const contracts = await this.ctx.service.contract.getAllDataByCondition({ where: { id: cids } });
+                const insertDatas = [];
+                for (const c of contracts) {
+                    insertDatas.push({
+                        spid: c.spid,
+                        tid: fp.tid,
+                        fpid: fp.id,
+                        cid: c.id,
+                        c_code: c.c_code,
+                        name: c.name,
+                        total_price: c.total_price,
+                        entity: c.entity,
+                        bank: c.bank,
+                        bank_account: c.bank_account,
+                    });
+                }
+                if (insertDatas.length > 0) await transaction.insert(this.tableName, insertDatas);
+                // 重新算资金支付总额
+                // const tp = await this.calcCamountSum(fpid, transaction);
+                await transaction.commit();
+                return { contractList: await this.getContractList(fp.id) };
+            } catch (err) {
+                await transaction.rollback();
+                throw err;
+            }
+        }
+
+        async delContract(fpid, ids) {
+            const transaction = await this.db.beginTransaction();
+            try {
+                // 还要删除附件
+                const attList = await this.ctx.service.financialPayAtt.getAllDataByCondition({ where: { fpid, fpcid: ids } });
+                await this.ctx.helper.delFiles(attList);
+                await transaction.delete(this.ctx.service.financialPayAtt.tableName, { fpcid: ids });
+                // 判断是否可删
+                await transaction.delete(this.tableName, { id: ids });
+                // 重新算资金支付总额
+                const tp = await this.calcCamountSum(fpid, transaction);
+                await transaction.commit();
+                return { tp, contractList: await this.getContractList(fpid) };
+            } catch (err) {
+                await transaction.rollback();
+                throw err;
+            }
+        }
+
+        async updateContract(fpid, data) {
+            const transaction = await this.db.beginTransaction();
+            try {
+                await transaction.update(this.tableName, data);
+                const tp = await this.calcCamountSum(fpid, transaction);
+                await transaction.commit();
+                return { tp };
+            } catch (err) {
+                await transaction.rollback();
+                throw err;
+            }
+        }
+
+        /**
+         * 修改变更清单 复制粘贴
+         * @param {Object} datas 修改内容
+         * @return {void}
+         */
+        async updateContracts(fpid, datas) {
+            const transaction = await this.db.beginTransaction();
+            try {
+                const updateArray = [];
+                for (const d of datas) {
+                    if (d.id) {
+                        updateArray.push(d);
+                    }
+                }
+                if (updateArray.length > 0) await transaction.updateRows(this.tableName, updateArray);
+                const tp = await this.calcCamountSum(fpid, transaction);
+                await transaction.commit();
+                return { tp, contractList: await this.getContractList(fpid) };
+            } catch (err) {
+                await transaction.rollback();
+                throw err;
+            }
+        }
+
+        async calcCamountSum(fpid, transaction) {
+            // 防止小数位不精确,采用取值计算
+            const sql = 'SELECT `small_expenses`, `pay_price` FROM ?? WHERE fpid = ?';
+            const sqlParam = [this.tableName, fpid];
+            const list = await transaction.query(sql, sqlParam);
+            let total_price = 0;
+            let small_expenses_tp = 0;
+            // const tp_decimal = this.ctx.change.decimal.tp;
+            for (const l of list) {
+                total_price = this.ctx.helper.accAdd(total_price, l.pay_price || 0);
+                small_expenses_tp = this.ctx.helper.accAdd(small_expenses_tp, l.small_expenses ? l.pay_price || 0 : 0);
+            }
+            const updateData = {
+                id: fpid,
+                total_price,
+                small_expenses_tp,
+            };
+            await transaction.update(this.ctx.service.financialPay.tableName, updateData);
+            return total_price;
+        }
+
+        async getEntities(fpid) {
+            const entities = [];
+            const contracts = await this.getAllDataByCondition({ columns: ['entity'], where: { fpid } });
+            for (const c of contracts) {
+                if (c.entity && !this._.includes(entities, c.entity)) {
+                    entities.push(c.entity);
+                }
+            }
+            return entities.join(',');
+        }
+    }
+    return FinancialPayContract;
+};

+ 37 - 0
app/service/financial_pay_tender.js

@@ -0,0 +1,37 @@
+'use strict';
+
+/**
+ * Created by EllisRan on 2020/3/3.
+ */
+
+const BaseService = require('../base/base_service');
+
+module.exports = app => {
+
+    class FinancialPayTender extends BaseService {
+
+        /**
+         * 构造函数
+         *
+         * @param {Object} ctx - egg全局变量
+         * @return {void}
+         */
+        constructor(ctx) {
+            super(ctx);
+            this.tableName = 'financial_pay_tender';
+            this.dataId = 'id';
+        }
+
+        async savePayTender(spid, data) {
+            if (data.id === 0) {
+                delete data.id;
+                data.spid = spid;
+                const result = await this.db.insert(this.tableName, data);
+                return await this.getDataById(result.insertId);
+            }
+            await this.db.update(this.tableName, data);
+            return await this.getDataById(data.id);
+        }
+    }
+    return FinancialPayTender;
+};

+ 256 - 0
app/service/financial_pay_tender_audit.js

@@ -0,0 +1,256 @@
+'use strict';
+
+/**
+ * Created by EllisRan on 2020/3/3.
+ */
+
+const BaseService = require('../base/base_service');
+const shenpiConst = require('../const/shenpi');
+const auditType = require('../const/audit').auditType;
+
+module.exports = app => {
+
+    class FinancialPayTenderAudit extends BaseService {
+
+        /**
+         * 构造函数
+         *
+         * @param {Object} ctx - egg全局变量
+         * @return {void}
+         */
+        constructor(ctx) {
+            super(ctx);
+            this.tableName = 'financial_pay_tender_audit';
+            this.dataId = 'id';
+        }
+
+        async getList(spid, tid, accountList = null) {
+            if (accountList === null) {
+                accountList = await this.ctx.service.projectAccount.getAllDataByCondition({
+                    where: { project_id: this.ctx.session.sessionProject.id, enable: 1 },
+                    columns: ['id', 'account', 'name', 'company', 'role', 'enable', 'is_admin', 'account_group', 'mobile'],
+                });
+            }
+            const list = await this.db.select(this.tableName, { where: { spid, tid }, orders: [['id', 'asc']] });
+            const removeList = [];
+            for (const l of list) {
+                const accountInfo = this._.find(accountList, { id: l.uid });
+                if (!accountInfo) {
+                    removeList.push(l.id);
+                    continue;
+                }
+                l.name = accountInfo.name;
+                l.company = accountInfo.company;
+            }
+            if (removeList.length > 0) {
+                for (const r of removeList) {
+                    list.splice(this._.findIndex(list, { id: r }), 1);
+                }
+            }
+            return list;
+        }
+
+        async saveAudits(spid, tid, accountList) {
+            const transaction = await this.db.beginTransaction();
+            try {
+                // 判断是否已存在该用户,存在则不插入
+                const pauditList = await this.getAllDataByCondition({ where: { spid, tid } });
+                const financialAudits = await this.ctx.service.financialAudit.getAllDataByCondition({ where: { spid } });
+                const pushData = [];
+                const pushFinancialData = [];
+                for (const a of this._.uniqBy(accountList, 'id')) {
+                    if (this._.findIndex(pauditList, { uid: a.id }) === -1) {
+                        const data = {
+                            spid,
+                            tid,
+                            uid: a.id,
+                            create_time: new Date(),
+                        };
+                        pushData.push(data);
+                    }
+                    if (this._.findIndex(financialAudits, { uid: a.id }) === -1) {
+                        pushFinancialData.push({
+                            spid,
+                            uid: a.id,
+                            create_time: new Date(),
+                        });
+                    }
+                }
+                if (pushData.length > 0) {
+                    await transaction.insert(this.tableName, pushData);
+                }
+                if (pushFinancialData.length > 0) {
+                    await transaction.insert(this.ctx.service.financialAudit.tableName, pushFinancialData);
+                }
+                transaction.commit();
+            } catch (error) {
+                console.log(error);
+                transaction.rollback();
+            }
+            return true;
+        }
+
+        async delAudits(spid, tid, datas) {
+            // return await this.db.delete(this.tableName, { id });
+            // 需要同时移除审批流程里的人
+            const reponseData = {};
+            const transaction = await this.db.beginTransaction();
+            try {
+                await transaction.delete(this.tableName, { id: this._.map(datas, 'id') });
+                const shenpiAudits = await this.ctx.service.shenpiAudit.getAuditList(tid, shenpiConst.sp_other_type.financial, shenpiConst.sp_status.gdspl);
+                const uids = this._.map(datas, 'uid');
+                const removeData = [];
+                for (const sp of shenpiAudits) {
+                    if (this._.includes(uids, sp.audit_id)) {
+                        removeData.push(sp.id);
+                    }
+                }
+                if (removeData.length > 0) {
+                    await transaction.delete(this.ctx.service.shenpiAudit.tableName, { id: removeData });
+                    // 重新分配order值
+                    const newSpAudits = await transaction.select(this.ctx.service.shenpiAudit.tableName, { where: { tid, sp_type: shenpiConst.sp_other_type.financial, sp_status: shenpiConst.sp_status.gdspl }, orders: [['audit_order', 'asc'], ['id', 'asc']] });
+                    // groupby audit_order
+                    const newSpAuditsGroup = this._.groupBy(newSpAudits, 'audit_order');
+                    const updateData = [];
+                    let order = 1;
+                    for (const a in newSpAuditsGroup) {
+                        const sameOrderAudits = newSpAuditsGroup[a];
+                        for (const sa of sameOrderAudits) {
+                            if (sa.audit_order !== order) {
+                                updateData.push({
+                                    id: sa.id,
+                                    audit_order: order,
+                                });
+                            }
+                        }
+                        order++;
+                    }
+                    if (updateData.length > 0) {
+                        await transaction.updateRows(this.ctx.service.shenpiAudit.tableName, updateData);
+                    }
+                }
+                await transaction.commit();
+                reponseData.permissionList = await this.getList(spid, tid);
+                reponseData.auditGroupList = await this.ctx.service.shenpiAudit.getAuditGroupList(tid, shenpiConst.sp_other_type.financial, shenpiConst.sp_status.gdspl);
+            } catch (error) {
+                console.log(error);
+                await transaction.rollback();
+            }
+            return reponseData;
+        }
+
+        async updatePermission(updateData) {
+            if (!updateData.id) {
+                return false;
+            }
+            return await this.db.update(this.tableName, updateData);
+        }
+
+        async addShenpiAudit(spid, data, tid) {
+            const reponseData = {};
+            const info = await this.ctx.service.shenpiAudit.addAudit(data, tid);
+            reponseData.shenpi = info;
+            if (info) {
+                const transaction = await this.db.beginTransaction();
+                try {
+                    // 判断是否存在并加入到成员表中
+                    const fptAudit = await this.getDataByCondition({ spid, tid, uid: data.audit_id });
+                    if (!fptAudit) {
+                        await transaction.insert(this.tableName, { spid, tid, uid: data.audit_id, create_time: new Date() });
+                    }
+                    // 判断是否存在并加入到成员表中
+                    const fAudit = await this.ctx.service.financialAudit.getDataByCondition({ spid, uid: data.audit_id });
+                    if (!fAudit) {
+                        await transaction.insert(this.ctx.service.financialAudit.tableName, { spid, uid: data.audit_id, create_time: new Date() });
+                    }
+                    await transaction.commit();
+                    reponseData.permissionList = await this.getList(spid, tid);
+                } catch (error) {
+                    console.log(error);
+                    await transaction.rollback();
+                }
+            }
+            return reponseData;
+        }
+
+        async copyShenpi2otherTender(spid, data, tid) {
+            const reponseData = {};
+            const info = await this.ctx.service.shenpiAudit.copyAudit2otherTender(data, tid);
+            if (info) {
+                const transaction = await this.db.beginTransaction();
+                try {
+                    // const permissionList = await this.getAllDataByCondition({ where: { spid, tid } });
+                    const uidList = this._.map(data.auditList, 'audit_id');
+                    const tidList = data.tidList.split(',');
+                    if (tidList.length === 0) {
+                        throw '没有选择要复制的标段';
+                    }
+                    if (uidList.length > 0) {
+                        const pushData = [];
+                        const times = new Date();
+                        for (const t of tidList) {
+                            const tenderAudits = await this.getAllDataByCondition({ where: { spid, tid: t } });
+                            const diffList = this._.difference(uidList, this._.map(tenderAudits, 'uid'));
+                            for (const d of diffList) {
+                                pushData.push({
+                                    spid, tid: t, uid: d,
+                                    create_time: times,
+                                });
+                            }
+                        }
+                        if (pushData.length > 0) await transaction.insert(this.tableName, pushData);
+                    }
+                    await transaction.commit();
+                    reponseData.otherPermissionList = await this.getList(spid, tidList);
+                } catch (error) {
+                    console.log(error);
+                    await transaction.rollback();
+                }
+            }
+            return reponseData;
+        }
+
+        async copyAudit2otherTender(spid, data, tid) {
+            const reponseData = {};
+            const transaction = await this.db.beginTransaction();
+            try {
+                const permissionList = await this.getAllDataByCondition({ where: { spid, tid } });
+                const uidList = this._.map(permissionList, 'uid');
+                const tidList = data.tidList.split(',');
+                if (tidList.length === 0) {
+                    throw '没有选择要复制的标段';
+                }
+                if (uidList.length > 0) {
+                    const pushData = [];
+                    const updateData = [];
+                    const times = new Date();
+                    for (const t of tidList) {
+                        const tenderAudits = await this.getAllDataByCondition({ where: { spid, tid: t } });
+                        const diffList = this._.difference(uidList, this._.map(tenderAudits, 'uid'));
+                        for (const d of diffList) {
+                            const p = permissionList.find(item => item.uid === d);
+                            pushData.push({ spid, tid: t, uid: p.uid, is_report: p.is_report, create_time: times });
+                        }
+                        const sameList = this._.intersection(this._.map(tenderAudits, 'uid'), uidList);
+                        for (const s of sameList) {
+                            const p = permissionList.find(item => item.uid === s);
+                            const t = tenderAudits.find(item => item.uid === s);
+                            if (p.is_report !== t.is_report) {
+                                updateData.push({ id: t.id, is_report: p.is_report });
+                            }
+                        }
+                    }
+                    if (pushData.length > 0) await transaction.insert(this.tableName, pushData);
+                    if (updateData.length > 0) await transaction.updateRows(this.tableName, updateData);
+                }
+                await transaction.commit();
+                reponseData.otherPermissionList = await this.getList(spid, tidList);
+            } catch (error) {
+                console.log(error);
+                await transaction.rollback();
+            }
+            return reponseData;
+        }
+    }
+    return FinancialPayTenderAudit;
+};

+ 108 - 0
app/service/financial_transfer.js

@@ -0,0 +1,108 @@
+'use strict';
+
+/**
+ * Created by EllisRan on 2020/3/3.
+ */
+
+const BaseService = require('../base/base_service');
+
+module.exports = app => {
+
+    class FinancialTransfer extends BaseService {
+
+        /**
+         * 构造函数
+         *
+         * @param {Object} ctx - egg全局变量
+         * @return {void}
+         */
+        constructor(ctx) {
+            super(ctx);
+            this.tableName = 'financial_transfer';
+        }
+
+        async getList(spid) {
+            const sql = 'SELECT ft.*, pa.`name` as username, pa.`company` FROM ?? as ft LEFT JOIN ?? as pa ON ft.`uid` = pa.`id` WHERE ft.`spid` = ? ORDER BY ft.`id` DESC';
+            const sqlParams = [this.tableName, this.ctx.service.projectAccount.tableName, spid];
+            const list = await this.db.query(sql, sqlParams);
+            for (const l of list) {
+                l.files = await this.ctx.service.financialTransferAtt.getAtt(l.id);
+            }
+            return list;
+        }
+
+        async addTransfer(spid, date, remark) {
+            const node = await this.getDataByCondition({ spid, t_time: date });
+            if (node) {
+                throw '资金划拨年月已存在,请勿重复';
+            }
+            const insertData = {
+                spid,
+                t_time: date,
+                create_time: new Date(),
+                remark,
+                uid: this.ctx.session.sessionUser.accountId,
+            };
+            const result = await this.db.insert(this.tableName, insertData);
+            if (result.affectedRows === 1) {
+                return result.insertId;
+            }
+            return false;
+        }
+
+        async delTransfer(trid) {
+            if (!trid) {
+                throw '参数有误';
+            }
+            const node = await this.getDataById(trid);
+            if (!node) {
+                throw '资金划拨不存在';
+            }
+            const transaction = await this.db.beginTransaction();
+            try {
+                await transaction.delete(this.tableName, { id: node.id });
+                // 删除合同附件
+                const attList = await this.ctx.service.financialTransferAtt.getAllDataByCondition({ where: { trid } });
+                const tenderAttList = await this.ctx.service.financialTransferTenderAtt.getAllDataByCondition({ where: { trid } });
+                await this.ctx.helper.delFiles(this._.concat(attList, tenderAttList));
+                await transaction.delete(this.ctx.service.financialTransferTender.tableName, { trid });
+                await transaction.commit();
+            } catch (err) {
+                await transaction.rollback();
+                throw err;
+            }
+            return true;
+        }
+
+        async lockTransfer(trid, lock) {
+            if (!trid) {
+                throw '参数有误';
+            }
+            const node = await this.getDataById(trid);
+            if (!node) {
+                throw '该资金划拨不存在';
+            }
+            if (node.uid !== this.ctx.session.sessionUser.accountId) {
+                throw '您没有权限操作';
+            }
+            const result = await this.db.update(this.tableName, { id: node.id, is_lock: lock });
+            return result.affectedRows === 1;
+        }
+
+        async saveTransfer(data) {
+            if (!data.id) {
+                throw '参数有误';
+            }
+            const node = await this.getDataById(data.id);
+            if (!node) {
+                throw '该资金划拨不存在';
+            }
+            if (node.uid !== this.ctx.session.sessionUser.accountId) {
+                throw '您没有权限操作';
+            }
+            const result = await this.db.update(this.tableName, data);
+            return result.affectedRows === 1;
+        }
+    }
+    return FinancialTransfer;
+};

+ 60 - 0
app/service/financial_transfer_att.js

@@ -0,0 +1,60 @@
+'use strict';
+
+/**
+ * Created by EllisRan on 2020/3/3.
+ */
+
+const BaseService = require('../base/base_service');
+
+module.exports = app => {
+
+    class FinancialTransferAtt extends BaseService {
+
+        /**
+         * 构造函数
+         *
+         * @param {Object} ctx - egg全局变量
+         * @return {void}
+         */
+        constructor(ctx) {
+            super(ctx);
+            this.tableName = 'financial_transfer_attachment';
+            this.dataId = 'id';
+        }
+
+        async getAtt(trid) {
+            const sql = 'SELECT a.*, b.name as username FROM ?? as a LEFT JOIN ?? as b ON a.`uid` = b.`id` WHERE a.`trid` = ? ORDER BY `upload_time` DESC';
+            const sqlParam = [this.tableName, this.ctx.service.projectAccount.tableName, trid];
+            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 = `/financial/${item.spid}/transfer/${item.trid}/file/${item.id}/download`;
+                } else {
+                    item.filepath = this.ctx.app.config.fujianOssPath + item.filepath;
+                    item.viewpath = item.filepath;
+                }
+                return item;
+            });
+        }
+
+        /**
+         * 存储上传的文件信息至数据库
+         * @param {Array} payload 载荷
+         * @return {Promise<void>} 数据库插入执行实例
+         */
+        async saveFileMsgToDb(payload) {
+            return await this.db.insert(this.tableName, payload);
+        }
+
+        /**
+         * 删除附件
+         * @param {Number} id - 附件id
+         * @return {void}
+         */
+        async delete(id) {
+            return await this.deleteById(id);
+        }
+    }
+    return FinancialTransferAtt;
+};

+ 119 - 0
app/service/financial_transfer_tender.js

@@ -0,0 +1,119 @@
+'use strict';
+
+/**
+ * Created by EllisRan on 2020/3/3.
+ */
+
+const BaseService = require('../base/base_service');
+
+module.exports = app => {
+
+    class FinancialTransferTender extends BaseService {
+
+        /**
+         * 构造函数
+         *
+         * @param {Object} ctx - egg全局变量
+         * @return {void}
+         */
+        constructor(ctx) {
+            super(ctx);
+            this.tableName = 'financial_transfer_tender';
+        }
+
+        async getList(trid) {
+            const sql = 'SELECT ftt.*, t.`name` as name FROM ?? as ftt LEFT JOIN ?? as t ON ftt.`tid` = t.`id` WHERE ftt.`trid` = ?';
+            const sqlParams = [this.tableName, this.ctx.service.tender.tableName, trid];
+            const list = await this.db.query(sql, sqlParams);
+            for (const l of list) {
+                l.files = await this.ctx.service.financialTransferTenderAtt.getAtt(l.id);
+            }
+            return list;
+        }
+
+        async addTenders(transferInfo, tenders) {
+            const transaction = await this.db.beginTransaction();
+            try {
+                const insertDatas = [];
+                for (const t of tenders) {
+                    const tender = await this.ctx.service.tender.getDataById(t.tid);
+                    const stages = await this.ctx.service.stage.getAllDataByCondition({ where: { tid: t.tid, order: t.sorder } });
+                    const insertData = {
+                        spid: transferInfo.spid,
+                        trid: transferInfo.id,
+                        tid: t.tid,
+                        sorder: t.sorder.join(','),
+                        uid: this.ctx.session.sessionUser.accountId,
+                        total_price: tender.total_price,
+                        contract_tp: this._.sumBy(stages, 'contract_tp'),
+                        qc_tp: this._.sumBy(stages, 'qc_tp'),
+                        pc_tp: this._.sumBy(stages, 'pc_tp'),
+                        yf_tp: this._.sumBy(stages, 'yf_tp'),
+                        sf_tp: this._.sumBy(stages, 'sf_tp'),
+                        hb_tp: this._.sumBy(stages, 'sf_tp'),
+                    };
+                    insertDatas.push(insertData);
+                }
+                await transaction.insert(this.tableName, insertDatas);
+                await this.calcHbTp(transferInfo.id, transaction);
+                await transaction.commit();
+                return true;
+            } catch (e) {
+                console.log(e);
+                await transaction.rollback();
+                return false;
+            }
+        }
+
+        async delTenders(ftid) {
+            if (!ftid) {
+                throw '参数有误';
+            }
+            const node = await this.getDataById(ftid);
+            if (!node) {
+                throw '资金划拨标段不存在';
+            }
+            const transaction = await this.db.beginTransaction();
+            try {
+                await transaction.delete(this.tableName, { id: node.id });
+                // 删除合同附件
+                const attList = await this.ctx.service.financialTransferTenderAtt.getAllDataByCondition({ where: { ftid } });
+                await this.ctx.helper.delFiles(attList);
+                await this.calcHbTp(node.trid, transaction);
+                await transaction.commit();
+            } catch (err) {
+                await transaction.rollback();
+                throw err;
+            }
+            return true;
+        }
+
+        async updateHbTp(ftid, hb_tp) {
+            if (!ftid) {
+                throw '参数有误';
+            }
+            const node = await this.getDataById(ftid);
+            if (!node) {
+                throw '资金划拨标段不存在';
+            }
+            const transaction = await this.db.beginTransaction();
+            try {
+                await transaction.update(this.tableName, { id: node.id, hb_tp });
+                await this.calcHbTp(node.trid, transaction);
+                await transaction.commit();
+            } catch (err) {
+                await transaction.rollback();
+                throw err;
+            }
+            return true;
+        }
+
+        async calcHbTp(trid, transaction = null) {
+            const sql = 'SELECT SUM(hb_tp) as hb_tp FROM ?? WHERE `trid` = ?';
+            const sqlParams = [this.tableName, trid];
+            const result = transaction ? await transaction.queryOne(sql, sqlParams) : await this.db.queryOne(sql, sqlParams);
+            transaction ? await transaction.update(this.ctx.service.financialTransfer.tableName, { id: trid, total_price: result.hb_tp }) : await this.db.update(this.ctx.service.financialTransfer.tableName, { id: trid, total_price: result.hb_tp });
+        }
+    }
+    return FinancialTransferTender;
+};

+ 60 - 0
app/service/financial_transfer_tender_att.js

@@ -0,0 +1,60 @@
+'use strict';
+
+/**
+ * Created by EllisRan on 2020/3/3.
+ */
+
+const BaseService = require('../base/base_service');
+
+module.exports = app => {
+
+    class FinancialTransferTenderAtt extends BaseService {
+
+        /**
+         * 构造函数
+         *
+         * @param {Object} ctx - egg全局变量
+         * @return {void}
+         */
+        constructor(ctx) {
+            super(ctx);
+            this.tableName = 'financial_transfer_tender_attachment';
+            this.dataId = 'id';
+        }
+
+        async getAtt(ftid) {
+            const sql = 'SELECT a.*, b.name as username FROM ?? as a LEFT JOIN ?? as b ON a.`uid` = b.`id` WHERE a.`ftid` = ? ORDER BY `upload_time` DESC';
+            const sqlParam = [this.tableName, this.ctx.service.projectAccount.tableName, ftid];
+            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 = `/financial/${item.spid}/transfer/${item.trid}/tender/${item.ftid}/file/${item.id}/download`;
+                } else {
+                    item.filepath = this.ctx.app.config.fujianOssPath + item.filepath;
+                    item.viewpath = item.filepath;
+                }
+                return item;
+            });
+        }
+
+        /**
+         * 存储上传的文件信息至数据库
+         * @param {Array} payload 载荷
+         * @return {Promise<void>} 数据库插入执行实例
+         */
+        async saveFileMsgToDb(payload) {
+            return await this.db.insert(this.tableName, payload);
+        }
+
+        /**
+         * 删除附件
+         * @param {Number} id - 附件id
+         * @return {void}
+         */
+        async delete(id) {
+            return await this.deleteById(id);
+        }
+    }
+    return FinancialTransferTenderAtt;
+};

+ 11 - 11
app/service/shenpi_audit.js

@@ -62,7 +62,7 @@ module.exports = app => {
         }
 
         async getAuditList(tid, type, status, group_id = 0) {
-            const sql = 'SELECT sp.id, sp.audit_id, sp.audit_type, pa.name FROM ?? AS sp LEFT JOIN ?? AS pa ON sp.audit_id = pa.id' +
+            const sql = 'SELECT sp.id, sp.audit_id, sp.audit_order, sp.audit_type, 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 = ? AND sp.sp_group = ? ORDER BY sp.audit_order ASC, sp.id ASC';
             const sqlParam = [this.tableName, this.ctx.service.projectAccount.tableName, tid, type, status, group_id];
             return await this.db.query(sql, sqlParam);
@@ -88,14 +88,14 @@ module.exports = app => {
             return result;
         }
 
-        async addAudit(data) {
+        async addAudit(data, tid = this.ctx.tender.id) {
             let result;
             const transaction = await this.db.beginTransaction();
             try {
                 if (parseInt(data.code) === shenpiConst.sp_type.stage && parseInt(data.status) === shenpiConst.sp_status.gdspl) {
                     const options = {
                         where: {
-                            tid: this.ctx.tender.id,
+                            tid,
                             user_id: data.audit_id,
                         },
                     };
@@ -104,9 +104,9 @@ module.exports = app => {
                     };
                     await transaction.update(this.ctx.service.ledgerCooperation.tableName, updateData, options);
                 }
-                const group = await this.ctx.service.shenpiGroup.getGroupBySelect(this.ctx.tender.id, data.code);
+                const group = await this.ctx.service.shenpiGroup.getGroupBySelect(tid, data.code);
                 const insertData = {
-                    tid: this.ctx.tender.id,
+                    tid,
                     sp_type: data.code,
                     sp_status: data.status,
                     audit_id: data.audit_id,
@@ -169,7 +169,7 @@ module.exports = app => {
             }
         }
 
-        async copyAudit2otherTender(data) {
+        async copyAudit2otherTender(data, tid = this.ctx.tender.id) {
             const transaction = await this.db.beginTransaction();
             try {
                 const shenpi_status = parseInt(data.status);
@@ -193,16 +193,16 @@ module.exports = app => {
                 }
                 if (tenderInfoUpdateList.length > 0) await transaction.updateRows(this.ctx.service.tenderInfo.tableName, tenderInfoUpdateList);
                 const insertList = [];
-                const needYB = ['ledger', 'revise', 'change'];
+                const needYB = ['ledger', 'revise', 'change', 'financial'];
                 const canYB = needYB.indexOf(data.code) !== -1;
                 let is_group = false;
                 let groupList = [];
                 if (shenpi_status === shenpiConst.sp_status.gdspl) {
-                    groupList = await this.ctx.service.shenpiGroup.getGroupList(this.ctx.tender.id, shenpiConst.sp_type[data.code]);
+                    groupList = await this.ctx.service.shenpiGroup.getGroupList(tid, shenpiConst.sp_type[data.code]);
                     if (groupList.length > 0) {
                         is_group = true;
                         for (const g of groupList) {
-                            g.auditList = await this.getAllDataByCondition({ where: { tid: this.ctx.tender.id, sp_type: shenpiConst.sp_type[data.code], sp_status: shenpi_status, sp_group: g.id } });
+                            g.auditList = await this.getAllDataByCondition({ where: { tid, sp_type: shenpiConst.sp_type[data.code], sp_status: shenpi_status, sp_group: g.id } });
                         }
                     }
                 }
@@ -213,7 +213,7 @@ module.exports = app => {
                                 tid: t.id, sp_type: shenpiConst.sp_type[data.code],
                             });
                         }
-                        await transaction.delete(this.tableName, { tid: t.id, sp_type: shenpiConst.sp_type[data.code], sp_status: shenpi_status });
+                        await transaction.delete(this.tableName, { tid: t.id, sp_type: shenpiConst.sp_type[data.code] || shenpiConst.sp_other_type[data.code], sp_status: shenpi_status });
                         if (is_group) {
                             for (const g of groupList) {
                                 const result = await transaction.insert(this.ctx.service.shenpiGroup.tableName, {
@@ -247,7 +247,7 @@ module.exports = app => {
                                 }
                                 insertList.push({
                                     tid: t.id,
-                                    sp_type: shenpiConst.sp_type[data.code],
+                                    sp_type: shenpiConst.sp_type[data.code] || shenpiConst.sp_other_type[data.code],
                                     sp_status: shenpi_status,
                                     audit_id: s.audit_id,
                                     audit_type: s.audit_type,

+ 4 - 2
app/service/stage_bills.js

@@ -563,8 +563,10 @@ module.exports = app => {
                 const his = await this.ctx.service.sumLoadHistory.saveStageHistory(this.ctx.tender.id, this.ctx.stage.id, lid, tenders, result.errors, cover);
                 if (updateStageBills.length > 0) await conn.updateRows(this.tableName, updateStageBills);
                 if (insertStageBills.length > 0) await conn.insert(this.tableName, insertStageBills);
-                await conn.delete(this.ctx.service.stageImportChange.tableName, { import_lid: lid, sid: this.ctx.stage.id });
-                if (result.qc_detail.length > 0) await conn.insert(this.ctx.service.stageImportChange.tableName, result.qc_detail);
+                if (loadChange) {
+                    await conn.delete(this.ctx.service.stageImportChange.tableName, { import_lid: lid, sid: this.ctx.stage.id });
+                    if (result.qc_detail.length > 0) await conn.insert(this.ctx.service.stageImportChange.tableName, result.qc_detail);
+                }
                 await conn.commit();
                 return { curStageData: result.update, sumLoadHis: his };
             } catch (err) {

+ 86 - 3
app/service/stage_stash.js

@@ -8,6 +8,8 @@
  * @version
  */
 
+const Ledger = require('../lib/ledger');
+
 class loadStageExcelTree {
     constructor(ctx) {
         this.ctx = ctx;
@@ -48,7 +50,7 @@ class loadStageExcelTree {
             if (node.b_code) {
                 if (x.has_pos === undefined) {
                     const relaPos = self.pos.getLedgerPos(x.id);
-                    x.has_pos = !!relaPos && relaPos.length > 0;
+                    x.has_pos = !!(relaPos && relaPos.length > 0);
                 }
                 return node.b_code === _.trimEnd(x.b_code) && node.name === _.trimEnd(x.name) && node.unit === _.trimEnd(x.unit)
                     && node.unit_price === x.unit_price && node.has_pos === x.has_pos;
@@ -122,7 +124,7 @@ class loadStageExcelTree {
     }
     loadNode(node, parent) {
         node.is_leaf = !node.children || node.children.length === 0 ? 1 : 0;
-        node.has_pos = node.pos && node.pos.length > 0;
+        node.has_pos = !!(node.pos && node.pos.length > 0);
         const cur = this.findNode(node, parent);
         if (!cur || cur.settle_status === this.settleStatus.finish) return;
 
@@ -144,8 +146,31 @@ class loadStageExcelTree {
             this.loadNode(node, null);
         }
     }
-}
 
+    loadDealNode(node, parent, pos) {
+        node.pos = pos.getLedgerPos(node.id) || [];
+        node.has_pos = node.pos.length > 0;
+        const cur = this.findNode(node, parent);
+        if (!cur || cur.settle_status === this.settleStatus.finish) return;
+
+        if (cur) {
+            if (!node.b_code) this.loadDgn(node, cur);
+            if (node.is_leaf) {
+                this.loadLeaf(node, cur);
+            } else {
+                for (const c of node.children) {
+                    this.loadDealNode(c, cur, pos);
+                }
+            }
+        }
+    }
+    loadDeal(dealTree, dealPos, source) {
+        this.init(source);
+        for (const node of dealTree.children) {
+            this.loadDealNode(node, null, dealPos);
+        }
+    }
+}
 
 module.exports = app => {
     class StageStash extends app.BaseService {
@@ -423,6 +448,64 @@ module.exports = app => {
                 throw '解析Excel错误';
             }
         }
+
+        async loadStageDealData(stage, dealData) {
+            try {
+                const dealTree = new Ledger.billsTree(this.ctx, {
+                    id: 'ledger_id',
+                    pid: 'ledger_pid',
+                    order: 'order',
+                    level: 'level',
+                    rootId: -1,
+                });
+                const dealPos = new Ledger.pos({ id: 'id', ledgerId: 'lid' });
+                dealTree.loadDatas(dealData.ledger);
+                dealPos.loadDatas(dealData.pos);
+
+                const ledgerData = await this.ctx.service.ledger.getAllDataByCondition({
+                    columns: ['id', 'ledger_id', 'ledger_pid', 'level', 'order', 'full_path', 'is_leaf', 'code', 'b_code', 'name', 'unit', 'unit_price'],
+                    where: { tender_id: stage.tid},
+                });
+                const extraData = await this.ctx.service.ledgerExtra.getData(this.ctx.tender.id, ['is_tp']);
+                const settleStatusBills = stage.readySettle ? await this.ctx.service.settleBills.getAllDataByCondition({ where: { settle_id: stage.readySettle.id }}) : [];
+                this.ctx.helper.assignRelaData(ledgerData, [
+                    { data: extraData, fields: ['is_tp'], prefix: '', relaId: 'id' },
+                    { data: settleStatusBills, fields: ['settle_status'], prefix: '', relaId: 'lid' },
+                ]);
+                const posData = await this.ctx.service.pos.getAllDataByCondition({
+                    columns: ['id', 'lid', 'name', 'porder'],
+                    where: { tid: stage.tid },
+                });
+                const settleStatusPos = stage.readySettle ? await this.ctx.service.settlePos.getAllDataByCondition({ where: { settle_id: stage.readySettle.id }}) : [];
+                this.ctx.helper.assignRelaData(posData, [
+                    { data: settleStatusPos, fields: ['settle_status'], prefix: '', relaId: 'pid' },
+                ]);
+                const stageBills = await this.ctx.service.stageBills.getAllDataByCondition({ where: { sid: stage.id } });
+                const stagePos = await this.ctx.service.stagePos.getAllDataByCondition({ where: { sid: stage.id } });
+                const stageBillsDgn = await this.ctx.service.stageBillsDgn.getAllDataByCondition({ where: { tid: stage.tid } });
+
+                const loadModal = new loadStageExcelTree(this.ctx);
+                loadModal.loadDeal(dealTree, dealPos, { ledgerData, posData, stageBills, stagePos, stageBillsDgn, default: { tid: stage.tid, sid: stage.id, said: this.ctx.session.sessionUser.accountId } });
+
+                const conn = await this.db.beginTransaction();
+                try {
+                    if (loadModal.insertBills.length > 0) conn.insert(this.ctx.service.stageBills.tableName, loadModal.insertBills);
+                    if (loadModal.updateBills.length > 0) conn.updateRows(this.ctx.service.stageBills.tableName, loadModal.updateBills);
+                    if (loadModal.insertPos.length > 0) conn.insert(this.ctx.service.stagePos.tableName, loadModal.insertPos);
+                    if (loadModal.updatePos.length > 0) conn.updateRows(this.ctx.service.stagePos.tableName, loadModal.updatePos);
+                    if (loadModal.insertDgn.length > 0) conn.insert(this.ctx.service.stageBillsDgn.tableName, loadModal.insertDgn);
+                    if (loadModal.updateDgn.length > 0) conn.updateRows(this.ctx.service.stageBillsDgn.tableName, loadModal.updateDgn);
+                    await conn.commit();
+                } catch (err) {
+                    await conn.rollback();
+                    this.ctx.log(err);
+                    throw '保存导入数据失败';
+                }
+            } catch (err) {
+                this.ctx.log(err);
+                throw '导入本期合同计量';
+            }
+        }
     }
 
     return StageStash;

+ 15 - 0
app/service/sub_project.js

@@ -490,6 +490,21 @@ module.exports = app => {
             });
             return this._filterEmptyFolder(result);
         }
+
+        // 合同管理获取项目列表
+        async getSubProjectByFinancial(pid, uid, admin, filterFolder = false) {
+            let result = await this.getAllDataByCondition({ where: { project_id: pid, is_delete: 0 } });
+            if (admin) return this._filterEmptyFolder(result);
+
+            const permission = await this.ctx.service.financialAudit.getAllDataByCondition({ where: { uid } });
+            result = result.filter(x => {
+                if (x.is_folder) return !filterFolder;
+                const pb = permission.find(y => { return x.id === y.spid; });
+                if (!pb) return false;
+                return true;
+            });
+            return this._filterEmptyFolder(result);
+        }
     }
 
     return SubProject;

+ 72 - 41
app/service/tender_cache.js

@@ -28,6 +28,20 @@ module.exports = app => {
             this.tableName = 'tender_cache';
         }
 
+        _tranLedgerCache(tender, cache) {
+            tender.cur_flow = (cache.ledger_status === auditConst.ledger.status.checkNo)
+                ? JSON.parse(cache.ledger_flow_pre_info) : JSON.parse(cache.ledger_flow_cur_info || cache.ledger_flow_pre_info);
+            tender.cur_flow.title = '台账';
+            tender.pre_flow = cache.ledger_flow_pre_info ? JSON.parse(cache.ledger_flow_pre_info) : null;
+            tender.stage_tp = {};
+            tender.stage_count = 0;
+            tender.stage_status = cache.stage_status;
+            tender.progress = {
+                title: '台账',
+                status: auditConst.ledger.tiStatusString[cache.ledger_status],
+                status_class: auditConst.ledger.tiStatusStringClass[cache.ledger_status],
+            };
+        }
         _analysisTenderCache(tender, cache, uid) {
             commonField.forEach(f => { tender[f] = cache[f]; });
             tender.ledger_tp = cache.ledger_tp ? JSON.parse(cache.ledger_tp) : {};
@@ -38,71 +52,87 @@ module.exports = app => {
             cache.stage_flow_cur_uid = cache.stage_flow_cur_uid ? cache.stage_flow_cur_uid.split(',') : [];
             cache.stage_flow_pre_uid = cache.stage_flow_pre_uid ? cache.stage_flow_pre_uid.split(',') : [];
             if (!cache.stage_count) {
-                tender.cur_flow = (cache.ledger_status === auditConst.ledger.status.checkNo)
-                    ? JSON.parse(cache.ledger_flow_pre_info) : JSON.parse(cache.ledger_flow_cur_info || cache.ledger_flow_pre_info);
-                tender.cur_flow.title = '台账';
-                tender.pre_flow = cache.ledger_flow_pre_info ? JSON.parse(cache.ledger_flow_pre_info) : null;
-                tender.stage_tp = {};
-                tender.stage_count = 0;
-                tender.stage_status = cache.stage_status;
-                tender.progress = {
-                    title: '台账',
-                    status: auditConst.ledger.tiStatusString[cache.ledger_status],
-                    status_class: auditConst.ledger.tiStatusStringClass[cache.ledger_status],
-                };
+                this._tranLedgerCache(tender, cache);
             } else if (cache.stage_count === 1 && cache.stage_status === auditConst.stage.status.uncheck && cache.stage_flow_cur_uid.indexOf(uid) < 0) {
-                tender.cur_flow = JSON.parse(cache.ledger_flow_cur_info || cache.ledger_flow_pre_info);
-                tender.cur_flow.title = '台账';
-                tender.pre_flow = cache.ledger_flow_pre_info ? JSON.parse(cache.ledger_flow_pre_info) : null;
-                tender.stage_tp = {};
-                tender.stage_count = 0;
-                tender.stage_status = cache.stage_status;
-                tender.progress = {
-                    title: '台账',
-                    status: auditConst.ledger.tiStatusString[cache.ledger_status],
-                    status_class: auditConst.ledger.tiStatusStringClass[cache.ledger_status],
-                };
-            } else if (cache.stage_status !== auditConst.stage.status.uncheck || cache.stage_flow_cur_uid.indexOf(uid) >= 0) {
-                tender.stage_status = cache.stage_status;
+                this._tranLedgerCache(tender, cache);
+            } else if (cache.stage_status === auditConst.stage.status.uncheck) {
+                tender.stage_status = cache.stage_flow_cur_uid.indexOf(uid) >= 0 ? auditConst.stage.status.uncheck : auditConst.stage.status.checked;
                 tender.stage_count = tender.stage_count;
                 tender.stage_complete_count = tender.stage_complete_count;
-                tender.cur_flow = cache.stage_status === auditConst.stage.status.checkNo
-                    ? JSON.parse(cache.stage_flow_pre_info)
-                    : JSON.parse(cache.stage_flow_cur_info || cache.stage_flow_pre_info);
+                tender.cur_flow = cache.stage_flow_cur_uid.indexOf(uid) >= 0 ?  JSON.parse(cache.stage_flow_cur_info || cache.stage_flow_pre_info) : JSON.parse(cache.stage_flow_pre_info);
                 tender.pre_flow = cache.stage_flow_pre_info ? JSON.parse(cache.stage_flow_pre_info) : null;
-                tender.stage_tp = JSON.parse(cache.stage_flow_cur_tp || cache.stage_flow_pre_tp);
+                tender.stage_tp =  cache.stage_flow_cur_uid.indexOf(uid) >= 0 ? JSON.parse(cache.stage_flow_cur_tp || cache.stage_flow_pre_tp) : JSON.parse(cache.stage_flow_pre_tp);
                 try {
                     tender.progress = {
                         title: `第${tender.cur_flow.order === undefined ? tender.cur_flow[0].order : tender.cur_flow.order}期`,
-                        status: auditConst.stage.tiStatusString[cache.stage_status],
-                        status_class: auditConst.stage.tiStatusStringClass[cache.stage_status],
+                        status: auditConst.stage.tiStatusString[tender.stage_status],
+                        status_class: auditConst.stage.tiStatusStringClass[tender.stage_status],
                     };
                 } catch (err) {
                     tender.progress = {
                         title: `第${tender.stage_count}期`,
-                        status: auditConst.stage.tiStatusString[cache.stage_status],
-                        status_class: auditConst.stage.tiStatusStringClass[cache.stage_status],
+                        status: auditConst.stage.tiStatusString[tender.stage_status],
+                        status_class: auditConst.stage.tiStatusStringClass[tender.stage_status],
                     };
                 }
-            } else {
+            } else if (cache.stage_status === auditConst.stage.status.checkNo) {
+                tender.stage_status = auditConst.stage.status.checkNo;
+                tender.stage_count = tender.stage_count;
+                tender.stage_complete_count = tender.stage_complete_count;
+                tender.cur_flow = JSON.parse(cache.stage_flow_cur_info || cache.stage_flow_pre_info);
+                tender.pre_flow = cache.stage_flow_pre_info ? JSON.parse(cache.stage_flow_pre_info) : null;
+                tender.stage_tp =  cache.stage_flow_cur_uid.indexOf(uid) >= 0 ? JSON.parse(cache.stage_flow_cur_tp || cache.stage_flow_pre_tp) : JSON.parse(cache.stage_flow_pre_tp);
+                try {
+                    tender.progress = {
+                        title: `第${tender.cur_flow.order === undefined ? tender.cur_flow[0].order : tender.cur_flow.order}期`,
+                        status: auditConst.stage.statusButton[tender.stage_status],
+                        status_class: auditConst.stage.tiStatusStringClass[auditConst.stage.status.uncheck],
+                    };
+                } catch (err) {
+                    tender.progress = {
+                        title: `第${tender.stage_count}期`,
+                        status: auditConst.stage.statusButton[tender.stage_status],
+                        status_class: auditConst.stage.tiStatusStringClass[auditConst.stage.status.uncheck],
+                    };
+                }
+            } else if (cache.stage_status === auditConst.stage.status.checked) {
                 tender.stage_status = auditConst.stage.status.checked;
                 tender.stage_count = tender.stage_complete_count;
                 tender.stage_complete_count = tender.stage_complete_count;
-                tender.cur_flow = (cache.stage_status !== auditConst.stage.status.uncheck || cache.stage_flow_cur_uid.indexOf(uid) < 0)
-                    ? JSON.parse(cache.stage_flow_pre_info) : JSON.parse(cache.stage_flow_cur_info || cache.stage_flow_pre_info);
+                tender.cur_flow = JSON.parse(cache.stage_flow_pre_info || cache.stage_flow_cur_info);
+                tender.pre_flow = cache.stage_flow_pre_info ? JSON.parse(cache.stage_flow_pre_info) : null;
+                tender.stage_tp = JSON.parse(cache.stage_flow_pre_tp);
+                try {
+                    tender.progress = {
+                        title: `第${tender.cur_flow.order === undefined ? tender.cur_flow[0].order : tender.cur_flow.order}期`,
+                        status: auditConst.stage.tiStatusString[tender.stage_status],
+                        status_class: auditConst.stage.tiStatusStringClass[tender.stage_status],
+                    };
+                } catch (err) {
+                    tender.progress = {
+                        title: `第${tender.stage_count}期`,
+                        status: auditConst.stage.tiStatusString[tender.stage_status],
+                        status_class: auditConst.stage.tiStatusStringClass[tender.stage_status],
+                    };
+                }
+            } else {
+                tender.stage_status = auditConst.stage.status.checking;
+                tender.stage_count = tender.stage_count;
+                tender.stage_complete_count = tender.stage_complete_count;
+                tender.cur_flow = JSON.parse(cache.stage_flow_cur_info || cache.stage_flow_pre_info);
                 tender.pre_flow = cache.stage_flow_pre_info ? JSON.parse(cache.stage_flow_pre_info) : null;
-                tender.stage_tp = cache.stage_flow_pre_tp ? JSON.parse(cache.stage_flow_pre_tp) : {};
+                tender.stage_tp = JSON.parse(cache.stage_flow_cur_tp || cache.stage_flow_pre_tp);
                 try {
                     tender.progress = {
                         title: `第${tender.pre_flow.order === undefined ? tender.pre_flow[0].order : tender.pre_flow.order}期`,
-                        status: auditConst.stage.tiStatusString[auditConst.stage.status.checked],
-                        status_class: auditConst.stage.tiStatusStringClass[auditConst.stage.status.checked],
+                        status: auditConst.stage.tiStatusString[tender.stage_status],
+                        status_class: auditConst.stage.tiStatusStringClass[tender.stage_status],
                     };
                 } catch(err) {
                     tender.progress = {
                         title: `第${tender.stage_count}期`,
-                        status: auditConst.stage.tiStatusString[auditConst.stage.status.checked],
-                        status_class: auditConst.stage.tiStatusStringClass[auditConst.stage.status.checked],
+                        status: auditConst.stage.tiStatusString[tender.stage_status],
+                        status_class: auditConst.stage.tiStatusStringClass[tender.stage_status],
                     };
                 }
             }
@@ -117,6 +147,7 @@ module.exports = app => {
                 try {
                     this._analysisTenderCache(tender, cache, uid);
                 } catch(err) {
+                    console.log(err);
                     console.log(tender);
                 }
             }

+ 34 - 2
app/view/dashboard/index.ejs

@@ -69,12 +69,15 @@
                                     <% if (ctx.session.sessionProject.page_show.openPayment && auditPayments.length !== 0) { %>
                                         <option value="10">支付审批(<%- auditPayments.length %>)</option>
                                     <% } %>
+                                    <% if (ctx.session.sessionProject.page_show.openFinancial && auditFinancials.length !== 0) { %>
+                                        <option value="11">资金支付(<%- auditFinancials.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 || auditPayments.length !== 0 || auditStageAss.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 || auditStageAss.length !== 0 || auditFinancials.length !== 0) { %>
                                 <style>
                                     #doing-list td {
                                         word-wrap:break-word;
@@ -283,6 +286,19 @@
                                                 </tr>
                                             <% } %>
                                         <% } %>
+                                        <% for (const af of auditFinancials) { %>
+                                            <tr data-type="11">
+                                                <td><span class="bg-new-financial text-new-financial badge text-width">资金支付</span></td>
+                                                <td><a href="/financial/<%- af.spid %>/pay"><%- af.name %></a> <a href="/financial/<%- af.spid %>/pay/<%- af.fpid %>/detial"><%- af.fpcode %></a></td>
+                                                <td>支付</td>
+                                                <td><%- (
+                                                            af.fpcstatus !== acFinancial.status.checkNo
+                                                                    ? (af.begin_time ? ctx.moment(af.begin_time).format('YYYY/MM/DD HH:mm') : '')
+                                                                    : (af.end_time ? ctx.moment(af.end_time).format('YYYY/MM/DD HH:mm') : '')
+                                                    ) %></td>
+                                                <td><a href="/financial/<%- af.spid %>/pay/<%- af.fpid %>/detail" class="btn btn-sm btn-table <% if (af.fpstatus === acFinancial.status.checkNo) { %>btn-outline-warning text-warning<% } else { %>btn-outline-primary<% } %>" role="button"><% if (af.fpstatus !== acFinancial.status.checkNo) { %>审批<% } else { %>重新上报<% } %></a></td>
+                                            </tr>
+                                        <% } %>
                                     </tbody>
                                 </table>
                                 <% } else { %>
@@ -378,7 +394,10 @@
                                         <option value="9">变更方案</option>
                                     <% } %>
                                     <% if (ctx.session.sessionProject.page_show.openMaterial) { %>
-                                    <option value="1">材料调差</option>
+                                        <option value="1">材料调差</option>
+                                    <% } %>
+                                    <% if (ctx.session.sessionProject.page_show.openFinancial) { %>
+                                        <option value="11">资金支付</option>
                                     <% } %>
                                 </select>
                             </div>
@@ -496,6 +515,16 @@
                                                     <td class="<%- acAdvance.statusClass[notice.status]%>"><%- acAdvance.statusString[notice.status]%></td>
                                                     <td><%- notice.opinion ? notice.opinion : '' %></td>
                                                 </tr>
+                                            <% } else if(notice.type === pushType.financial && ctx.session.sessionProject.page_show.openFinancial) { %>
+                                                <tr data-type="11">
+                                                    <td><span class="bg-new-financial text-new-financial badge text-width">资金支付</span></td>
+                                                    <td><a href="/financial/<%- notice.spid %>"><%- notice.name %></a> <a href="/financial/<%- notice.spid %>/pay/<%- notice.fpid %>/detail"><%- notice.code %></a></td>
+                                                    <td><%- notice.su_name %><%- (notice.su_role ? '-' + notice.su_role : '') %></td>
+                                                    <td><%- ctx.helper.dateTran(notice.create_time, 'YYYY/MM/DD HH:mm') %></td>
+                                                    <td>支付</td>
+                                                    <td class="<%- acFinancial.statusClass[notice.status] %>"><%- acFinancial.statusString[notice.status] %></td>
+                                                    <td><%- notice.opinion ? notice.opinion : '' %></td>
+                                                </tr>
                                             <% } %>
                                         <% } %>
                                     </tbody>
@@ -630,6 +659,9 @@
             <% if (ctx.session.sessionProject.page_show.openMaterial) { %>
             'rgba(187, 41, 210,'+ transparentCount +')',
             <% } %>
+            <% if (ctx.session.sessionProject.page_show.openFinancial) { %>
+            'rgba(58, 88, 50,'+ transparentCount +')',
+            <% } %>
         ],
         tooltip: {
             trigger: 'item'

+ 35 - 0
app/view/financial/index.ejs

@@ -0,0 +1,35 @@
+<div class="panel-content">
+    <div class="panel-title fluid">
+        <div class="title-main  d-flex">
+            <div class="d-inline-block" id="show-level"></div>
+        </div>
+    </div>
+    <div class="content-wrap">
+        <div class="sjs-height-0" style="background-color: #fff">
+            <div class="c-body">
+                <% if (!projectList || projectList.length === 0) { %>
+                <div class="jumbotron" id="no-project">
+                    <h3 class="display-6">还没有项目数据</h3>
+                </div>
+                <% } else { %>
+                <table class="table table-bordered">
+                    <tr class="text-center">
+                        <th style="min-width: 200px" rowspan="2">项目名称</th>
+                        <th width="10%" rowspan="2">创建时间</th>
+                        <% if (ctx.session.sessionUser.is_admin) { %>
+                        <th width="10%" rowspan="2">操作</th>
+                        <% } %>
+                    </tr>
+                    <tbody id="projectList">
+                    </tbody>
+                </table>
+                <% } %>
+            </div>
+        </div>
+    </div>
+</div>
+<script>
+    const projectList = JSON.parse(unescape('<%- escape(JSON.stringify(projectList)) %>'));
+    const category = JSON.parse(unescape('<%- escape(JSON.stringify(categoryData)) %>'));
+    const is_admin = <%- ctx.session.sessionUser.is_admin %>;
+</script>

+ 266 - 0
app/view/financial/modal.ejs

@@ -0,0 +1,266 @@
+<% if (ctx.session.sessionUser.is_admin) { %>
+<link href="/public/css/bootstrap/bootstrap-table.min.css" rel="stylesheet">
+<link href="/public/css/bootstrap/bootstrap-table-fixed-columns.min.css" rel="stylesheet">
+<style>
+    /*.bootstrap-table .fixed-table-container.fixed-height:not(.has-footer) {*/
+    /*border-bottom: 0;*/
+    /*}*/
+    @-moz-document url-prefix() {
+        table {
+            table-layout: fixed;
+        }
+    }
+    .customize-header tr th .th-inner{
+        padding: 0.3rem!important;
+    }
+</style>
+<!-- 成员管理 -->
+<div class="modal fade" id="authority-list" data-backdrop="static">
+    <div class="modal-dialog modal-lg" role="document">
+        <div class="modal-content">
+            <div class="modal-header">
+                <h5 class="modal-title">成员管理</h5>
+            </div>
+            <div class="modal-body pt-0">
+                <div class="d-flex flex-row bg-graye">
+                    <input type="hidden" id="spid" />
+                    <div class="p-2 dropdown">
+                        <button class="btn btn-outline-primary btn-sm dropdown-toggle" type="button" id="dropdownMenuButton"
+                                data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
+                            添加用户
+                        </button>
+                        <div class="dropdown-menu dropdown-menu-left" aria-labelledby="dropdownMenuButton" style="width:220px">
+                            <div class="mb-2 p-2"><input class="form-control form-control-sm" placeholder="姓名/手机 检索"
+                                                         id="gr-search" autocomplete="off"></div>
+                            <dl class="list-unstyled book-list">
+                                <% 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 => { %>
+                                            <dd class="border-bottom p-2 mb-0 " data-id="<%- item.id %>">
+                                                <p class="mb-0 d-flex"><span class="text-primary"><%- item.name %></span><span
+                                                            class="ml-auto"><%- item.mobile %></span></p>
+                                                <span class="text-muted"><%- item.role %></span>
+                                            </dd>
+                                        <% });%>
+                                    </div>
+                                <% }) %>
+                            </dl>
+                        </div>
+                    </div>
+<!--                    <div class="p-2"><a href="">同步计量账号</a></div>-->
+                    <div class="ml-auto p-2">
+<!--                        <div class="btn-group">-->
+<!--                            <a href="javascript:void(0)" data-toggle="dropdown" title="权限说明"><i class="fa fa-question-circle"></i></a>-->
+<!--                            <div class="dropdown-menu bg-dark">-->
+<!--                                <div class="dropdown-item text-light bg-dark">1、编辑节点:编辑合同管理内页树结构</div>-->
+<!--                                <div class="dropdown-item text-light bg-dark">2、添加合同:允许添加合同</div>-->
+<!--                                <div class="dropdown-item text-light bg-dark">3、授权范围本单位:授权节点下查看本单位人员添加的所有合同</div>-->
+<!--                                <div class="dropdown-item text-light bg-dark">4、授权范围本节点:授权节点下查看所有人上传的合同</div>-->
+<!--                            </div>-->
+<!--                        </div>-->
+                    </div>
+                </div>
+                <table id="contract-audit-table" class="table table-bordered text-center" data-height="500" data-toggle="table">
+                    <thead>
+                    <tr>
+                        <th rowspan="2" class="align-middle">用户名</th>
+                        <th rowspan="2" class="align-middle">角色/职位</th>
+                        <th colspan="3">资金划拨</th>
+                        <th colspan="2">资金支付</th>
+                        <th rowspan="2" class="align-middle">操作</th>
+                    </tr>
+                    <tr>
+                        <th>查看划拨</th>
+                        <th>新建划拨</th>
+                        <th>上传附件</th>
+                        <th>查看支付</th>
+                        <th>上传附件</th>
+                    </tr>
+                    </thead>
+                    <tbody id="contract-audit-list">
+                    </tbody>
+                </table>
+            </div>
+            <div class="modal-footer">
+                <button type="button" class="btn btn-sm btn-outline-secondary" data-dismiss="modal">关闭</button>
+            </div>
+        </div>
+    </div>
+</div>
+<!-- 弹窗删除权限用户 -->
+<div class="modal fade" id="del-contract-audit" 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">
+                <h6>确认删除当前所选用户?</h6>
+            </div>
+            <div class="modal-footer">
+                <button type="button" class="btn btn-sm btn-secondary" data-dismiss="modal">取消</button>
+                <input type="hidden" id="del-audit-ids" />
+                <button type="button" class="btn btn-sm btn-danger" id="del-audit-btn">确定删除</button>
+            </div>
+        </div>
+    </div>
+</div>
+<script src="/public/js/bootstrap/bootstrap-table.min.js"></script>
+<script src="/public/js/bootstrap/locales/bootstrap-table-zh-CN.min.js"></script>
+    <script>
+        const accountGroup = JSON.parse(unescape('<%- escape(JSON.stringify(accountGroup)) %>'));
+        const accountList = JSON.parse(unescape('<%- escape(JSON.stringify(accountList)) %>'));
+    </script>
+<script>
+    $(function () {
+        let timer = null
+        let oldSearchVal = null
+        $('#gr-search').bind('input propertychange', function (e) {
+            oldSearchVal = e.target.value
+            timer && clearTimeout(timer)
+            timer = setTimeout(() => {
+                const newVal = $('#gr-search').val()
+                let html = ''
+                if (newVal && newVal === oldSearchVal) {
+                    accountList.filter(item => item && (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>`
+                    })
+                    $('#authority-list .book-list').empty()
+                    $('#authority-list .book-list').append(html)
+                } else {
+                    if (!$('#authority-list .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 => {
+                                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>'
+                        })
+                        $('#authority-list .book-list').empty()
+                        $('#authority-list .book-list').append(html)
+                    }
+                }
+            }, 400);
+        });
+        // 添加到成员中
+        $('.book-list').on('click', '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
+        });
+        // 添加到成员中
+        $('body').on('click', '#authority-list dl dd', function () {
+            const id = parseInt($(this).data('id'));
+            console.log(id);
+            if (!isNaN(id) && id !== 0) {
+                postData('/financial/'+ $('#spid').val() + '/audit/save', {type: 'add-audit', id: id}, function (result) {
+                    setList(result);
+                })
+            }
+        });
+        let first = 1;
+        $('#authority-list').on('shown.bs.modal', function () {
+            if (first) {
+                const option = {
+                    locale: 'zh-CN',
+                    height: 500,
+                }
+                $("#contract-audit-table").bootstrapTable('destroy').bootstrapTable(option);
+                first = 0;
+            }
+            const spid = $('#spid').val();
+            if (spid) {
+                postData('/financial/'+ $('#spid').val() + '/audit/save', { type: 'list' }, function (result) {
+                    setList(result);
+                });
+            }
+        });
+        $('body').on('click', '.get-audits', function () {
+            const spid = $(this).data('spid');
+            $('#spid').val(spid);
+            $('#contract-audit-list').html('');
+            $('#authority-list').modal('show');
+        });
+
+        function setList(datas) {
+            let list = '';
+            for (const ca of datas) {
+                list += `<tr>
+                            <td>${ca.name}</td>
+                            <td>${ca.role}</td>
+                            <td>
+                                <input type="checkbox" class="permission-checkbox" data-type="permission_transfer_show" value="${ca.id}" ${ca.permission_transfer_show ? 'checked' : ''}>
+                            </td>
+                            <td>
+                                <input type="checkbox" class="permission-checkbox" data-type="permission_transfer_add" value="${ca.id}" ${ca.permission_transfer_add ? 'checked' : ''}>
+                            </td>
+                            <td>
+                                <input type="checkbox" class="permission-checkbox" data-type="permission_transfer_file" value="${ca.id}" ${ca.permission_transfer_file ? 'checked' : ''}>
+                            </td>
+                            <td>
+                                <input type="checkbox" class="permission-checkbox" data-type="permission_pay_show" value="${ca.id}" ${ca.permission_pay_show ? 'checked' : ''}>
+                            </td>
+                            <td>
+                                <input type="checkbox" class="permission-checkbox" data-type="permission_pay_file" value="${ca.id}" ${ca.permission_pay_file ? 'checked' : ''}>
+                            </td>
+                            <td>
+                                <a href="#del-contract-audit" data-toggle="modal" data-target="#del-contract-audit" class="btn btn-outline-danger btn-sm ml-1 del-contract-audit-a" data-id="${ca.id}">移除</a>
+                            </td>
+                        </tr>`;
+            }
+            $('#contract-audit-list').html(list);
+            $("#contract-audit-table").bootstrapTable('resetView');
+        }
+
+        $('body').on('click', '.del-contract-audit-a', function () {
+            $('#del-audit-ids').val($(this).attr('data-id'));
+            $('#del-contract-audit').modal('show');
+        });
+
+        $('#del-audit-btn').click(function () {
+            let uids = $('#del-audit-ids').val();
+            postData('/financial/'+ $('#spid').val() + '/audit/save', { type: 'del-audit', id: uids.split(',') }, function (result) {
+                // toastr.success(`成功添加 位用户`);
+                $('#del-contract-audit').modal('hide');
+                setList(result);
+            })
+        });
+
+        // 上报人权限勾选
+        $('body').on('click', '.permission-checkbox', function () {
+            const type = $(this).attr('data-type');
+            const value = $(this).is(':checked') ? 1 : 0;
+            const id = parseInt($(this).val());
+            const updateInfo = { id };
+            updateInfo[type] = value;
+            postData('/financial/'+ $('#spid').val() + '/audit/save', { type: 'save-permission', updateData: updateInfo }, function (result) {
+            })
+        });
+    });
+</script>
+<% } %>

+ 134 - 0
app/view/financial/pay.ejs

@@ -0,0 +1,134 @@
+<% include ./sub_menu.ejs %>
+<div class="panel-content">
+    <div class="panel-title">
+        <div class="title-main  d-flex">
+            <% include ./sub_mini_menu.ejs %>
+            <div class="col-10 pl-0">
+                <div class="d-inline-block"><span class="mr-3">支付列表</span></div>
+                <div class="d-inline-block col-sm-3">
+                    <div class="input-group input-group-sm pr-1">
+                        <select class="form-control form-control-sm col-auto" id="tid_select">
+                            <option value="0">全部</option>
+                            <% for (const t of tenders) { %>
+                            <option value="<%- t.id %>" <% if (t.id === tid) { %>selected<% } %> ><%- t.name %></option>
+                            <% } %>
+                        </select>
+                    </div>
+                </div>
+                <div class="d-inline-block">
+                    <div class="input-group input-group-sm pr-1">
+                        <div class="btn-group">
+                            <button type="button" class="btn btn-sm btn-light text-primary dropdown-toggle" data-toggle="dropdown" id="used_selected" data-value="<%- used %>">资金用途:<%- used ? used : '全部' %></button>
+                            <div class="dropdown-menu" aria-labelledby="used_selected" id="used_select">
+                                <% if (used !== null) { %><a class="dropdown-item to-log-link" data-val="" href="javascript:void(0);">全部</a><% } %>
+                                <% for (const u of usedList) { %>
+                                <% if (used !== u) { %>
+                                <a class="dropdown-item to-log-link" href="javascript:void(0)" data-val="<%- u %>"><%- u %></a>
+                                <% } %>
+                                <% } %>
+                            </div>
+                        </div>
+                    </div>
+                </div>
+                <div class="d-inline-block">
+                    <div class="input-group input-group-sm pr-1">
+                        <div class="btn-group">
+                            <button type="button" class="btn btn-sm btn-light text-primary dropdown-toggle" data-toggle="dropdown" id="status_selected" data-value="<%- status %>">审批状态:<% if (status !== 0) { %><%- filter.statusString[status] %>(<%- filter.count[status] %>)<% } else { %>全部<% } %></button>
+                            <div class="dropdown-menu" aria-labelledby="status_selected" id="status_select">
+                                <% if (status !== 0) { %><a class="dropdown-item to-log-link" data-val="0" href="javascript:void(0);">全部</a><% } %>
+                                <% for (const fs in filter.status) { %>
+                                    <% const f = filter.status[fs]; %>
+                                    <% if (f !== status) { %><a class="dropdown-item to-log-link" data-val="<%- f %>" href="javascript:void(0);"><%- filter.statusString[f] %>(<%- filter.count[f] %>)</a><% } %>
+                                <% } %>
+                            </div>
+                        </div>
+                    </div>
+                </div>
+                <div class="d-inline-block">
+                    <a href="#payaccount" data-toggle="modal" data-target="#payaccount" class="btn btn-sm btn-primary mr-2" id="show-pay-account" style="display: none">付款账号</a>
+                    <% if (ctx.session.sessionUser.is_admin) { %>
+                    <a href="#liucheng" data-toggle="modal" data-target="#liucheng" class="btn btn-sm btn-primary mr-2">审批流程</a>
+                    <% } %>
+                </div>
+            </div>
+            <div class="d-inline-block ml-auto">
+                <% if (ctx.session.sessionUser.is_admin || fptAuditTids.length > 0) { %>
+                <a href="#add-pay" data-toggle="modal" data-target="#add-pay" class="btn btn-primary btn-sm pull-right">申请支付</a>
+                <% } %>
+<!--                <a href="#batch-sp" data-toggle="modal" data-target="#batch-sp" class="btn btn-success btn-sm pull-right mr-2">批量审批</a>-->
+            </div>
+        </div>
+    </div>
+    <div class="content-wrap">
+        <div class="c-body">
+            <div class="sjs-height-0">
+                <table class="table table-bordered text-center">
+                    <thead>
+                    <tr>
+                        <th style="min-width: 150px;">标段名称</th>
+                        <th width="200px">支付编号</th>
+                        <th width="150px">申请时间</th>
+                        <th width="100px">申请人</th>
+                        <th width="200px">资金用途</th>
+                        <th width="300px">收款单位</th>
+                        <th width="150px">支付金额</th>
+                        <th width="200px">审批进度</th>
+                        <th width="150px">操作</th>
+                    </tr>
+                    </thead>
+                    <tbody id="pay-list">
+                    <% for (const pay of payList) { %>
+                        <tr class="text-center" data-tid="<%- pay.tid %>">
+                            <td class="text-left"><%- pay.tenderName %></td>
+                            <td class=""><a href="/financial/<%- ctx.subProject.id %>/pay/<%- pay.id %>/detail"><%- pay.code %></a></td>
+                            <td class=""><%- moment(pay.create_time).format('YYYY-MM-DD') %></td>
+                            <td class=""><%- pay.username %></td>
+                            <td class=""><%- pay.used %></td>
+                            <td class=""><%- pay.entities %></td>
+                            <td class="text-right"><%- pay.total_price %></td>
+                            <td class="text-left <%- auditConst.auditProgressClass[pay.status] %>">
+                                <% if (pay.status === auditConst.status.checked && pay.final_auditor_str) { %>
+                                    <a href="#sp-list" data-toggle="modal" data-target="#sp-list" c-id="<%- pay.id %>"><%- pay.final_auditor_str %></a>
+                                <% } else if (pay.curAuditors.length > 0) { %>
+                                    <% if (pay.curAuditors[0].audit_type === auditType.key.common) { %>
+                                        <a href="#sp-list" data-toggle="modal" data-target="#sp-list" c-id="<%- pay.id %>"><%- pay.curAuditors[0].name %><%if (pay.curAuditors[0].role !== '' && pay.curAuditors[0].role !== null) { %>-<%- pay.curAuditors[0].role %><% } %></a>
+                                    <% } else { %>
+                                        <a href="#sp-list" data-toggle="modal" data-target="#sp-list" c-id="<%- pay.id %>"><%- ctx.helper.transFormToChinese(pay.curAuditors[0].audit_order) + '审' %></a>
+                                    <% } %>
+                                <% } %>
+                                <%- auditConst.auditProgress[pay.status] %>
+                            </td>
+                            <td>
+                                <% if (pay.status === auditConst.status.uncheck && pay.uid === ctx.session.sessionUser.accountId) { %>
+                                    <a href="/financial/<%- ctx.subProject.id %>/pay/<%- pay.id %>/detail" class="btn <%- auditConst.statusButtonClass[pay.status] %> btn-sm"><%- auditConst.statusButton[pay.status] %></a>
+                                <% } else if (pay.status === auditConst.status.checkNo && pay.curAuditors && pay.uid === ctx.session.sessionUser.accountId) { %>
+                                    <a href="/financial/<%- ctx.subProject.id %>/pay/<%- pay.id %>/detail" class="btn <%- auditConst.statusButtonClass[pay.status] %> btn-sm"><%- auditConst.statusButton[pay.status] %></a>
+                                <% } else if (pay.status === auditConst.status.checking && pay.curAuditors && pay.curAuditors.findIndex(x => { return x.aid === ctx.session.sessionUser.accountId; }) >= 0) { %>
+                                    <% const curAudit = pay.curAuditors.find(x => { return x.aid === ctx.session.sessionUser.accountId; }); %>
+                                    <% if (curAudit.status === auditConst.status.checking) { %>
+                                        <a href="/financial/<%- ctx.subProject.id %>/pay/<%- pay.id %>/detail" class="btn <%- auditConst.statusButtonClass[pay.status] %> btn-sm"><%- auditConst.statusButton[pay.status] %></a>
+                                    <% } else { %>
+                                        <span class="<%- auditConst.auditStringClass[curAudit.status] %>"><%- auditConst.auditString[curAudit.status] %></span>
+                                    <% } %>
+                                <% } else { %>
+                                    <span class="<%- auditConst.auditStringClass[pay.status] %>"><%- auditConst.auditString[pay.status] %></span>
+                                <% } %>
+                                <% if (pay.uid === ctx.session.sessionUser.accountId && (pay.status === auditConst.status.uncheck || pay.status === auditConst.status.checkNo)) { %><a href="javascript:void(0);" data-id="<%- pay.id %>" class="text-danger del-pay-btn">删除</a><% } %>
+                            </td>
+                        </tr>
+                    <% } %>
+                    </tbody>
+                </table>
+            </div>
+        </div>
+    </div>
+</div>
+<script>
+    const user_id = <%- ctx.session.sessionUser.accountId %>;
+    const is_admin = <%- ctx.session.sessionUser.is_admin %>;
+    const spid = '<%- ctx.subProject.id %>';
+    const tenders = JSON.parse(unescape('<%- escape(JSON.stringify(tenders)) %>'));
+    const fptAuditTids = JSON.parse(unescape('<%- escape(JSON.stringify(fptAuditTids)) %>'));
+    const auditConst = JSON.parse(unescape('<%- escape(JSON.stringify(auditConst)) %>'));
+    const auditType = JSON.parse(unescape('<%- escape(JSON.stringify(auditType)) %>'));
+</script>

+ 128 - 0
app/view/financial/pay_detail.ejs

@@ -0,0 +1,128 @@
+<% include ./sub_menu.ejs %>
+<div class="panel-content">
+    <div class="panel-title">
+        <div class="title-main  d-flex">
+            <% include ./sub_mini_menu.ejs %>
+            <div class="col-10 pl-0">
+<!--                <div class="d-inline-block mr-3">-->
+<!--                    <div class="btn-group btn-group-toggle group-tab" data-toggle="buttons">-->
+<!--                        <label class="btn btn-sm btn-light active">-->
+<!--                            <input type="radio" name="options" id="option0" autocomplete="off"> 支付详情-->
+<!--                        </label>-->
+<!--                        <label class="btn btn-sm btn-light">-->
+<!--                            <input type="radio" name="option3" id="option3" autocomplete="off"> 输出报表-->
+<!--                        </label>-->
+<!--                    </div>-->
+<!--                </div>-->
+                <div class="d-inline-block">
+                    <span class="mr-3"><strong><%- financialPay.tenderName %></strong></span>
+                </div>
+            </div>
+            <div class="d-inline-block ml-auto">
+                <% if (financialPay.status === auditConst.status.uncheck) { %>
+                    <% if (ctx.session.sessionUser.accountId === financialPay.uid) { %>
+                        <a id="sub-sp-btn" href="javascript: void(0);" data-toggle="modal" data-target="#sub-sp" class="btn btn-primary btn-sm">上报审批</a>
+                    <% } else { %>
+                        <a id="sub-sp-btn" href="javascript: void(0);" data-toggle="modal" data-target="#sub-sp" class="btn btn-outline-secondary btn-sm">上报中</a>
+                    <% } %>
+                <% } else if (financialPay.status === auditConst.status.checking) { %>
+                    <% if (financialPay.curAuditorIds.indexOf(ctx.session.sessionUser.accountId) >= 0) { %>
+                        <a id="sp-done-btn" href="javascript: void(0);" data-toggle="modal" data-target="#sp-done" class="btn btn-success btn-sm">审批通过</a>
+                        <a href="#sp-back" data-toggle="modal" data-target="#sp-back" class="btn btn-warning btn-sm">审批退回</a>
+                    <% } else { %>
+                        <a href="#sp-list" data-toggle="modal" data-target="#sp-list" class="btn btn-outline-secondary btn-sm">审批中</a>
+                    <% } %>
+                <% } else if (financialPay.status === auditConst.status.checked) { %>
+                    <a href="#sp-list" data-toggle="modal" data-target="#sp-list" class="btn btn-outline-success btn-sm">审批完成</a>
+                <% } else if (financialPay.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 text-muted sp-list-btn">审批<% if (financialPay.status === auditConst.status.checkNo) { %>退回<% } %></a>
+                    <% if (ctx.session.sessionUser.accountId === financialPay.uid) { %>
+                        <a href="#sp-list" data-type="show" data-toggle="modal" data-target="#sp-list"  class="btn btn-primary btn-sm sp-list-btn">重新上报</a>
+                    <% } %>
+                <% } %>
+            </div>
+        </div>
+    </div>
+    <div class="content-wrap">
+        <div class="c-body">
+            <div class="sjs-height-0">
+                <div class="col-xl-11 mx-auto">
+                    <h5 class="mt-4">基本信息</h5>
+                    <table class="table table-bordered" id="pay-table">
+                        <tbody>
+                        <tr>
+                            <th width="120px" class="text-center align-middle">支付编号</th>
+                            <td width="300px"><input class="form-control form-control-sm" disabled value="<%- financialPay.code %>" /></td>
+                            <th width="120px" class="text-center align-middle">支付用途</th>
+                            <% if (financialPay.readOnly) { %>
+                                <td width="300px"><input class="form-control form-control-sm" disabled value="<%- financialPay.used %>" /></td>
+                            <% } else { %>
+                            <td width="300px">
+                                <select class="form-control form-control-sm" data-name="used">
+                                    <% for (const used of usedList) { %>
+                                    <option <% if (used === financialPay.used) { %>selected<% } %>><%- used %></option>
+                                    <% } %>
+                                </select>
+                            </td>
+                            <% } %>
+                            <th width="120px" class="text-center align-middle">支付金额</th>
+                            <td width="300px"><input id="pay-total-price" class="form-control form-control-sm" disabled value="<%- financialPay.total_price %>" /></td>
+                        </tr>
+                        <tr>
+                            <th width="" class="text-center align-middle">申请支付单位</th>
+                            <td width=""><input type="text" data-name="entity" value="<%- financialPay.entity %>" <% if (financialPay.readOnly) { %>readonly<% } %> class="form-control form-control-sm"></td>
+                            <th width="" class="text-center align-middle">支付单位开户行</th>
+                            <td width=""><input type="text" data-name="bank" value="<%- financialPay.bank %>" <% if (financialPay.readOnly) { %>readonly<% } %> class="form-control form-control-sm"></td>
+                            <th width="" class="text-center align-middle">支付账号</th>
+                            <td width=""><input type="text" data-name="bank_account" value="<%- financialPay.bank_account %>" <% if (financialPay.readOnly) { %>readonly<% } %> class="form-control form-control-sm"></td>
+                        </tr>
+                        <tr>
+                            <th width="120px" class="text-center align-middle">备注</th>
+                            <td width=""colspan="5"><textarea class="form-control form-control-sm" data-name="remark" rows="3" <% if (financialPay.readOnly) { %>readonly<% } %>><%- financialPay.remark %></textarea></td>
+                        </tr>
+                        </tbody>
+                    </table>
+                    <div class="d-flex mt-4" id="qingdan">
+                        <h5 class="d-inline-block">支付明细</h5>
+                        <% if (!financialPay.readOnly) { %>
+                        <div class="d-inline-block ml-auto">
+                            <a href="javascript:void(0);" class="btn btn-sm btn-light text-primary mr-2" id="add-white-contract">添加</a>
+                            <a href="#add-deal" data-toggle="modal" data-target="#add-deal" class="btn btn-sm btn-light text-primary mr-2">添加合同</a>
+                        </div>
+                        <% } %>
+                    </div>
+                    <div class="modal-height-400" id="pay-contract-spread"></div>
+                    <h5 class="mt-4" id="fujian">附件</h5>
+                    <table class="table table-bordered">
+                        <thead>
+                        <tr>
+                            <th></th>
+                            <th>附件</th>
+                            <th>上传者</th>
+                            <th>上传时间</th>
+                            <th>操作</th>
+                        </tr>
+                        </thead>
+                        <tbody id="file-content">
+                        </tbody>
+                    </table>
+                </div>
+            </div>
+        </div>
+    </div>
+</div>
+<div style="display: none">
+    <img src="/public/images/file_clip.png" id="rela-file-icon" />
+    <img src="/public/images/file_clip_hover.png" id="rela-file-hover" />
+</div>
+<script>
+    const cur_uid = <%- ctx.session.sessionUser.accountId %>;
+    const auditConst = JSON.parse('<%- JSON.stringify(auditConst) %>');
+    const fileList = JSON.parse(unescape('<%- escape(JSON.stringify(fileList)) %>')) || [];
+    const whiteList = JSON.parse('<%- JSON.stringify(whiteList) %>');
+    const preUrl = '<%- preUrl2 %>';
+    const readOnly = <%- financialPay.readOnly %>;
+    const financialPay = JSON.parse(unescape('<%- escape(JSON.stringify(financialPay)) %>'));
+    const payTypeList = JSON.parse(unescape('<%- escape(JSON.stringify(payTypeList)) %>'));
+    let contractList;
+</script>

+ 758 - 0
app/view/financial/pay_detail_modal.ejs

@@ -0,0 +1,758 @@
+<% include ../shares/delete_hint_modal.ejs %>
+<!--添加附件-->
+<div class="modal fade" 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><a href="javascript: void(0);" class="btn btn-primary" id="file-modal-target">选择文件</a></p> -->
+                <input type="file" id="file-modal" multiple="multiple">
+            </div>
+            <div class="modal-footer">
+                <button id="file-cancel" type="button" class="btn btn-secondary btn-sm" data-dismiss="modal">取消</button>
+                <button id="file-ok" type="button" class="btn btn-primary btn-sm">添加</button>
+            </div>
+        </div>
+    </div>
+</div>
+<!--附件-->
+<div class="modal fade" id="pay-contract-file" data-backdrop="static" style="z-index: 1049">
+    <div class="modal-dialog modal-lg" role="document">
+        <div class="modal-content">
+            <div class="modal-header">
+                <h5 class="modal-title">附件</h5>
+            </div>
+            <div class="modal-body">
+                <% if (financialPay.filePermission) { %>
+                <div class="form-group upload-permission">
+                    <label for="formGroupExampleInput">单个文件大小限制:30MB,支持<span data-toggle="tooltip" data-placement="bottom" title="doc,docx,xls,xlsx,ppt,pptx,pdf">office等文档格式</span>、<span data-toggle="tooltip" data-placement="bottom" title="jpg,png,bmp">图片格式</span>、<span data-toggle="tooltip" data-placement="bottom" title="rar,zip">压缩包格式</span></label>
+                    <br>
+                    <input type="file" class="" multiple>
+                </div>
+                <div>
+                    <div class="form-check form-check-inline">
+                        <input class="form-check-input" type="radio" name="type" id="file_type_0" value="0" checked>
+                        <label class="form-check-label" for="file_type_0">普通附件</label>
+                    </div>
+                    <% if (!financialPay.readOnly) { %>
+                    <div class="form-check form-check-inline">
+                        <input class="form-check-input" type="radio" name="type" id="file_type_1" value="1">
+                        <label class="form-check-label" for="file_type_1">发票</label>
+                    </div>
+                    <% } %>
+                </div>
+                <% } %>
+                <div class="mt-2 modal-height-500" style="overflow:auto;">
+                    <table class="table table-sm table-bordered text-center" style="word-break:break-all; table-layout: fixed">
+                        <thead>
+                        <tr><th width="5%">序号</th><th>名称</th><th width="8%">上传人</th><th width="20%">发票号<th width="20%">上传时间</th><th width="15%">操作</th></tr>
+                        </thead>
+                        <tbody id="contract-files">
+                        </tbody>
+                    </table>
+                </div>
+            </div>
+            <div class="modal-footer">
+                <input value="" type="hidden" id="file-contract-id">
+                <button type="button" class="btn btn-secondary btn-sm" data-dismiss="modal">关闭</button>
+            </div>
+        </div>
+    </div>
+</div>
+<% if ((financialPay.status === auditConst.status.uncheck || financialPay.status === auditConst.status.checkNo) && ctx.session.sessionUser.accountId === financialPay.uid) { %>
+    <!--上报审批-->
+    <div class="modal fade" id="sub-sp" data-backdrop="static">
+        <div class="modal-dialog" role="document">
+            <div class="modal-content">
+                <div class="modal-header">
+                    <h5 class="modal-title">上报审批</h5>
+                </div>
+                <div class="modal-body">
+                    <div class="card mt-3">
+                        <div class="card-header">
+                            审批流程
+                        </div>
+                        <div class="modal-height-500" style="overflow: auto">
+                            <ul class="list-group list-group-flush" id="auditors">
+                                <% for (let i = 0, iLen = financialPay.auditorGroups.length; i < iLen; i++) { %>
+                                    <li class="list-group-item d-flex" auditorId="<%- financialPay.auditorGroups[i][0].aid %>">
+                                        <div class="col-auto"><%- i+1 %></div>
+                                        <div class="col">
+                                            <% for (const auditor of financialPay.auditorGroups[i]) { %>
+                                                <div class="d-inline-block mx-1" auditorId="<%- auditor.aid %>">
+                                                    <i class="fa fa-user text-muted"></i> <%- auditor.name %> <small class="text-muted"><%- auditor.role %></small>
+                                                </div>
+                                            <% } %>
+                                        </div>
+                                        <div class="col-auto">
+                                            <% if (financialPay.auditorGroups[i][0].audit_type !== auditType.key.common) { %>
+                                                <span class="badge badge-pill badge-<%- auditType.info[financialPay.auditorGroups[i][0].audit_type].class %> badge-bg-small"><small><%- auditType.info[financialPay.auditorGroups[i][0].audit_type].long%></small></span>
+                                            <% } %>
+                                        </div>
+                                    </li>
+                                <% } %>
+                            </ul>
+                        </div>
+                    </div>
+                </div>
+                <form class="modal-footer" method="post" action="<%- preUrl %>/audit/start" onsubmit="return checkStartFrom()">
+                    <button type="button" class="btn btn-secondary btn-sm" data-dismiss="modal">关闭</button>
+                    <input type="hidden" name="_csrf_j" value="<%= ctx.csrf %>">
+                    <% if (ctx.session.sessionUser.accountId === financialPay.uid) { %>
+                        <button class="btn btn-primary btn-sm" type="submit">确认上报</button>
+                    <% } %>
+                </form>
+            </div>
+        </div>
+    </div>
+    <!-- 添加合同 -->
+    <div class="modal fade show" id="add-deal" data-backdrop="static">
+        <div class="modal-dialog modal-xl" role="document">
+            <div class="modal-content">
+                <div class="modal-header">
+                    <h5 class="modal-title">添加合同</h5>
+                    <div class="ml-auto"><strong><%- financialPay.tenderName %></strong> </div>
+                </div>
+                <div class="modal-body">
+                    <div class="mb-2">
+                        <div class="d-inline-block">
+                            <h6 class="mt-1 col-auto px-0">合同分类:</h6>
+                        </div>
+                        <div class="d-inline-block col-4">
+                            <select class="form-control form-control-sm col-auto" id="contract-tree">
+                                <option value="">劳务</option>
+                                <option value="" selected>材料</option>
+                                <option value="">机械</option>
+                                <option value="">其他</option>
+                                <option value="" selected>全部</option>
+                            </select>
+                        </div>
+                        <div class="d-inline-block col-5 px-0">
+                            <div class="input-group">
+                                <input placeholder="输入合同编号/名称可查询" type="text" id="contract-keyword" name="keyword" value="" class="form-control form-control-sm">
+                                <div class="input-group-append">
+                                    <button class="btn btn-secondary btn-sm" id="search-contract-btn"><i aria-hidden="true" class="fa fa-search"></i></button>
+                                </div>
+                            </div>
+                        </div>
+                    </div>
+                    <div style="max-height: 500px;overflow: auto;">
+                        <table class="table table-bordered">
+                            <tr class="text-center">
+                                <th width="50px"><input type="checkbox" id="select-all-contract"></th>
+                                <th width="100px">合同类别</th>
+                                <th width="150px">合同编号</th>
+                                <th width="">合同名称</th>
+                                <th width="100px">合同金额</th>
+                                <th width="200px">收款单位</th>
+                                <th width="200px">收款账号</th>
+                            </tr>
+                            <tbody id="contract-list">
+                            </tbody>
+                        </table>
+                    </div>
+
+                </div>
+                <div class="modal-footer">
+                    <button type="button" class="btn btn-sm btn-secondary" data-dismiss="modal">关闭</button>
+                    <button type="button" class="btn btn-sm  btn-primary" id="add-contract-btn">确定添加</button>
+                </div>
+            </div>
+        </div>
+    </div>
+<% } %>
+<% if(financialPay && (financialPay.status !== auditConst.status.uncheck)) { %>
+    <!--审批流程/结果-->
+    <div class="modal fade" id="sp-list" data-backdrop="static">
+        <div class="modal-dialog modal-lg" role="document">
+            <div class="modal-content">
+                <div class="modal-header">
+                    <h5 class="modal-title"><%- financialPay.status === auditConst.status.checkNo ? '重新上报' : '审批流程' %></h5>
+                </div>
+                <div class="modal-body">
+                    <div class="row">
+                        <div class="col-4">
+                            <div class="card modal-height-500 mt-3" style="overflow: auto">
+                                <ul class="list-group list-group-flush auditors-list" id="auditors-list">
+                                    <% financialPay.auditors2.forEach((item, idx) => { %>
+                                        <% if (idx === 0) { %>
+                                            <li class="list-group-item d-flex justify-content-between align-items-center">
+                                                <span class="mr-1"><i class="fa fa fa-play-circle fa-rotate-90"></i></span>
+                                                <span class="text-muted">
+                                                    <% for (const u of item) { %>
+                                                        <small class="d-inline-block text-dark mx-1" title="<%- u.role %>" data-auditorId="<%- u.aid %>"><%- u.name %></small>
+                                                    <% } %>
+                                                </span>
+                                                <span class="badge badge-light badge-pill ml-auto"><small>原报</small></span>
+                                            </li>
+                                        <% } else if(idx === financialPay.auditors2.length -1 && idx !== 0) { %>
+                                            <li class="list-group-item d-flex justify-content-between align-items-center" data-auditorid="<%- item[0].aid %>">
+                                                <span class="mr-1"><i class="fa fa fa-stop-circle"></i></span>
+                                                <span class="text-muted">
+                                                    <% for (const u of item) { %>
+                                                        <small class="d-inline-block text-dark mx-1" title="<%- u.role %>" data-auditorId="<%- u.aid %>"><%- u.name %></small>
+                                                    <% } %>
+                                                </span>
+                                                <div class="d-flex ml-auto">
+                                                    <% if (item[0].audit_type !== auditType.key.common) { %>
+                                                        <span class="badge badge-pill badge-<%- auditType.info[item[0].audit_type].class %> p-1"><small><%- auditType.info[item[0].audit_type].short %></small></span>
+                                                    <% } %>
+                                                    <span class="badge badge-light badge-pill"><small>终审</small></span>
+                                                </div>
+                                            </li>
+                                        <% } else {%>
+                                            <li class="list-group-item d-flex justify-content-between align-items-center" data-auditorid="<%- item[0].aid %>">
+                                                <span class="mr-1"><i class="fa fa-chevron-circle-down"></i></span>
+                                                <span class="text-muted">
+                                                    <% for (const u of item) { %>
+                                                        <small class="d-inline-block text-dark mx-1" title="<%- u.role %>" data-auditorId="<%- u.aid %>"><%- u.name %></small>
+                                                    <% } %>
+                                                </span>
+                                                <div class="d-flex ml-auto">
+                                                    <% if (item[0].audit_type !== auditType.key.common) { %>
+                                                        <span class="badge badge-pill badge-<%- auditType.info[item[0].audit_type].class %> p-1"><small><%- auditType.info[item[0].audit_type].short %></small></span>
+                                                    <% } %>
+                                                    <span class="badge badge-light badge-pill"><small><%= ctx.helper.transFormToChinese(idx) %>审</small></span>
+                                                </div>
+                                            </li>
+                                        <% } %>
+                                    <% }) %>
+                                </ul>
+                            </div>
+                        </div>
+                        <div class="col-8 modal-height-500" style="overflow: auto">
+                            <% financialPay.auditHistory.forEach((his, idx) => { %>
+                                <!-- 展开/收起历史流程 -->
+                                <% if(idx === financialPay.auditHistory.length - 1 && financialPay.auditHistory.length !== 1) { %>
+                                    <div class="text-right">
+                                        <a href="javascript: void(0);" id="fold-btn" data-target="show">展开历史审批流程</a>
+                                    </div>
+                                <% } %>
+                                <div class="<%- idx < financialPay.auditHistory.length - 1 ? 'fold-card' : '' %>">
+                                    <div class="text-center text-muted"><%- idx+1 %>#</div>
+                                    <ul class="timeline-list list-unstyled mt-2 <% if (idx === financialPay.auditHistory.length - 1) { %>last-auditor-list<% } %>">
+                                        <% his.forEach((group, index) => { %>
+                                            <% if (index === 0) { %>
+                                                <li class="timeline-list-item pb-2">
+                                                    <div class="timeline-item-date">
+                                                        <%- group.beginYear %>
+                                                        <span><%- group.beginDate %></span>
+                                                        <span><%- group.beginTime %></span>
+                                                    </div>
+                                                    <div class="timeline-item-tail"></div>
+                                                    <div class="timeline-item-icon bg-success text-light"><i class="fa fa-caret-down"></i></div>
+                                                    <div class="timeline-item-content">
+                                                        <div class="py-1">
+                                                            <span class="text-black-50">原报</span>
+                                                            <span class="pull-right text-success"><%- idx !== 0 ? '重新' : '' %>上报审批</span>
+                                                        </div>
+                                                        <div class="card">
+                                                            <div class="card-body px-3 py-0">
+                                                                <div class="card-text p-2 py-3 row">
+                                                                    <div class="col">
+                                                                        <span class="h6"><%- financialPay.user.name %></span>
+                                                                        <span class="text-muted ml-1"><%- financialPay.user.role %></span>
+                                                                    </div>
+                                                                    <div class="col">
+                                                                        <span class="pull-right text-success"><i class="fa fa-check-circle"></i></span>
+                                                                    </div>
+                                                                </div>
+                                                            </div>
+                                                        </div>
+                                                    </div>
+                                                </li>
+                                            <% } %>
+                                            <li class="timeline-list-item pb-2 <% if (group.status === auditConst.status.uncheck && idx === financialPay.auditHistory.length - 1) { %>is_uncheck<% } %>">
+                                                <% if (group.endYear) { %>
+                                                    <div class="timeline-item-date">
+                                                        <%- group.endYear %>
+                                                        <span><%- group.endDate %></span>
+                                                        <span><%- group.endTime %></span>
+                                                    </div>
+                                                <% } %>
+                                                <% if (index < his.length - 1) { %>
+                                                    <div class="timeline-item-tail"></div>
+                                                <% } %>
+                                                <% if (group.status === auditConst.status.checked || group.status === auditConst.status.cancelRevise) { %>
+                                                    <div class="timeline-item-icon bg-success text-light"><i class="fa fa-check"></i></div>
+                                                <% } else if (ctx.helper._.includes([auditConst.status.checkNo], group.status)) { %>
+                                                    <div class="timeline-item-icon bg-warning text-light"><i class="fa fa-level-up"></i></div>
+                                                <% } else if (group.status === auditConst.status.checking) { %>
+                                                    <div class="timeline-item-icon bg-warning text-light"><i class="fa fa-ellipsis-h"></i></div>
+                                                <% } else { %>
+                                                    <div class="timeline-item-icon bg-secondary text-light"></div>
+                                                <% } %>
+                                                <div class="timeline-item-content">
+                                                    <div class="py-1">
+                                                    <span class="text-black-50">
+                                                        <%- (group.audit_order === 0 ? '原报' : !group.is_final ? group.audit_order + '审' : '终审') %>
+                                                        <% if (group.audit_type !== auditType.key.common) { %><span class="text-<%- auditType.info[group.audit_type].class %> "><%- auditType.info[group.audit_type].long %></span><% } %>
+                                                    </span>
+                                                        <% if (group.status !== auditConst.status.uncheck) { %>
+                                                            <span class="pull-right <%- auditConst.statusClass[group.status] %>"><%- auditConst.statusString[group.status] %></span>
+                                                        <% } %>
+                                                    </div>
+                                                    <div class="card">
+                                                        <div class="card-body px-3 py-0">
+                                                            <% for (const [i, auditor] of group.auditors.entries()) { %>
+                                                                <div class="card-text p-2 py-3 row <%- ( i > 0 ? 'border-top' : '') %>">
+                                                                    <div class="col">
+                                                                        <span class="h6"><%- auditor.name %></span>
+                                                                        <span class="text-muted ml-1"><%- auditor.role %></span>
+                                                                    </div>
+                                                                    <div class="col">
+                                                                        <% if (auditor.status === auditConst.status.checked) { %>
+                                                                            <span class="pull-right text-success"><i class="fa fa-check-circle"></i></span>
+                                                                        <% } else if (ctx.helper._.includes([auditConst.status.checkNo], auditor.status)) { %>
+                                                                            <span class="pull-right text-warning"><i class="fa fa-share-square fa-rotate-270"></i></span>
+                                                                        <% } else if (auditor.status === auditConst.status.checking) { %>
+                                                                            <span class="pull-right text-warning"><i class="fa fa-commenting"></i></span>
+                                                                        <% } %>
+                                                                    </div>
+                                                                    <% if (auditor.opinion) { %>
+                                                                        <div class="col-12 py-1 bg-light"><i class="fa fa-commenting-o mr-1"></i><%- auditor.opinion %></div>
+                                                                    <% } %>
+                                                                </div>
+                                                            <% } %>
+                                                        </div>
+                                                    </div>
+                                                </div>
+                                            </li>
+                                        <% }) %>
+                                    </ul>
+                                </div>
+                            <% }) %>
+                        </div>
+                    </div>
+                </div>
+                <form class="modal-footer" method="post" action="<%- preUrl %>/audit/start" onsubmit="return checkStartFrom()">
+                    <input type="hidden" name="_csrf_j" value="<%= ctx.csrf %>">
+                    <button type="button" class="btn btn-secondary btn-sm" data-dismiss="modal">关闭</button>
+                    <% if(financialPay.status === auditConst.status.checkNo && ctx.session.sessionUser.accountId === financialPay.uid) { %>
+                        <button class="btn btn-primary btn-sm sp-list-item" type="submit">确认上报</button>
+                    <% } %>
+                </form>
+            </div>
+        </div>
+    </div>
+<% } %>
+<% if (financialPay.status === auditConst.status.checking) { %>
+    <% if (financialPay.curAuditorIds.indexOf(ctx.session.sessionUser.accountId) >= 0) { %>
+        <!--审批通过-->
+        <div class="modal fade sp-location-list" id="sp-done" data-backdrop="static">
+            <div class="modal-dialog modal-lg" role="document">
+                <form class="modal-content" action="<%- preUrl %>/audit/check" method="post" onsubmit="return auditCheck(0);">
+                    <div class="modal-header">
+                        <h5 class="modal-title">审批通过</h5>
+                    </div>
+                    <div class="modal-body">
+                        <div class="row">
+                            <div class="col-4">
+                                <div class="card modal-height-500 mt-3" style="overflow: auto">
+                                    <ul class="list-group list-group-flush auditors-list">
+                                        <% financialPay.auditors2.forEach((item, idx) => { %>
+                                            <% if (idx === 0) { %>
+                                                <li class="list-group-item d-flex justify-content-between align-items-center">
+                                                    <span class="mr-1"><i class="fa fa fa-play-circle fa-rotate-90"></i></span>
+                                                    <span class="text-muted">
+                                                        <% for (const u of item) { %>
+                                                            <small class="d-inline-block text-dark mx-1" title="<%- u.role %>" data-auditorId="<%- u.aid %>"><%- u.name %></small>
+                                                        <% } %>
+                                                    </span>
+                                                    <span class="badge badge-light badge-pill ml-auto"><small>原报</small></span>
+                                                </li>
+                                            <% } else if(idx === financialPay.auditors2.length -1 && idx !== 0) { %>
+
+                                                <li class="list-group-item d-flex justify-content-between align-items-center">
+                                                    <span class="mr-1"><i class="fa fa fa-stop-circle"></i></span>
+                                                    <span class="text-muted">
+                                                        <% for (const u of item) { %>
+                                                            <small class="d-inline-block text-dark mx-1" title="<%- u.role %>" data-auditorId="<%- u.aid %>"><%- u.name %></small>
+                                                        <% } %>
+                                                    </span>
+                                                    <div class="d-flex ml-auto">
+                                                        <% if (item[0].audit_type !== auditType.key.common) { %>
+                                                            <span class="badge badge-pill badge-<%-  auditType.info[item[0].audit_type].class %> p-1"><small><%- auditType.info[item[0].audit_type].short %></small></span>
+                                                        <% } %>
+                                                        <span class="badge badge-light badge-pill"><small>终审</small></span>
+                                                    </div>
+                                                </li>
+                                            <% } else {%>
+                                                <li class="list-group-item d-flex justify-content-between align-items-center">
+                                                    <span class="mr-1"><i class="fa fa-chevron-circle-down"></i></span>
+                                                    <span class="text-muted">
+                                                        <% for (const u of item) { %>
+                                                            <small class="d-inline-block text-dark mx-1" title="<%- u.role %>" data-auditorId="<%- u.aid %>"><%- u.name %></small>
+                                                        <% } %>
+                                                    </span>
+                                                    <div class="d-flex ml-auto">
+                                                        <% if (item[0].audit_type !== auditType.key.common) { %>
+                                                            <span class="badge badge-pill badge-<%-  auditType.info[item[0].audit_type].class %> p-1 ml-auto"><small><%- auditType.info[item[0].audit_type].short %></small></span>
+                                                        <% } %>
+                                                        <span class="badge badge-light badge-pill"><small><%= ctx.helper.transFormToChinese(idx) %>审</small></span>
+                                                    </div>
+                                                </li>
+                                            <% } %>
+                                        <% }) %>
+                                    </ul>
+                                </div>
+                            </div>
+                            <div class="col-8 modal-height-500" style="overflow: auto">
+                                <% financialPay.auditHistory.forEach((his, idx) => { %>
+                                    <!-- 展开/收起历史流程 -->
+                                    <% if(idx === financialPay.auditHistory.length - 1 && financialPay.auditHistory.length !== 1) { %>
+                                        <div class="text-right">
+                                            <a href="javascript: void(0);" id="fold-btn" data-target="show">展开历史审批流程</a>
+                                        </div>
+                                    <% } %>
+                                    <div class="<%- idx < financialPay.auditHistory.length - 1 ? 'fold-card' : '' %>">
+                                        <div class="text-center text-muted"><%- idx+1 %>#</div>
+                                        <ul class="timeline-list list-unstyled mt-2 <% if (idx === financialPay.auditHistory.length - 1) { %>last-auditor-list<% } %>">
+                                            <% his.forEach((group, index) => { %>
+                                                <% if (index === 0) { %>
+                                                    <li class="timeline-list-item pb-2">
+                                                        <div class="timeline-item-date">
+                                                            <%- group.beginYear %>
+                                                            <span><%- group.beginDate %></span>
+                                                            <span><%- group.beginTime %></span>
+                                                        </div>
+                                                        <div class="timeline-item-tail"></div>
+                                                        <div class="timeline-item-icon bg-success text-light"><i class="fa fa-caret-down"></i></div>
+                                                        <div class="timeline-item-content">
+                                                            <div class="py-1">
+                                                                <span class="text-black-50">原报</span>
+                                                                <span class="pull-right text-success"><%- idx !== 0 ? '重新' : '' %>上报审批</span>
+                                                            </div>
+                                                            <div class="card">
+                                                                <div class="card-body px-3 py-0">
+                                                                    <div class="card-text p-2 py-3 row">
+                                                                        <div class="col">
+                                                                            <span class="h6"><%- financialPay.user.name %></span>
+                                                                            <span class="text-muted ml-1"><%- financialPay.user.role %></span>
+                                                                        </div>
+                                                                        <div class="col">
+                                                                            <span class="pull-right text-success"><i class="fa fa-check-circle"></i></span>
+                                                                        </div>
+                                                                    </div>
+                                                                </div>
+                                                            </div>
+                                                        </div>
+                                                    </li>
+                                                <% } %>
+                                                <li class="timeline-list-item pb-2 <% if (group.status === auditConst.status.uncheck && idx === financialPay.auditHistory.length - 1) { %>is_uncheck<% } %>">
+                                                    <% if (group.endYear) { %>
+                                                        <div class="timeline-item-date">
+                                                            <%- group.endYear %>
+                                                            <span><%- group.endDate %></span>
+                                                            <span><%- group.endTime %></span>
+                                                        </div>
+                                                    <% } %>
+                                                    <% if (index < his.length - 1) { %>
+                                                        <div class="timeline-item-tail"></div>
+                                                    <% } %>
+                                                    <% if (group.status === auditConst.status.checked) { %>
+                                                        <div class="timeline-item-icon bg-success text-light"><i class="fa fa-check"></i></div>
+                                                    <% } else if (ctx.helper._.includes([auditConst.status.checkNo], group.status)) { %>
+                                                        <div class="timeline-item-icon bg-warning text-light"><i class="fa fa-level-up"></i></div>
+                                                    <% } else if (group.status === auditConst.status.checking) { %>
+                                                        <div class="timeline-item-icon bg-warning text-light"><i class="fa fa-ellipsis-h"></i></div>
+                                                    <% } else { %>
+                                                        <div class="timeline-item-icon bg-secondary text-light"></div>
+                                                    <% } %>
+                                                    <div class="timeline-item-content">
+                                                        <div class="py-1">
+                                                        <span class="text-black-50">
+                                                            <%- (group.audit_order === 0 ? '原报' : !group.is_final ? group.audit_order + '审' : '终审') %>
+                                                            <% if (group.audit_type !== auditType.key.common) { %><span class="text-<%- auditType.info[group.audit_type].class %> "><%- auditType.info[group.audit_type].long %></span><% } %>
+                                                        </span>
+                                                            <% if (group.status !== auditConst.status.uncheck) { %>
+                                                                <span class="pull-right <%- auditConst.statusClass[group.status] %>"><%- auditConst.statusString[group.status] %></span>
+                                                            <% } %>
+                                                        </div>
+                                                        <div class="card">
+                                                            <div class="card-body px-3 py-0">
+                                                                <% for (const [i, auditor] of group.auditors.entries()) { %>
+                                                                    <div class="card-text p-2 py-3 row <%- ( i > 0 ? 'border-top' : '') %>">
+                                                                        <div class="col">
+                                                                            <span class="h6"><%- auditor.name %></span>
+                                                                            <span class="text-muted ml-1"><%- auditor.role %></span>
+                                                                        </div>
+                                                                        <div class="col">
+                                                                            <% if (auditor.status === auditConst.status.checked) { %>
+                                                                                <span class="pull-right text-success"><i class="fa fa-check-circle"></i></span>
+                                                                            <% } else if (ctx.helper._.includes([auditConst.status.checkNo], auditor.status)) { %>
+                                                                                <span class="pull-right text-warning"><i class="fa fa-share-square fa-rotate-270"></i></span>
+                                                                            <% } else if (auditor.status === auditConst.status.checking) { %>
+                                                                                <span class="pull-right text-warning"><i class="fa fa-commenting"></i></span>
+                                                                            <% } %>
+                                                                        </div>
+                                                                        <% if (auditor.status !== auditConst.status.uncheck && auditor.opinion) { %>
+                                                                            <div class="col-12 py-1 bg-light"><i class="fa fa-commenting-o mr-1"></i><%- auditor.opinion %></div>
+                                                                        <% } %>
+                                                                        <% if (auditor.status === auditConst.status.checking && auditor.aid === ctx.session.sessionUser.accountId) { %>
+                                                                            <div class="col-12 py-1 bg-light">
+                                                                                <textarea class="form-control form-control-sm" name="opinion">同意</textarea>
+                                                                            </div>
+                                                                        <% } %>
+                                                                    </div>
+                                                                <% } %>
+                                                            </div>
+                                                        </div>
+                                                    </div>
+                                                </li>
+                                            <% }) %>
+                                        </ul>
+                                    </div>
+                                <% }) %>
+                            </div>
+                        </div>
+                    </div>
+                    <div class="modal-footer">
+                        <button type="button" class="btn btn-secondary btn-sm" data-dismiss="modal">关闭</button>
+                        <input type="hidden" name="_csrf_j" value="<%= ctx.csrf %>" />
+                        <input type="hidden" name="checkType" value="<%= auditConst.status.checked %>" />
+                        <button type="submit" class="btn btn-success btn-sm">确认通过</button>
+                    </div>
+                </form>
+            </div>
+        </div>
+        <!--审批退回-->
+        <div class="modal fade sp-location-list" id="sp-back" data-backdrop="static">
+            <div class="modal-dialog modal-lg" role="document">
+                <form class="modal-content modal-lg" action="<%- preUrl %>/audit/check" method="post"
+                      onsubmit="return auditCheck(1);">
+                    <div class="modal-header">
+                        <h5 class="modal-title">审批退回</h5>
+                    </div>
+                    <div class="modal-body">
+                        <div class="row">
+                            <div class="col-4">
+                                <div class="card modal-height-500 mt-3" style="overflow: auto">
+                                    <ul class="list-group list-group-flush auditors-list">
+                                        <% financialPay.auditors2.forEach((item, idx) => { %>
+                                            <% if (idx === 0) { %>
+                                                <li class="list-group-item d-flex justify-content-between align-items-center">
+                                                    <span class="mr-1"><i class="fa fa fa-play-circle fa-rotate-90"></i></span>
+                                                    <span class="text-muted">
+                                                        <% for (const u of item) { %>
+                                                            <small class="d-inline-block text-dark mx-1" title="<%- u.role %>" data-auditorId="<%- u.aid %>"><%- u.name %></small>
+                                                        <% } %>
+                                                    </span>
+                                                    <span class="badge badge-light badge-pill ml-auto"><small>原报</small></span>
+                                                </li>
+                                            <% } else if(idx === financialPay.auditors2.length -1 && idx !== 0) { %>
+                                                <li class="list-group-item d-flex justify-content-between align-items-center">
+                                                    <span class="mr-1"><i class="fa fa fa-stop-circle"></i></span>
+                                                    <span class="text-muted">
+                                                        <% for (const u of item) { %>
+                                                            <small class="d-inline-block text-dark mx-1" title="<%- u.role %>" data-auditorId="<%- u.aid %>"><%- u.name %></small>
+                                                        <% } %>
+                                                    </span>
+                                                    <div class="d-flex ml-auto">
+                                                        <% if (item[0].audit_type !== auditType.key.common) { %>
+                                                            <span class="badge badge-pill badge-<%-  auditType.info[item[0].audit_type].class %> p-1"><small><%- auditType.info[item[0].audit_type].short %></small></span>
+                                                        <% } %>
+                                                        <span class="badge badge-light badge-pill"><small>终审</small></span>
+                                                    </div>
+                                                </li>
+                                            <% } else {%>
+                                                <li class="list-group-item d-flex justify-content-between align-items-center">
+                                                    <span class="mr-1"><i class="fa fa-chevron-circle-down"></i></span>
+                                                    <span class="text-muted">
+                                                        <% for (const u of item) { %>
+                                                            <small class="d-inline-block text-dark mx-1" title="<%- u.role %>" data-auditorId="<%- u.aid %>"><%- u.name %></small>
+                                                        <% } %>
+                                                    </span>
+                                                    <div class="d-flex ml-auto">
+                                                        <% if (item[0].audit_type !== auditType.key.common) { %>
+                                                            <span class="badge badge-pill badge-<%-  auditType.info[item[0].audit_type].class %> p-1"><small><%- auditType.info[item[0].audit_type].short %></small></span>
+                                                        <% } %>
+                                                        <span class="badge badge-light badge-pill"><small><%= ctx.helper.transFormToChinese(idx) %>审</small></span>
+                                                    </div>
+                                                </li>
+                                            <% } %>
+                                        <% }) %>
+                                    </ul>
+                                </div>
+                            </div>
+                            <div class="col-8 modal-height-500" style="overflow: auto">
+                                <% financialPay.auditHistory.forEach((his, idx) => { %>
+                                    <!-- 展开/收起历史流程 -->
+                                    <% if(idx === financialPay.auditHistory.length - 1 && financialPay.auditHistory.length !== 1) { %>
+                                        <div class="text-right">
+                                            <a href="javascript: void(0);" id="fold-btn" data-target="show" data-idx="<%- idx + 1 %>">展开历史审批流程</a>
+                                        </div>
+                                    <% } %>
+                                    <div class="<%- idx < financialPay.auditHistory.length - 1 ? 'fold-card' : '' %>">
+                                        <div class="text-center text-muted"><%- idx+1 %>#</div>
+                                        <ul class="timeline-list list-unstyled mt-2 <% if (idx === financialPay.auditHistory.length - 1) { %>last-auditor-list<% } %>">
+                                            <% his.forEach((group, index) => { %>
+                                                <% if (index === 0) { %>
+                                                    <li class="timeline-list-item pb-2">
+                                                        <div class="timeline-item-date">
+                                                            <%- group.beginYear %>
+                                                            <span><%- group.beginDate %></span>
+                                                            <span><%- group.beginTime %></span>
+                                                        </div>
+                                                        <div class="timeline-item-tail"></div>
+                                                        <div class="timeline-item-icon bg-success text-light"><i class="fa fa-caret-down"></i></div>
+                                                        <div class="timeline-item-content">
+                                                            <div class="py-1">
+                                                                <span class="text-black-50">原报</span>
+                                                                <span class="pull-right text-success"><%- idx !== 0 ? '重新' : '' %>上报审批</span>
+                                                            </div>
+                                                            <div class="card">
+                                                                <div class="card-body px-3 py-0">
+                                                                    <div class="card-text p-2 py-3 row">
+                                                                        <div class="col">
+                                                                            <span class="h6"><%- financialPay.user.name %></span>
+                                                                            <span class="text-muted ml-1"><%- financialPay.user.role %></span>
+                                                                        </div>
+                                                                        <div class="col">
+                                                                            <span class="pull-right text-success"><i class="fa fa-check-circle"></i></span>
+                                                                        </div>
+                                                                    </div>
+                                                                </div>
+                                                            </div>
+                                                        </div>
+                                                    </li>
+                                                <% } %>
+                                                <li class="timeline-list-item pb-2 <% if (group.status === auditConst.status.uncheck && idx === financialPay.auditHistory.length - 1) { %>is_uncheck<% } %>">
+                                                    <% if (his.endYear) { %>
+                                                        <div class="timeline-item-date">
+                                                            <%- group.endYear %>
+                                                            <span><%- group.endDate %></span>
+                                                            <span><%- group.endTime %></span>
+                                                        </div>
+                                                    <% } %>
+                                                    <% if (index < his.length - 1) { %>
+                                                        <div class="timeline-item-tail"></div>
+                                                    <% } %>
+                                                    <% if (group.status === auditConst.status.checked) { %>
+                                                        <div class="timeline-item-icon bg-success text-light"><i class="fa fa-check"></i></div>
+                                                    <% } else if (ctx.helper._.includes([auditConst.status.checkNo], group.status)) { %>
+                                                        <div class="timeline-item-icon bg-warning text-light"><i class="fa fa-level-up"></i></div>
+                                                    <% } else if (group.status === auditConst.status.checking) { %>
+                                                        <div class="timeline-item-icon bg-warning text-light"><i class="fa fa-ellipsis-h"></i></div>
+                                                    <% } else { %>
+                                                        <div class="timeline-item-icon bg-secondary text-light"></div>
+                                                    <% } %>
+                                                    <div class="timeline-item-content">
+                                                        <div class="py-1">
+                                                        <span class="text-black-50">
+                                                            <%- (group.audit_order === 0 ? '原报' : !group.is_final ? group.audit_order + '审' : '终审') %>
+                                                            <% if (group.audit_type !== auditType.key.common) { %><span class="text-<%- auditType.info[group.audit_type].class %> "><%- auditType.info[group.audit_type].long %></span><% } %>
+                                                        </span>
+                                                            <% if (group.status !== auditConst.status.uncheck) { %>
+                                                                <span class="pull-right <%- auditConst.statusClass[group.status] %>"><%- auditConst.statusString[group.status] %></span>
+                                                            <% } %>
+                                                        </div>
+                                                        <div class="card">
+                                                            <div class="card-body px-3 py-0">
+                                                                <% for (const [i, auditor] of group.auditors.entries()) { %>
+                                                                    <div class="card-text p-2 py-3 row <%- ( i > 0 ? 'border-top' : '') %>">
+                                                                        <div class="col">
+                                                                            <span class="h6"><%- auditor.name %></span>
+                                                                            <span class="text-muted ml-1"><%- auditor.role %></span>
+                                                                        </div>
+                                                                        <div class="col">
+                                                                            <% if (auditor.status === auditConst.status.checked) { %>
+                                                                                <span class="pull-right text-success"><i class="fa fa-check-circle"></i></span>
+                                                                            <% } else if (ctx.helper._.includes([auditConst.status.checkNo], auditor.status)) { %>
+                                                                                <span class="pull-right text-warning"><i class="fa fa-share-square fa-rotate-270"></i></span>
+                                                                            <% } else if (auditor.status === auditConst.status.checking) { %>
+                                                                                <span class="pull-right text-warning"><i class="fa fa-commenting"></i></span>
+                                                                            <% } %>
+                                                                        </div>
+                                                                        <% if (auditor.status !== auditConst.status.uncheck && auditor.opinion) { %>
+                                                                            <div class="col-12 py-1 bg-light"><i class="fa fa-commenting-o mr-1"></i><%- auditor.opinion%></div>
+                                                                        <% } %>
+                                                                        <% if (auditor.status === auditConst.status.checking && auditor.aid === ctx.session.sessionUser.accountId) { %>
+                                                                            <div class="col-12 py-1 bg-light">
+                                                                                <textarea class="form-control form-control-sm" name="opinion">不同意</textarea>
+                                                                                <div class="alert alert-warning mt-1 mb-0 p-2">
+                                                                                    <div class="form-check form-check-inline">
+                                                                                        <input class="form-check-input" type="radio" name="checkType" id="inlineRadio1" value="<%- auditConst.status.checkNo %>" checked>
+                                                                                        <label class="form-check-label" for="inlineRadio1">退回原报 <%- financialPay.user.name %></label>
+                                                                                    </div>
+                                                                                </div>
+                                                                            </div>
+                                                                        <% } %>
+                                                                    </div>
+                                                                <% } %>
+                                                            </div>
+                                                        </div>
+                                                    </div>
+                                                </li>
+                                            <% }) %>
+                                        </ul>
+                                    </div>
+                                <% }) %>
+                            </div>
+                        </div>
+                    </div>
+                    <div class="modal-footer">
+                        <button type="button" class="btn btn-secondary btn-sm" data-dismiss="modal">关闭</button>
+                        <input type="hidden" name="_csrf_j" value="<%= ctx.csrf %>" />
+                        <button type="submit" class="btn btn-warning btn-sm">确认退回</button>
+                    </div>
+                </form>
+            </div>
+        </div>
+    <% } %>
+<% } %>
+<script>
+    $(function () {
+        $('.sp-location-list').on('shown.bs.modal', function () {
+            const scrollBox = $(this).find('div[class="col-8 modal-height-500"]');
+            const bdiv = (scrollBox.offset() && scrollBox.offset().top) || 0;
+            scrollBox.scrollTop(0);
+            const hdiv = divSearch($(this).find('textarea')) ? $(this).find('textarea') : null;
+            const hdheight = hdiv ? hdiv.parents('.timeline-item-content').offset().top : null;
+            if (hdiv && scrollBox.length && scrollBox[0].scrollHeight > 200 && hdheight - bdiv > 200) {
+                scrollBox.scrollTop(hdheight - bdiv);
+            }
+        });
+
+        function divSearch(div) {
+            if (div.length > 0) {
+                return true;
+            }
+            return false;
+        }
+
+        // 展开历史审核记录
+        $('.modal-body #fold-btn').click(function () {
+            const type = $(this).data('target')
+            const auditCard = $(this).parent().parent()
+            if (type === 'show') {
+                $(this).data('target', 'hide')
+                auditCard.find('.fold-card').slideDown('swing', () => {
+                    auditCard.find('#fold-btn').text('收起历史审核记录')
+                })
+            } else {
+                $(this).data('target', 'show')
+                auditCard.find('.fold-card').slideUp('swing', () => {
+                    auditCard.find('#fold-btn').text('展开历史审核记录')
+                })
+            }
+        });
+
+        $('.sp-list-btn').click(function () {
+            const type = $(this).data('type')
+            if (type === 'hide') {
+                $('.sp-list-item').hide()
+                $('.modal-title').text('审批流程')
+            } else {
+                $('.sp-list-item').show()
+                $('.modal-title').text('重新上报')
+            }
+        });
+    });
+</script>

+ 205 - 0
app/view/financial/pay_modal.ejs

@@ -0,0 +1,205 @@
+<% include ../shares/delete_hint_modal.ejs %>
+<% if (ctx.session.sessionUser.is_admin || fptAuditTids.length > 0) { %>
+<!-- 申请支付 -->
+<div class="modal fade show" id="add-pay" data-backdrop="static">
+    <div class="modal-dialog" role="document">
+        <div class="modal-content">
+            <div class="modal-header">
+                <h5 class="modal-title">申请支付</h5>
+            </div>
+            <div class="modal-body">
+                <div class="form-group mb-2">
+                    <label>支付标段<strong class="text-danger">*</strong></label>
+                    <select class="form-control form-control-sm" id="add-pay-tender">
+                        <% for (const t of tenders) { %>
+                        <% if (ctx.session.sessionUser.is_admin || ctx.helper._.includes(fptAuditTids, t.id)) { %>
+                            <option value="<%- t.id %>"><%- t.name %></option>
+                        <% } %>
+                        <% } %>
+                    </select>
+                </div>
+                <div class="form-group mb-2">
+                    <label>支付编号</label>
+                    <input class="form-control form-control-sm" placeholder="标段编号-年月日-3位流水号" id="add-pay-code" type="text" readonly="">
+                    <span class="text-danger"></span>
+                </div>
+                <div class="form-group mb-2">
+                    <label>支付用途</label>
+                    <select class="form-control form-control-sm" id="add-pay-used">
+                        <% for (const used of usedList) { %>
+                            <option><%- used %></option>
+                        <% } %>
+                    </select>
+                </div>
+            </div>
+            <div class="modal-footer">
+                <button type="button" class="btn btn-sm btn-secondary" data-dismiss="modal">关闭</button>
+                <button type="button" class="btn btn-sm  btn-primary" id="add-pay-btn">确定添加</button>
+            </div>
+        </div>
+    </div>
+</div>
+<% } %>
+<!--批量审批-->
+<div class="modal fade" id="batch-sp" data-backdrop="static">
+    <div class="modal-dialog" role="document">
+        <div class="modal-content">
+            <div class="modal-header">
+                <h5 class="modal-title">批量审批</h5>
+            </div>
+            <div class="modal-body">
+                <table class="table table-bordered">
+                    <tr><th>选择</th><th>标段名称</th><th>支付编号</th><th>收款单位</th><th>支付金额</th></tr>
+                    <tr><td><input type="checkbox"></td><td>TJ01</td><td>TJ01-20231207001</td><td>中国交通物资有限公司</td><td class="text-right">12345678</td></tr>
+                    <tr><td><input type="checkbox"></td><td>TJ01</td><td>TJ01-20231207001</td><td>中国交通物资有限公司</td><td class="text-right">12345678</td></tr>
+                    <tr><td><input type="checkbox"></td><td>TJ01</td><td>TJ01-20231207001</td><td>中国交通物资有限公司</td><td class="text-right">12345678</td></tr>
+                    <tr><td><input type="checkbox"></td><td>TJ01</td><td>TJ01-20231207001</td><td>中国交通物资有限公司</td><td class="text-right">12345678</td></tr>
+                </table>
+            </div>
+            <div class="modal-footer">
+                <button type="button" class="btn btn-sm btn-secondary" data-dismiss="modal">关闭</button>
+                <button type="button" class="btn btn-sm btn-success">审批</button>
+            </div>
+        </div>
+    </div>
+</div>
+<!--付款账号-->
+<div class="modal fade" id="payaccount" 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">
+                <table class="table table-bordered">
+                    <tr><td>开户名称<b class="text-danger">*</b></td><td><input type="text" name="name" class="form-control form-control-sm"></td></tr>
+                    <tr><td>开户银行<b class="text-danger">*</b></td><td><input type="text" name="bank" class="form-control form-control-sm"></td></tr>
+                    <tr><td>开户账号<b class="text-danger">*</b></td><td><input type="text" name="bank_account" class="form-control form-control-sm"></td></tr>
+                    <tr><td>联系人</td><td><input type="text" name="contact" class="form-control form-control-sm"></td></tr>
+                    <tr><td>联系电话</td><td><input type="text" name="phone" class="form-control form-control-sm"></td></tr>
+                    </tbody>
+                </table>
+            </div>
+            <div class="modal-footer">
+                <input type="hidden" name="id" value="">
+                <input type="hidden" name="tid" value="">
+                <button type="button" class="btn btn-sm btn-primary mr-auto" id="get-form-tender">从计量标段同步</button>
+                <button type="button" class="btn btn-sm btn-secondary" data-dismiss="modal">关闭</button>
+                <button type="button" class="btn btn-sm btn-primary" id="set-pay-btn">确定添加</button>
+            </div>
+        </div>
+    </div>
+</div>
+<!--审批流程/结果-->
+<div class="modal fade" id="sp-list" data-backdrop="static">
+    <div class="modal-dialog modal-lg" role="document">
+        <div class="modal-content">
+            <div class="modal-header">
+                <h5 class="modal-title">审批流程</h5>
+            </div>
+            <div class="modal-body">
+                <div class="row">
+                    <div class="col-4 modal-height-500" style="overflow: auto">
+                        <div class="card mt-3">
+                            <ul class="list-group list-group-flush" id="auditor-list">
+                            </ul>
+                        </div>
+                    </div>
+                    <div class="col-8 modal-height-500" style="overflow: auto" id="audit-list">
+                    </div>
+                </div>
+            </div>
+            <div class="modal-footer">
+                <button type="button" class="btn btn-secondary btn-sm" data-dismiss="modal">关闭</button>
+            </div>
+        </div>
+    </div>
+</div>
+<% if (ctx.session.sessionUser.is_admin) { %>
+<div class="modal fade" id="liucheng" data-backdrop="static" style="z-index: 1049">
+    <div class="modal-dialog modal-xl" role="document">
+        <div class="modal-content">
+            <div class="modal-header">
+                <h5 class="modal-title">审批流程</h5>
+            </div>
+            <div class="modal-body pt-0">
+                <div class="row" style="min-height: 400px;max-height: 700px;overflow:auto;">
+                    <div class="col-3">
+                        <div class="p-2">标段列表</div>
+                        <table class="table table-bordered">
+                            <thead>
+                            <tr class="text-center"><th width="50px">选择</th><th>标段名称</th></tr>
+                            </thead>
+                            <tbody id="shenpi-tender-list">
+                            <% for (const t of tenders) { %>
+                                <tr data-tid="<%- t.id %>"><td class="text-center"><input type="checkbox"></td><td class="change-tender" style="cursor: pointer;"><%- t.name %></td></tr>
+                            <% } %>
+                            </tbody>
+                        </table>
+                    </div>
+                    <div class="col-4">
+                        <div class="d-flex flex-row bg-graye">
+                            <div class="px-2 pt-1 dropdown">
+                                <button class="btn btn-outline-primary btn-sm dropdown-toggle" type="button" id="report_audit_dropdownMenuButton" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
+                                    添加用户
+                                </button>
+                                <div class="dropdown-menu dropdown-menu-right" id="report_audit_dropdownMenu" aria-labelledby="report_audit_dropdownMenuButton" style="width:220px">
+                                    <div class="mb-2 p-2"><input class="form-control form-control-sm gr-search" placeholder="姓名/手机 检索" data-code="report_audit" 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 => { %>
+                                                    <dd class="border-bottom p-2 mb-0 " data-id="<%- item.id %>" >
+                                                        <p class="mb-0 d-flex"><span class="text-primary"><%- item.name %></span><span
+                                                                    class="ml-auto"><%- item.mobile %></span></p>
+                                                        <span class="text-muted"><%- item.role %></span>
+                                                    </dd>
+                                                <% });%>
+                                            </div>
+                                        <% }) %>
+                                    </dl>
+                                </div>
+                            </div>
+                            <div class="p-2"><a href="javascript:void(0);" class="text-danger" id="batch-del-ptAudit">批量删除</a></div>
+                            <div class="p-2"><a href="javascript:void(0);" id="batch-other-ptAudit">同步勾选用户至其他已勾选标段</a></div>
+                        </div>
+                        <table class="table table-bordered">
+                            <thead>
+                            <tr class="text-center"><th><input type="checkbox" id="select-all-ptAudits"></th><th>用户名</th><th>单位</th><th>填报</th><th>操作</th></tr>
+                            </thead>
+                            <tbody id="report-list">
+                            </tbody>
+                        </table>
+                    </div>
+                    <div class="col-5">
+                        <div class="d-flex flex-row bg-graye">
+                            <div class="p-2"><a href="javascript:void(0);" id="set-other-tenders">设置流程至其他已勾选标段</a></div>
+                        </div>
+<!--                        <div>-->
+<!--                            <ul class="list-unstyled">-->
+<!--                                <li class="d-flex justify-content-start align-items-center">-->
+<!--                                    <span class="col-auto">填报人</span>-->
+<!--                                    <span class="col-10 spr-span" id="report-list">-->
+<!--                                    </span>-->
+<!--                                </li>-->
+<!--                            </ul>-->
+<!--                        </div>-->
+                        <div id="shenpi-list">
+                        </div>
+                    </div>
+                </div>
+            </div>
+            <div class="modal-footer">
+                <button type="button" class="btn btn-sm btn-secondary" data-dismiss="modal">关闭</button>
+            </div>
+        </div>
+    </div>
+</div>
+<script>
+    const accountGroup = JSON.parse(unescape('<%- escape(JSON.stringify(accountGroup)) %>'));
+    const accountList = JSON.parse(unescape('<%- escape(JSON.stringify(accountList)) %>'));
+    const sp_status = JSON.parse('<%- JSON.stringify(shenpi.sp_status) %>');
+    const sp_type = JSON.parse('<%- JSON.stringify(shenpi.sp_other_type) %>');
+</script>
+<% } %>

+ 15 - 0
app/view/financial/sub_menu.ejs

@@ -0,0 +1,15 @@
+<div class="panel-sidebar" id="sub-menu">
+    <div class="sidebar-title" data-toggle="tooltip" data-placement="right" data-original-title="<%- ctx.subProject.name %>"><%- ctx.subProject.name %></div>
+    <div class="scrollbar-auto">
+        <% include ./sub_menu_list.ejs %>
+        <div class="side-show"></div>
+        <div class="side-fold" data-toggle="tooltip" data-placement="top" data-original-title="折叠侧栏" id="to-mini-menu">
+            <i class="fa fa-angle-left"></i>
+        </div>
+    </div>
+    <script>
+        new Vue({
+            el: '.scrollbar-auto',
+        });
+    </script>
+</div>

+ 26 - 0
app/view/financial/sub_menu_list.ejs

@@ -0,0 +1,26 @@
+<nav-menu title="返回" url="<%- preUrl %>" tclass="text-primary" ml="1" icon="fa-chevron-left"></nav-menu>
+<% if (financialPermission.transfer_show) { %>
+<div class="nav-box">
+    <ul class="nav-list list-unstyled">
+        <li class="<% if (ctx.url.indexOf('/transfer') !== -1) { %>active<% } %>">
+            <a href="/financial/<%- ctx.subProject.id %>/transfer"><span class="ml-3">资金划拨</span></a>
+        </li>
+    </ul>
+</div>
+<% } %>
+<% if (financialPermission.pay_show) { %>
+<div class="nav-box">
+    <ul class="nav-list list-unstyled">
+        <li class="<% if (ctx.url.indexOf('/pay') !== -1) { %>active<% } %>">
+            <a href="/financial/<%- ctx.subProject.id %>/pay"><span class="ml-3">资金支付</span></a>
+        </li>
+    </ul>
+</div>
+<% } %>
+<div class="nav-box">
+    <ul class="nav-list list-unstyled">
+        <li class="<% if (ctx.url.indexOf('/summary') !== -1) { %>active<% } %>">
+            <a href="/financial/<%- ctx.subProject.id %>/summary"><span class="ml-3">资金统计</span></a>
+        </li>
+    </ul>
+</div>

+ 16 - 0
app/view/financial/sub_mini_menu.ejs

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

+ 59 - 0
app/view/financial/transfer.ejs

@@ -0,0 +1,59 @@
+<% include ./sub_menu.ejs %>
+<div class="panel-content">
+    <div class="panel-title">
+        <div class="title-main  d-flex">
+            <% include ./sub_mini_menu.ejs %>
+            <h2>划拨列表</h2>
+            <div class="ml-auto">
+                <% if (ctx.session.sessionUser.is_admin || financialPermission.transfer_add) { %>
+                <a href="#add-transfer" data-toggle="modal" data-target="#add-transfer" class="btn btn-primary btn-sm pull-right">新建划拨</a>
+                <% } %>
+            </div>
+        </div>
+    </div>
+    <div class="content-wrap">
+        <div class="c-body">
+            <div class="sjs-height-0">
+                <table class="table table-bordered text-center">
+                    <thead>
+                    <tr>
+                        <th width="100px">次数</th>
+                        <th width="10%">划拨年月</th>
+                        <th width="10%">划拨金额</th>
+                        <th width="10%">填报人</th>
+                        <th>填报单位</th>
+                        <th width="150px">填报时间</th>
+                        <th width="15%">填报备注</th>
+                        <th width="150px">附件</th>
+                        <th width="150px">操作</th>
+                    </tr>
+                    </thead>
+                    <tbody id="transfer-list">
+                    <% if (transferList.length > 0) { %>
+                    <% for (const [i, t] of transferList.entries()) { %>
+                    <tr class="text-center" data-id="<%- t.id %>">
+                        <td class=""><%- (transferList.length - i) %></td>
+                        <td class=""><a href="/financial/<%- ctx.subProject.id %>/transfer/<%- t.id %>/tender"><%- t.t_time %></a></td>
+                        <td class="text-right"><%- t.total_price %></td>
+                        <td class=""><%- t.username %></td>
+                        <td class="text-left" ><%- t.company %></td>
+                        <td class="" ><%- moment(t.create_time).format('YYYY-MM-DD HH:mm:ss') %></td>
+                        <td class="" ><span class="show-remark"><%- t.remark %></span> <% if (t.uid === ctx.session.sessionUser.accountId) { %><a href="javascript:void(0);" class="edit-remark" data-id="<%- t.id %>"><i class="fa fa-pencil-square-o"></i></a><% } %></td>
+                        <td class="" ><a href="javascript:void(0);" data-trid="<%- t.id %>" class="text-primary open-transfer-files"><i class="fa fa-paperclip fa-rotate-90"></i></a> <span class="file-num"><%- t.files.length > 0 ? t.files.length : '' %></span></td>
+                        <td><% if (t.uid === ctx.session.sessionUser.accountId) { %><a href="javascript:void(0);" class="text-danger del-transfer-btn" data-id="<%- t.id %>">删除</a><% } %></td>
+                    </tr>
+                    <% } %>
+                    <% } %>
+                    </tbody>
+                </table>
+            </div>
+        </div>
+    </div>
+</div>
+<script>
+    const user_id = <%- ctx.session.sessionUser.accountId %>;
+    const spid = '<%- ctx.subProject.id %>';
+    const whiteList = JSON.parse(unescape('<%- escape(JSON.stringify(whiteList)) %>'));
+    const transferList = JSON.parse(unescape('<%- escape(JSON.stringify(transferList)) %>'));
+    const financialPermission = JSON.parse(unescape('<%- escape(JSON.stringify(financialPermission)) %>'));
+</script>

+ 80 - 0
app/view/financial/transfer_modal.ejs

@@ -0,0 +1,80 @@
+<% include ../shares/delete_hint_modal.ejs %>
+<% if (ctx.session.sessionUser.is_admin || financialPermission.transfer_add) { %>
+<!-- 新建划拨 -->
+<div class="modal fade show" id="add-transfer" data-backdrop="static">
+    <div class="modal-dialog" role="document">
+        <form class="modal-content" id="add-transfer-form" action="/financial/<%- ctx.subProject.id %>/transfer/add" method="post">
+            <div class="modal-header">
+                <h5 class="modal-title">新建划拨</h5>
+            </div>
+            <div class="modal-body">
+                <div class="form-group mb-2">
+                    <label>项目名称</label>
+                    <input class="form-control form-control-sm" value="<%- ctx.subProject.name %>" type="text" readonly="">
+                </div>
+                <div class="form-group mb-2">
+                    <label>划拨年月</label>
+                    <input class="datepicker-here form-control form-control-sm" name="date" id="transfer-date" readonly placeholder="点击选择划拨年月" data-view="months" data-min-view="months" data-date-format="yyyy-MM" data-language="zh" type="text">
+                </div>
+                <div class="form-group mb-2">
+                    <label>填报备注</label>
+                    <textarea class="form-control form-control-sm" name="remark" id="transfer-remark" rows="3"></textarea>
+                </div>
+            </div>
+            <div class="modal-footer">
+                <input type="hidden" name="_csrf_j" value="<%= ctx.csrf %>" />
+                <button type="button" class="btn btn-sm btn-secondary" data-dismiss="modal">关闭</button>
+                <button type="button" class="btn btn-sm btn-primary" id="add-transfer-btn">确定添加</button>
+            </div>
+        </form>
+    </div>
+</div>
+<% } %>
+<!--附件-->
+<div class="modal fade" id="transfer-file" data-backdrop="static" style="z-index: 1049">
+    <input type="hidden" name="trid">
+    <div class="modal-dialog modal-lg" role="document">
+        <div class="modal-content">
+            <div class="modal-header">
+                <h5 class="modal-title">附件</h5>
+            </div>
+            <div class="modal-body">
+                <div class="form-group upload-permission">
+                    <label for="formGroupExampleInput">单个文件大小限制:30MB,支持<span data-toggle="tooltip" data-placement="bottom" title="doc,docx,xls,xlsx,ppt,pptx,pdf">office等文档格式</span>、<span data-toggle="tooltip" data-placement="bottom" title="jpg,png,bmp">图片格式</span>、<span data-toggle="tooltip" data-placement="bottom" title="rar,zip">压缩包格式</span></label>
+                    <br>
+                    <input type="file" class="" multiple>
+                </div>
+                <div class="modal-height-500" style="overflow:auto;">
+                    <table class="table table-sm table-bordered text-center" style="word-break:break-all; table-layout: fixed">
+                        <thead>
+                        <tr><th width="5%">序号</th><th>名称</th><th width="8%">上传人</th><th width="20%">上传时间</th><th width="15%">操作</th></tr>
+                        </thead>
+                        <tbody>
+                        </tbody>
+                    </table>
+                </div>
+            </div>
+            <div class="modal-footer">
+                <button type="button" class="btn btn-outline-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>
+<div class="modal fade" id="edit-remark-modal" data-backdrop="static" style="z-index: 1049">
+    <input type="hidden" name="ftid">
+    <div class="modal-dialog" role="document">
+        <div class="modal-content">
+            <div class="modal-header">
+                <h5 class="modal-title">填报备注(<span class="transfer-time"></span>)</h5>
+            </div>
+            <div class="modal-body">
+                <textarea class="form-control form-control-sm" rows="5" name="remark"></textarea>
+            </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="save-remark-btn">确定</button>
+            </div>
+        </div>
+    </div>
+</div>

+ 73 - 0
app/view/financial/transfer_tender.ejs

@@ -0,0 +1,73 @@
+<% include ./sub_menu.ejs %>
+<div class="panel-content">
+    <div class="panel-title">
+        <div class="title-main  d-flex">
+            <% include ./sub_mini_menu.ejs %>
+            <h2><%- transferInfo.t_time %></h2>
+            <div class="ml-auto">
+                <% if (transferInfo.uid === ctx.session.sessionUser.accountId) { %>
+                <% if (transferInfo.is_lock) { %>
+                <a href="#unlock" data-toggle="modal" data-target="#unlock" class="btn btn-danger btn-sm pull-right">解锁数据</a>
+                <% } else { %>
+                <a href="#lock" data-toggle="modal" data-target="#lock" class="btn btn-success btn-sm pull-right mr-2">锁定数据</a>
+                <a href="#add-bd" data-toggle="modal" data-target="#add-bd" class="btn btn-primary btn-sm pull-right mr-2">添加标段</a>
+                <% } %>
+                <% } %>
+            </div>
+        </div>
+    </div>
+    <div class="content-wrap">
+        <div class="c-body">
+            <div class="sjs-height-0">
+                <table class="table table-bordered">
+                    <thead class="text-center">
+                    <tr>
+                        <th width="50px">序号</th>
+                        <th width="">标段名称</th>
+                        <th width="130px">计量期数</th>
+                        <th width="100px">合同金额</th>
+                        <th width="100px">本期合同计量</th>
+                        <th width="100px">本期变更计量</th>
+                        <th width="100px">本期完成计量</th>
+                        <th width="100px">本期应付</th>
+                        <th width="100px">本期实付</th>
+                        <th width="100px">本次划拨金额</th>
+                        <th width="80px">附件</th>
+                        <% if (transferInfo.uid === ctx.session.sessionUser.accountId) { %>
+                        <th width="80px">操作</th>
+                        <% } %>
+                    </tr>
+                    </thead>
+                    <tbody id="tender-list">
+                    <% if (transferTenderList.length > 0) { %>
+                    <% for (const [i, t] of transferTenderList.entries()) { %>
+                    <tr class="text-right" data-id="<%- t.id %>">
+                        <td class="text-center"><%- i+1 %></td>
+                        <td class="text-left"><%- t.name %></td>
+                        <td class="text-center">第<%- t.sorder %>期</td>
+                        <td class="" ><%- t.total_price %></td>
+                        <td class="" ><%- t.contract_tp %></td>
+                        <td class="" ><%- t.qc_tp %></td>
+                        <td class="" ><%- ctx.helper.sum([t.contract_tp, t.qc_tp, t.pc_tp]) %></td>
+                        <td class="" ><%- t.yf_tp %></td>
+                        <td class="" ><%- t.sf_tp %></td>
+                        <td><% if (transferInfo.uid === ctx.session.sessionUser.accountId && !transferInfo.is_lock) { %><input type="text" class="form-control form-control-sm text-right" placeholder="默认等于本期实付" data-ftid="<%- t.id %>" value="<%- t.hb_tp %>"><% } else { %><%- t.hb_tp %><% } %></td>
+                        <td class="text-center" ><a href="javascript:void(0);" class="text-primary open-tender-files" data-ftid="<%- t.id %>"><i class="fa fa-paperclip fa-rotate-90"></i></a> <span class="file-num"><%- t.files.length > 0 ? t.files.length : '' %></span></td>
+                        <% if (transferInfo.uid === ctx.session.sessionUser.accountId) { %><td class="text-center"><% if (!transferInfo.is_lock) { %><a class="text-danger del-tender-btn" href="javascript:void(0);" data-id="<%- t.id %>">移除</a><% } %></td><% } %>
+                    </tr>
+                    <% } %>
+                    <% } %>
+                    </tbody>
+                </table>
+            </div>
+        </div>
+    </div>
+</div>
+<script>
+    const user_id = <%- ctx.session.sessionUser.accountId %>;
+    const spid = '<%- ctx.subProject.id %>';
+    const trid = '<%- transferInfo.id %>';
+    const whiteList = JSON.parse(unescape('<%- escape(JSON.stringify(whiteList)) %>'));
+    const tenderList = JSON.parse(unescape('<%- escape(JSON.stringify(transferTenderList)) %>'));
+    const financialPermission = JSON.parse(unescape('<%- escape(JSON.stringify(financialPermission)) %>'));
+</script>

+ 111 - 0
app/view/financial/transfer_tender_modal.ejs

@@ -0,0 +1,111 @@
+<% include ../shares/delete_hint_modal.ejs %>
+<% if (transferInfo.uid === ctx.session.sessionUser.accountId) { %>
+<% if (!transferInfo.is_lock) { %>
+<!--锁定数据-->
+<div class="modal fade" id="lock" 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">
+                <h6>锁定后,本次划拨数据将无法编辑。</h6>
+            </div>
+            <div class="modal-footer">
+                <button type="button" class="btn btn-sm btn-secondary" data-dismiss="modal">关闭</button>
+                <button type="button" class="btn btn-sm btn-success" id="lock-transfer-btn">锁定</button>
+            </div>
+        </div>
+    </div>
+</div>
+<% } else { %>
+<!--解锁数据-->
+<div class="modal fade" id="unlock" 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">
+                <h6>数据解锁后,将允许修改本次划拨金额。</h6>
+            </div>
+            <div class="modal-footer">
+                <button type="button" class="btn btn-sm btn-secondary" data-dismiss="modal">关闭</button>
+                <button type="button" class="btn btn-sm btn-danger" id="unlock-transfer-btn">解锁</button>
+            </div>
+        </div>
+    </div>
+</div>
+<% } %>
+<!--添加标段-->
+<div class="modal fade" id="add-bd" 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">
+                <table class="table table-bordered">
+                    <thead>
+                    <tr>
+                        <th width="50px">选择</th>
+                        <th width="">标段名称</th>
+                        <th width="150px">选择期数</th>
+                    </tr>
+                    </thead>
+                    <tbody id="tenders">
+                    <% for (const t of tenders) { %>
+                    <tr class="text-center">
+                        <td><input type="checkbox" value="<%- t.id %>"></td>
+                        <td class="text-left"><%- t.name %></td>
+                        <td>
+                            <select class="form-control form-control-sm" size="3" multiple>
+                                <% for (const s of t.stages) { %>
+                                <option value="<%- s.order %>">第<%- s.order %>期</option>
+                                <% } %>
+                            </select>
+                        </td>
+                    </tr>
+                    <% } %>
+                    </tbody>
+                </table>
+            </div>
+            <div class="modal-footer">
+                <button type="button" class="btn btn-sm btn-secondary" data-dismiss="modal">取消</button>
+                <button type="button" class="btn  btn-sm btn-primary" id="add-tender-btn">确认</button>
+            </div>
+        </div>
+    </div>
+</div>
+<% } %>
+<!--附件-->
+<div class="modal fade" id="tender-file" data-backdrop="static" style="z-index: 1049">
+    <input type="hidden" name="ftid">
+    <div class="modal-dialog modal-lg" role="document">
+        <div class="modal-content">
+            <div class="modal-header">
+                <h5 class="modal-title">附件</h5>
+            </div>
+            <div class="modal-body">
+                <div class="form-group upload-permission">
+                    <label for="formGroupExampleInput">单个文件大小限制:30MB,支持<span data-toggle="tooltip" data-placement="bottom" title="doc,docx,xls,xlsx,ppt,pptx,pdf">office等文档格式</span>、<span data-toggle="tooltip" data-placement="bottom" title="jpg,png,bmp">图片格式</span>、<span data-toggle="tooltip" data-placement="bottom" title="rar,zip">压缩包格式</span></label>
+                    <br>
+                    <input type="file" class="" multiple>
+                </div>
+                <div class="modal-height-500" style="overflow:auto;">
+                    <table class="table table-sm table-bordered text-center" style="word-break:break-all; table-layout: fixed">
+                        <thead>
+                        <tr><th width="5%">序号</th><th>名称</th><th width="8%">上传人</th><th width="20%">上传时间</th><th width="15%">操作</th></tr>
+                        </thead>
+                        <tbody>
+                        </tbody>
+                    </table>
+                </div>
+            </div>
+            <div class="modal-footer">
+                <button type="button" class="btn btn-outline-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>

+ 6 - 2
app/view/measure/compare_modal.ejs

@@ -5,8 +5,8 @@
             <div class="modal-header">
                 <h5 class="modal-title">选择参与比较的期</h5>
             </div>
-            <div class="modal-body">
-                <table class="table table-sm">
+            <div class="modal-body scroll-y" style="height: 600px">
+                <table class="table table-sm" >
                     <tr><th>期</th><th width="90">选择</th></tr>
                     <% for (const s of stages) { %>
                     <tr stage-id="<%- s.id %>"><td><%- s.order %>期</td><td><input type="checkbox"></td></tr>
@@ -14,6 +14,10 @@
                 </table>
             </div>
             <div class="modal-footer">
+                <div class="form-check form-check-inline">
+                    <input class="form-check-input" type="checkbox" id="select-qi-all">
+                    <label class="form-check-label" for="select-qi-all">全选</label>
+                </div>
                 <button type="button" class="btn btn-secondary btn-sm" data-dismiss="modal">关闭</button>
                 <button type="button" class="btn btn-primary btn-sm" id="select-qi-ok">确认</button>
             </div>

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

@@ -107,11 +107,9 @@
                         <td class="text-center">
                             <% if (s.status === auditConst.status.uncheck && s.user_id === ctx.session.sessionUser.accountId) { %>
                             <a href="<%- '/tender/' + ctx.tender.id + '/measure/stage/' + s.order %>" target="_blank" class="btn <%- auditConst.statusButtonClass[s.status] %> btn-sm"><%- auditConst.statusButton[s.status] %></a>
-                            <% } else if (s.status === auditConst.status.checkNo && s.curAuditors && s.user_id === ctx.session.sessionUser.accountId) { %>
+                            <% } else if (s.status === auditConst.status.checkNo && s.user_id === ctx.session.sessionUser.accountId) { %>
                             <a href="<%- '/tender/' + ctx.tender.id + '/measure/stage/' + s.order %>" target="_blank" class="btn <%- auditConst.statusButtonClass[s.status] %> btn-sm"><%- auditConst.statusButton[s.status] %></a>
-                            <% } else if (s.status === auditConst.status.checking && s.curAuditors && s.curAuditors.findIndex(x => { return x.aid === ctx.session.sessionUser.accountId; }) >= 0) { %>
-                            <a href="<%- '/tender/' + ctx.tender.id + '/measure/stage/' + s.order %>" target="_blank" class="btn <%- auditConst.statusButtonClass[s.status] %> btn-sm"><%- auditConst.statusButton[s.status] %></a>
-                            <% } else if (s.status === auditConst.status.checkNoPre && s.curAuditor2 && s.curAuditor2.findIndex(x => { return x.aid === ctx.session.sessionUser.accountId; }) >= 0) { %>
+                            <% } else if ((s.status === auditConst.status.checking || s.status === auditConst.status.checkNoPre) && s.curAuditors && s.curAuditors.findIndex(x => { return x.aid === ctx.session.sessionUser.accountId; }) >= 0) { %>
                             <a href="<%- '/tender/' + ctx.tender.id + '/measure/stage/' + s.order %>" target="_blank" class="btn <%- auditConst.statusButtonClass[s.status] %> btn-sm"><%- auditConst.statusButton[s.status] %></a>
                             <% } else { %>
                             <span class="<%- auditConst.auditStringClass[s.status] %>"><%- auditConst.auditString[s.status] %></span>

+ 1 - 1
app/view/shares/batch_import_modal.ejs

@@ -20,7 +20,7 @@
                                 <label class="form-check-label" for="bi-ignore">以本标段清单为准</label>
                             </div>
                             <div class="form-check form-check-inline">
-                                <input class="form-check-input" type="checkbox" id="bi-change">
+                                <input class="form-check-input" type="checkbox" id="bi-change" checked>
                                 <label class="form-check-label" for="bi-change">含变更</label>
                             </div>
                         </div>

+ 66 - 0
app/view/shares/import_file_modal.ejs

@@ -0,0 +1,66 @@
+<div class="modal fade" id="base-import-file" data-backdrop="static" enctype="multipart/form-data">
+    <div class="modal-dialog" role="document">
+        <div class="modal-content">
+            <div class="modal-header">
+                <h5 class="modal-title">导入</h5>
+            </div>
+            <div class="modal-body">
+                <div class="form-group">
+                    <label >选择文件</label>
+                    <input type="file" class="form-control-file" id="bsf-file" accept="*.cpd">
+                </div>
+            </div>
+            <div class="modal-footer d-flex justify-content-between">
+                <div class="ml-auto">
+                    <button type="button" class="btn btn-secondary btn-sm" data-dismiss="modal">关闭</button>
+                    <button type="button" class="btn btn-primary btn-sm" id="bsf-ok">确认</button>
+                </div>
+            </div>
+        </div>
+    </div>
+</div>
+<script>
+    const BaseImportFile = (function () {
+        let importSetting;
+        // 清除上一次数据
+        $('#base-import-file').bind('hidden.bs.modal', function () {
+            $('#bsf-file').val('');
+        });
+
+        // 上传excel内容,并导入
+        $('#bsf-ok').click(function () {
+            const files = $('#bsf-file')[0].files;
+            const formData = new FormData();
+
+            for (const file of files) {
+                if (file === undefined) {
+                    toast('未选择上传文件!', 'error');
+                    return false;
+                }
+                const filesize = file.size;
+                if (filesize > 30 * 1024 * 1024) {
+                    toast('存在上传文件大小过大!', 'error');
+                    return false;
+                }
+                const fileext = '.' + file.name.toLowerCase().split('.').splice(-1)[0];
+                if (importSetting.validList.indexOf(fileext) === -1) {
+                    toast('只能上传指定格式的附件!', 'error');
+                    return false;
+                }
+                formData.append('size', filesize);
+                formData.append('file[]', file);
+            }
+
+            postDataWithFile(importSetting.url, formData, function (data) {
+                if (importSetting.afterImport) importSetting.afterImport(data);
+                $('#base-import-file').modal('hide');
+            });
+        });
+
+        const show = function (setting) {
+            importSetting = setting;
+            $('#base-import-file').modal('show');
+        }
+        return { show };
+    })();
+</script>

+ 1 - 1
app/view/shares/tender_select_modal.ejs

@@ -28,7 +28,7 @@
                                 <label class="form-check-label" for="ts-ignore">以本标段清单为准</label>
                             </div>
                             <div class="form-check form-check-inline">
-                                <input class="form-check-input" type="checkbox" id="ts-change">
+                                <input class="form-check-input" type="checkbox" id="ts-change" checked>
                                 <label class="form-check-label" for="ts-change">含变更</label>
                             </div>
                         </div>

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

@@ -658,4 +658,5 @@
 <% include ../shares/import_excel_modal.ejs%>
 <% include ../shares/stage_stash_modal.ejs%>
 <% include ../shares/delete_hint_modal.ejs%>
-<% include ../shares/batch_import_modal.ejs%>
+<% include ../shares/batch_import_modal.ejs%>
+<% include ../shares/import_file_modal.ejs%>

+ 2 - 1
config/config.default.js

@@ -124,7 +124,8 @@ module.exports = appInfo => {
 
     // 上传设置
     config.multipart = {
-        whitelist: ['.json', '.txt',
+        whitelist: ['.cpd',
+            '.json', '.txt',
             '.xls', '.xlsx',
             '.doc', '.docx',
             '.pdf',

+ 0 - 13
config/config.local.js

@@ -58,19 +58,6 @@ module.exports = appInfo => {
         app: true,
     };
 
-    // 上传设置
-    config.multipart = {
-        whitelist: ['.json', '.txt',
-            '.xls', '.xlsx',
-            '.doc', '.docx',
-            '.pdf',
-            '.ppt', '.pptx',
-            '.png', '.jpg', '.jpeg', '.gif', '.bmp', '.cad', '.dwg',
-            '.zip', '.rar', '.7z', ''],
-        fileSize: '100mb',
-        fields: '15',
-    };
-
     // session配置
     config.session = {
         key: 'ZHC_SESS',

+ 0 - 11
config/config.remoteqa.js

@@ -55,17 +55,6 @@ module.exports = appInfo => {
         app: true,
     };
 
-    // 上传设置
-    config.multipart = {
-        whitelist: ['.json', '.txt',
-            '.xls', '.xlsx',
-            '.doc', '.docx',
-            '.pdf',
-            '.png', '.jpg', '.jpeg', '.gif', '.bmp',
-            '.zip', '.rar', '.7z'],
-        fileSize: '30mb',
-    };
-
     // session配置
     config.session = {
         key: 'ZHC_SESS',

+ 8 - 0
config/menu.js

@@ -81,6 +81,14 @@ const menu = {
         caption: '施工日志',
         children: null,
     },
+    financial: {
+        name: '资金监管',
+        icon: 'fa-money',
+        display: true,
+        url: '/financial',
+        caption: '资金监管',
+        children: null,
+    },
     management: {
         name: '项目管理系统',
         icon: 'fa-cubes',

+ 87 - 0
config/web.js

@@ -1623,6 +1623,93 @@ const JsFiles = {
                 mergeFile: 'contract_detail',
             },
         },
+        financial: {
+            index: {
+                files: [
+                    '/public/js/moment/moment.min.js',
+                    '/public/js/spreadjs/sheets/v11/gc.spread.sheets.all.11.2.2.min.js',
+                    '/public/js/decimal.min.js',
+                ],
+                mergeFiles: [
+                    '/public/js/zh_calc.js',
+                    '/public/js/shares/drag_tree.js',
+                    '/public/js/shares/sjs_setting.js',
+                    '/public/js/path_tree.js',
+                    '/public/js/shares/tenders2tree.js',
+                    '/public/js/shares/show_level.js',
+                    '/public/js/spreadjs_rela/spreadjs_zh.js',
+                    '/public/js/financial_index.js',
+                ],
+                mergeFile: 'financial_index',
+            },
+            transfer: {
+                files: [
+                    '/public/js/decimal.min.js',
+                    '/public/js/math.min.js',
+                    '/public/js/component/menu.js',
+                    '/public/js/moment/moment.min.js',
+                ],
+                mergeFiles: [
+                    '/public/js/sub_menu.js',
+                    '/public/js/div_resizer.js',
+                    '/public/js/datepicker/datepicker.min.js',
+                    '/public/js/datepicker/datepicker.zh.js',
+                    '/public/js/zh_calc.js',
+                    '/public/js/financial_transfer.js',
+                ],
+                mergeFile: 'financial_transfer',
+            },
+            transferTender: {
+                files: [
+                    '/public/js/decimal.min.js',
+                    '/public/js/math.min.js',
+                    '/public/js/component/menu.js',
+                    '/public/js/moment/moment.min.js',
+                ],
+                mergeFiles: [
+                    '/public/js/sub_menu.js',
+                    '/public/js/div_resizer.js',
+                    '/public/js/zh_calc.js',
+                    '/public/js/financial_transfer_tender.js',
+                ],
+                mergeFile: 'financial_transfer_tender',
+            },
+            pay: {
+                files: [
+                    '/public/js/decimal.min.js',
+                    '/public/js/math.min.js',
+                    '/public/js/component/menu.js',
+                    '/public/js/moment/moment.min.js',
+                ],
+                mergeFiles: [
+                    '/public/js/sub_menu.js',
+                    '/public/js/div_resizer.js',
+                    '/public/js/zh_calc.js',
+                    '/public/js/financial_pay.js',
+                ],
+                mergeFile: 'financial_pay',
+            },
+            payDetail: {
+                files: [
+                    '/public/js/decimal.min.js',
+                    '/public/js/math.min.js',
+                    '/public/js/component/menu.js',
+                    '/public/js/moment/moment.min.js',
+                    '/public/js/spreadjs/sheets/v11/gc.spread.sheets.all.11.2.2.min.js',
+                ],
+                mergeFiles: [
+                    '/public/js/sub_menu.js',
+                    '/public/js/div_resizer.js',
+                    '/public/js/zh_calc.js',
+                    '/public/js/spreadjs_rela/spreadjs_zh.js',
+                    '/public/js/shares/sjs_setting.js',
+                    '/public/js/shares/cs_tools.js',
+                    '/public/js/path_tree.js',
+                    '/public/js/financial_pay_detail.js',
+                ],
+                mergeFile: 'financial_pay_detail',
+            },
+        },
     },
 };
 

+ 1 - 0
package.json

@@ -11,6 +11,7 @@
         "atob": "^2.1.2",
         "axios": "^1.3.4",
         "bignumber.js": "^8.1.1",
+        "crypto-js": "^4.2.0",
         "decimal.js": "^10.2.0",
         "egg": "^1.13.0",
         "egg-etag": "^1.1.0",