MaiXinRong 3 месяцев назад
Родитель
Сommit
ea59c73689
100 измененных файлов с 6573 добавлено и 912 удалено
  1. 1 0
      app/const/audit.js
  2. 9 0
      app/const/project_setting.js
  3. 2 0
      app/const/shenpi.js
  4. 1 1
      app/const/sp_page_show.js
  5. 1 0
      app/const/tender_info.js
  6. 31 3
      app/controller/budget_controller.js
  7. 145 24
      app/controller/contract_controller.js
  8. 273 16
      app/controller/cost_controller.js
  9. 12 0
      app/controller/dashboard_controller.js
  10. 143 60
      app/controller/datacollect_controller.js
  11. 245 1
      app/controller/ledger_controller.js
  12. 1 1
      app/controller/profile_controller.js
  13. 15 0
      app/controller/report_controller.js
  14. 1 1
      app/controller/stage_controller.js
  15. 11 0
      app/controller/sub_proj_setting_controller.js
  16. 2 1
      app/controller/template_controller.js
  17. 67 1
      app/controller/tender_controller.js
  18. 2 4
      app/controller/wap_controller.js
  19. 4 0
      app/extend/context.js
  20. 144 0
      app/lib/ai_dify.js
  21. 0 53
      app/lib/ai_inspect.js
  22. 141 15
      app/lib/budget_final.js
  23. 3 2
      app/lib/ledger.js
  24. 12 0
      app/lib/rm/budget.js
  25. 4 0
      app/lib/rm/tender.js
  26. 12 0
      app/lib/rm/tender_budget.js
  27. 257 0
      app/lib/rptCustomData.js
  28. 25 3
      app/lib/ybp.js
  29. 157 0
      app/public/css/main.css
  30. BIN
      app/public/images/juecedaping07.png
  31. BIN
      app/public/images/juecedaping08.png
  32. 132 11
      app/public/js/budget_compare.js
  33. 10 0
      app/public/js/budget_detail.js
  34. 523 78
      app/public/js/contract_detail.js
  35. 340 0
      app/public/js/cost_compare.js
  36. 186 0
      app/public/js/cost_gather.js
  37. 177 34
      app/public/js/cost_stage_analysis.js
  38. 49 14
      app/public/js/cost_stage_book.js
  39. 550 0
      app/public/js/cost_stage_duty.js
  40. 56 15
      app/public/js/cost_stage_ledger.js
  41. 69 4
      app/public/js/ledger.js
  42. 4 0
      app/public/js/material.js
  43. 12 1
      app/public/js/path_tree.js
  44. 117 0
      app/public/js/shares/ai_trans.js
  45. 1 1
      app/public/js/shares/common_audit.js
  46. 38 2
      app/public/js/spreadjs_rela/spreadjs_zh.js
  47. 2 2
      app/public/js/stage.js
  48. 110 72
      app/public/report/js/rpt_main.js
  49. 3 1
      app/reports/rpt_component/jpc_flow_tab.js
  50. 28 1
      app/router.js
  51. 11 3
      app/service/budget.js
  52. 18 0
      app/service/budget_final_list.js
  53. 28 0
      app/service/budget_real.js
  54. 36 12
      app/service/contract.js
  55. 9 5
      app/service/contract_pay.js
  56. 270 170
      app/service/contract_sp_audit.js
  57. 10 7
      app/service/contract_supplement.js
  58. 12 4
      app/service/contract_tree.js
  59. 41 10
      app/service/cost_stage.js
  60. 229 173
      app/service/cost_stage_analysis.js
  61. 3 3
      app/service/cost_stage_analysis_detail.js
  62. 6 1
      app/service/cost_stage_audit.js
  63. 73 2
      app/service/cost_stage_book.js
  64. 9 3
      app/service/cost_stage_book_detail.js
  65. 13 9
      app/service/cost_stage_detail.js
  66. 403 0
      app/service/cost_stage_duty.js
  67. 59 13
      app/service/cost_stage_ledger.js
  68. 28 0
      app/service/ledger.js
  69. 117 8
      app/service/pos.js
  70. 1 1
      app/service/pos_calc_detail.js
  71. 19 0
      app/service/report.js
  72. 3 1
      app/service/shenpi_audit.js
  73. 5 0
      app/service/stage.js
  74. 23 1
      app/service/sub_project.js
  75. 96 0
      app/service/temp_file.js
  76. 1 0
      app/service/tender_permission.js
  77. 1 0
      app/view/budget/compare.ejs
  78. 54 2
      app/view/budget/compare_modal.ejs
  79. 29 1
      app/view/budget/detail_modal.ejs
  80. 10 2
      app/view/budget/sub_menu_list.ejs
  81. 23 12
      app/view/contract/detail.ejs
  82. 301 29
      app/view/contract/detail_modal.ejs
  83. 5 0
      app/view/cost/analysis.ejs
  84. 3 0
      app/view/cost/analysis_menu_list.ejs
  85. 18 1
      app/view/cost/analysis_modal.ejs
  86. 5 4
      app/view/cost/audit_btn.ejs
  87. 3 1
      app/view/cost/audit_modal.ejs
  88. 5 0
      app/view/cost/book.ejs
  89. 23 11
      app/view/cost/book_list.ejs
  90. 3 0
      app/view/cost/book_menu_list.ejs
  91. 2 1
      app/view/cost/book_modal.ejs
  92. 205 0
      app/view/cost/compare.ejs
  93. 1 0
      app/view/cost/compare_modal.ejs
  94. 66 0
      app/view/cost/cost_decimal.ejs
  95. 82 0
      app/view/cost/duty.ejs
  96. 2 0
      app/view/cost/duty_menu_list.ejs
  97. 2 0
      app/view/cost/duty_modal.ejs
  98. 33 0
      app/view/cost/gather.ejs
  99. 46 0
      app/view/cost/gather_modal.ejs
  100. 0 0
      app/view/cost/ledger.ejs

+ 1 - 0
app/const/audit.js

@@ -1656,6 +1656,7 @@ const pushType = {
     costStageLedger: 16,
     costStageBook: 17,
     costStageAnalysis: 18,
+    costStageDuty: 19,
 };
 
 module.exports = {

+ 9 - 0
app/const/project_setting.js

@@ -106,10 +106,19 @@ const daPing06Set = {
     cb_value: null,
     cb_show: [],
 };
+const daPing08Set = {
+    tenders: [
+        { id: 1, a: 0, b: 0 },
+        { id: 2, a: 0, b: 0 },
+        { id: 3, a: 0, b: 0 },
+        { id: 4, a: 0, b: 0 },
+    ],
+};
 module.exports = {
     noticeSetting: notice_setting,
     colSet,
     defaultProjectColSet,
     defaultColSet,
     daPing06Set,
+    daPing08Set,
 };

+ 2 - 0
app/const/shenpi.js

@@ -34,6 +34,7 @@ const cost_sp_type = {
     cost_stage_ledger: 13,
     cost_stage_book: 14,
     cost_stage_analysis: 15,
+    cost_stage_duty: 17,
 };
 // const sp_name = [];
 // sp_name[sp_type.advance] = '预付款审批';
@@ -59,6 +60,7 @@ const sp_lc = [
     { code: 'contract', type: sp_type.contract, name: '标段合同审批' },
 ];
 const cost_sp_lc = [
+    { code: 'cost_stage_duty', type: cost_sp_type.cost_stage_duty, name: '责任成本' },
     { code: 'cost_stage_ledger', type: cost_sp_type.cost_stage_ledger, name: '成本报审' },
     { code: 'cost_stage_book', type: cost_sp_type.cost_stage_book, name: '财务账面' },
     { code: 'cost_stage_analysis', type: cost_sp_type.cost_stage_analysis, name: '成本分析' },

+ 1 - 1
app/const/sp_page_show.js

@@ -88,7 +88,7 @@ const defaultSetting = {
     isOnlyChecked: 1,
     closeArchiveSignature: 0,
     openSettle: 0,
-    openContractExpr: 0,
+    openContractExpr: 1,
     openMultiStageCalc: 0,
     maxMultiStageCount: 5,
     openDataCollect: 1,

+ 1 - 0
app/const/tender_info.js

@@ -200,6 +200,7 @@ const defaultInfo = {
         cost_stage_ledger: 1,
         cost_stage_book: 1,
         cost_stage_analysis: 1,
+        cost_stage_duty: 1,
         contract: 1,
     },
     ledger_check: {

+ 31 - 3
app/controller/budget_controller.js

@@ -22,6 +22,7 @@ const ValidDskProject = {
     yu: [1, 18],
     zb: [4, 18, 19],
     ctrl: [4, 18, 19],
+    real: [4, 18, 19],
 };
 const DSK = require('../lib/dsk');
 const sendToWormhole = require('stream-wormhole');
@@ -52,12 +53,12 @@ module.exports = app => {
                     bl.yu_tp = await ctx.service.budgetYu.getSumTp(bl.budget_id);
                     bl.zb_tp = await ctx.service.budgetZb.getSumTp(bl.budget_id);
                     bl.ctrl_tp = await ctx.service.budgetCtrl.getSumTp(bl.budget_id);
+                    bl.real_tp = await ctx.service.budgetReal.getSumTp(bl.budget_id);
                 }
                 renderData.tenderList = await ctx.service.tender.getList4Select('stage');
                 renderData.categoryData = await this.ctx.service.category.getAllCategory(ctx.subProject);
                 await this.layout('budget/list.ejs', renderData, 'budget/list_modal.ejs');
             } catch (err) {
-                console.log(err);
                 ctx.log(err);
                 ctx.session.postError = err.toString();
                 ctx.redirect(this.menu.menu.dashboard.url);
@@ -92,6 +93,12 @@ module.exports = app => {
             }
         }
 
+        async _getContractTree(condition) {
+            const data = await this.ctx.service.contractTree.getAllDataByCondition({ where: condition });
+            const contract_data = await this.ctx.service.contract.getAllDataByCondition({ where: condition });
+            return [...data, ...contract_data];
+        }
+
         async compare(ctx) {
             try {
                 const renderData = {
@@ -106,6 +113,7 @@ module.exports = app => {
                     return { id: y.id, name: y.name, lastStageOrder: y.lastStage.order, lastStageStatus: auditConst.stage.statusString[y.lastStage.status], category: y.category };
                 });
                 renderData.categoryData = await this.ctx.service.category.getAllCategory(ctx.subProject);
+                renderData.contract = await this._getContractTree({ spid: this.ctx.subProject.id, contract_type: 1 });
                 await this.layout('budget/compare.ejs', renderData, 'budget/compare_modal.ejs');
             } catch (err) {
                 ctx.log(err);
@@ -118,12 +126,14 @@ module.exports = app => {
                 if (ctx.budget.final_id) {
                     data.final = await ctx.service.budgetFinal.getAllDataByCondition({ where: { final_id: ctx.budget.final_id } });
                     data.finalInfo = await ctx.service.budgetFinalList.getFinal(ctx.budget.final_id);
+                    await ctx.service.budgetFinalList.clearExpiredFinal(ctx.budget);
                 } else {
                     data.gu = await ctx.service.budgetGu.getData(ctx.budget.id);
                     data.gai = await ctx.service.budgetGai.getData(ctx.budget.id);
                     data.yu = await ctx.service.budgetYu.getData(ctx.budget.id);
                     data.zb = await ctx.service.budgetZb.getData(ctx.budget.id);
                     data.ctrl = await ctx.service.budgetCtrl.getData(ctx.budget.id);
+                    data.real = await ctx.service.budgetReal.getData(ctx.budget.id);
                 }
                 ctx.body = { err: 0, msg: '', data };
             } catch (err) {
@@ -144,7 +154,7 @@ module.exports = app => {
                     ctx.body = { err: 0, msg: `决算数据已在${ctx.moment(finalInfo.update_time).format('YYYY-DD-MM HH:mm:ss')}更新,请先查看后再决定是否生成`, data: { final, finalInfo } };
                 } else {
                     const finalInfo = await ctx.service.budgetFinalList.addFinal(ctx.budget, data.id);
-                    const final = await ctx.service.budget.doFinal(ctx.budget, finalInfo);
+                    const final = await ctx.service.budget.doFinal(ctx.budget, finalInfo, data.stage, data.contracts);
                     ctx.body = { err: 0, msg: '', data: { final, finalInfo } };
                 }
             } catch (err) {
@@ -191,13 +201,14 @@ module.exports = app => {
                 case 'yu': return this.ctx.service.budgetYu;
                 case 'zb': return this.ctx.service.budgetZb;
                 case 'ctrl': return this.ctx.service.budgetCtrl;
+                case 'real': return this.ctx.service.budgetReal;
                 default: return null;
             }
         }
         async _getNeedGcl() {
             if (!this.ctx.params.btype) throw '参数错误';
             const funRela = this.ctx.subProject.fun_rela;
-            return ['yu', 'zb', 'ctrl'].indexOf(this.ctx.params.btype) >= 0 && !!funRela.needGcl;
+            return ['yu', 'zb', 'ctrl', 'real'].indexOf(this.ctx.params.btype) >= 0 && !!funRela.needGcl;
         }
 
         async detail(ctx) {
@@ -208,6 +219,8 @@ module.exports = app => {
                     jsFiles: this.app.jsFiles.common.concat(this.app.jsFiles.budget.detail),
                     needGcl,
                     validDskProjType: ValidDskProject[ctx.params.btype],
+                    optimize_rate: this.ctx.budget[ctx.params.btype + '_optimize_rate'] || 0,
+                    profit: this.ctx.budget[ctx.params.btype + '_profit'] || 0,
                 };
                 [renderData.stdBills, renderData.stdChapters] = await ctx.service.budgetStd.getStdList(ctx.budget.std_id, ctx.params.btype);
                 await this.layout('budget/detail.ejs', renderData, 'budget/detail_modal.ejs');
@@ -429,6 +442,21 @@ module.exports = app => {
                 this.ajaxErrorBody(err, '保存小数位数失败');
             }
         }
+        async extraInfo(ctx) {
+            try {
+                if (!ctx.budget) throw '项目数据错误';
+                if (ctx.budget.readOnly) throw '你无权修改附加属性';
+
+                const data = JSON.parse(ctx.request.body.data);
+                if (!data || !data.btype) throw '缺少必要参数';
+
+                const refreshData = await ctx.service.budget.saveExtraInfo(data, data.btype);
+                ctx.body = { err: 0, msg: '', data: refreshData };
+            } catch (err) {
+                this.log(err);
+                this.ajaxErrorBody(err, '保存附加属性失败');
+            }
+        }
     }
 
     return BudgetController;

+ 145 - 24
app/controller/contract_controller.js

@@ -11,6 +11,7 @@ const moment = require('moment');
 const sendToWormhole = require('stream-wormhole');
 const fs = require('fs');
 const path = require('path');
+const AiDify = require('../lib/ai_dify');
 module.exports = app => {
 
     class ContractController extends app.BaseController {
@@ -406,26 +407,36 @@ module.exports = app => {
         async checkAudit(ctx) {
             try {
                 const data = JSON.parse(ctx.request.body.data);
-                if (!data.postType || !data.id) throw '数据错误';
+                if (!data.postType || !data.id || !data.type) throw '数据错误';
                 const responseData = {
                     err: 0, msg: '', data: {},
                 };
-                const contract = isNaN(data.id) ? await ctx.service.contract.getDataById(data.id) : await ctx.service.contractPay.getDataById(data.id);
+                const contract = await this._getContractByType(ctx, data);
                 if (!contract) {
                     throw '未找到合同相关数据';
                 }
-                const tip = isNaN(data.id) ? '' : contractConst.typeName[contract.contract_type];
+                let tip = '';
                 // 判断功能是否已关闭
                 const shenpi_status = ctx.contract_tender ? ctx.subProject.page_show.openContractTenderShenpi : ctx.subProject.page_show.openContractSubProjectShenpi;
                 const pay_shenpi_status = ctx.contract_tender ? ctx.subProject.page_show.openContractPayTenderShenpi : ctx.subProject.page_show.openContractPaySubProjectShenpi;
-                if (isNaN(data.id)) {
-                    if (!shenpi_status) {
-                        throw '合同审批功能已关闭, 无法进行审批操作, 请刷新页面';
-                    }
-                } else {
-                    if (!pay_shenpi_status) {
-                        throw '合同' + tip + '审批功能已关闭, 无法进行审批操作, 请刷新页面';
-                    }
+                switch (data.type) {
+                    case 'contract':
+                        if (!shenpi_status) {
+                            throw '合同审批功能已关闭, 无法进行审批操作, 请刷新页面';
+                        }
+                        break;
+                    case 'pay':
+                        if (!pay_shenpi_status) {
+                            throw '合同' + tip + '审批功能已关闭, 无法进行审批操作, 请刷新页面';
+                        }
+                        tip = contractConst.typeName[contract.contract_type];
+                        break;
+                    case 'supplement':
+                        if (!shenpi_status) {
+                            throw '合同审批功能已关闭, 无法进行审批操作, 请刷新页面';
+                        }
+                        break;
+                    default: throw '数据错误';
                 }
                 let shenpi = '';
                 if (ctx.contract_tender) {
@@ -434,16 +445,14 @@ module.exports = app => {
                 } else {
                     shenpi = ctx.subProject.shenpi;
                 }
-                await ctx.service.contractSpAudit.loadUser(contract);
-                await ctx.service.contractSpAudit.loadAuditViewData(contract);
-                const cid = contract.cid ? contract.cid : contract.id;
-                const cpid = contract.cid ? contract.id : null;
+                await ctx.service.contractSpAudit.loadUser(contract, data.type);
+                await ctx.service.contractSpAudit.loadAuditViewData(contract, data.type);
                 switch (data.postType) {
                     case 'start':
                         if (contract.status !== auditConst.contract.status.uncheck && contract.status !== auditConst.contract.status.checkNo) {
                             throw '该合同' + tip + '当前无法上报';
                         }
-                        await ctx.service.contractSpAudit.start(ctx.contractOptions, shenpi, cid, cpid, contract.times);
+                        await ctx.service.contractSpAudit.start(ctx.contractOptions, shenpi, contract, data.type, contract.times);
                         break;
                     case 'check':
                         if (contract.status !== auditConst.contract.status.checking && contract.status !== auditConst.contract.status.checkNoPre) {
@@ -459,7 +468,7 @@ module.exports = app => {
                         if (!checkData.checkType || isNaN(checkData.checkType)) {
                             throw '提交数据错误';
                         }
-                        await ctx.service.contractSpAudit.check(contract, checkData);
+                        await ctx.service.contractSpAudit.check(contract, data.type, checkData);
                         break;
                     case 'add-audit':
                         if (shenpi.contract === shenpiConst.sp_status.gdspl) {
@@ -477,7 +486,7 @@ module.exports = app => {
                             throw '当前不允许添加审核人';
                         }
 
-                        const auditorList = await ctx.service.contractSpAudit.getAuditors(cid, cpid, contract.times, 'asc', true);
+                        const auditorList = await ctx.service.contractSpAudit.getAuditors(contract, data.type, contract.times, 'asc', true);
                         // 检查审核人是否已存在
                         const exist = this.app._.find(auditorList, { aid: add_id });
                         if (exist) {
@@ -486,7 +495,7 @@ module.exports = app => {
                         const shenpiCondition = ctx.helper._.assign({ sp_type: shenpiConst.sp_type.contract, sp_status: shenpiConst.sp_status.gdzs }, ctx.contractOptions);
                         const shenpiInfo = await ctx.service.shenpiAudit.getDataByCondition(shenpiCondition);
                         const is_gdzs = shenpiInfo && shenpi.contract === shenpiConst.sp_status.gdzs ? 1 : 0;
-                        const add_result = await ctx.service.contractSpAudit.addAuditor(ctx.contractOptions, cid, cpid, add_id, contract.times, is_gdzs);
+                        const add_result = await ctx.service.contractSpAudit.addAuditor(ctx.contractOptions, contract, data.type, add_id, contract.times, is_gdzs);
                         if (!add_result) {
                             throw '添加审核人失败';
                         }
@@ -507,7 +516,7 @@ module.exports = app => {
                             throw '当前不允许移除审核人';
                         }
 
-                        const del_result = await ctx.service.contractSpAudit.deleteAuditor(cid, cpid, del_id, contract.times);
+                        const del_result = await ctx.service.contractSpAudit.deleteAuditor(contract, data.type, del_id, contract.times);
                         if (!del_result) {
                             throw '移除审核人失败';
                         }
@@ -519,7 +528,7 @@ module.exports = app => {
                         if (contract.status !== auditConst.contract.status.uncheck && contract.status !== auditConst.contract.status.checkNo) {
                             throw '当前状态不允许切换审批组';
                         }
-                        const chagne_result = await ctx.service.contractSpAudit.changeSpGroup(contract, data.sp_group);
+                        const chagne_result = await ctx.service.contractSpAudit.changeSpGroup(contract, data.type, data.sp_group);
                         if (!chagne_result) {
                             throw '切换审批组失败';
                         }
@@ -527,9 +536,9 @@ module.exports = app => {
                     default:
                         throw '参数错误';
                 }
-                const newContract = isNaN(data.id) ? await ctx.service.contract.getDataById(data.id) : await ctx.service.contractPay.getDataById(data.id);
-                await ctx.service.contractSpAudit.loadUser(newContract);
-                await ctx.service.contractSpAudit.loadAuditViewData(newContract);
+                const newContract = await this._getContractByType(ctx, data);
+                await ctx.service.contractSpAudit.loadUser(newContract, data.type);
+                await ctx.service.contractSpAudit.loadAuditViewData(newContract, data.type);
                 responseData.data = newContract;
                 ctx.body = responseData;
             } catch (err) {
@@ -539,6 +548,23 @@ module.exports = app => {
             }
         }
 
+        async _getContractByType(ctx, data) {
+            let contract = null;
+            switch (data.type) {
+                case 'contract':
+                    contract = await ctx.service.contract.getDataById(data.id);
+                    break;
+                case 'pay':
+                    contract = await ctx.service.contractPay.getDataById(data.id);
+                    break;
+                case 'supplement':
+                    contract = await ctx.service.contractSupplement.getDataById(data.id);
+                    break;
+                default: throw '数据错误';
+            }
+            return contract;
+        }
+
         async updateBills(ctx) {
             try {
                 const data = JSON.parse(ctx.request.body.data);
@@ -996,6 +1022,101 @@ module.exports = app => {
                 ctx.redirect(`/sp/${ctx.subProject.id}/dashboard`);
             }
         }
+
+        async editAndAi(ctx) {
+            try {
+                const data = JSON.parse(ctx.request.body.data);
+                if (!data.id || !data.fid) throw '数据错误';
+                const contract = await ctx.service.contract.getDataById(data.id);
+                if (!contract) {
+                    throw '未找到合同相关数据';
+                }
+                const att = await ctx.service.contractAtt.getDataById(data.fid);
+                if (!att) {
+                    throw '未找到合同文件';
+                }
+                att.viewpath = ctx.helper.getPreviewPath(att.fileext, ctx.app.config.fujianOssPath + att.filepath);
+                const aiDify = new AiDify({ apiKey: ctx.difyInfo.apiKey.contract });
+                const transResult = await aiDify.contractTrans(att);
+                transResult.res = JSON.parse(transResult.res);
+                const responseData = {
+                    ...transResult,
+                    cid: contract.id,
+                    attached: false,
+                };
+                ctx.body = { err: 0, msg: '', data: responseData };
+            } catch (err) {
+                console.log(err);
+                this.log(err);
+                ctx.body = { err: 1, msg: err.toString(), data: null };
+            }
+        }
+
+        async uploadAndAi(ctx) {
+            const stream = await ctx.getFileStream();
+            try {
+                const hasExistingCid = !!stream.fields.cid;
+                const cid = stream.fields.cid || this.ctx.app.uuid.v4();
+                if (!cid) throw '参数有误';
+
+                const attachToContract = stream.fields.attachToContract === 'true' || stream.fields.attachToContract === '1';
+                const create_time = Date.parse(new Date()) / 1000;
+                const parsedPath = path.parse(stream.filename);
+                const headpath = ctx.contractOptions.spid
+                    ? `sp/contract/${ctx.contractOptions.spid}`
+                    : `app/public/upload/${ctx.contractOptions.tid}`;
+                const filepath = `${headpath}/contract/${cid}/fujian_${create_time}${parsedPath.ext}`;
+
+                await ctx.app.fujianOss.put(ctx.app.config.fujianOssFolder + filepath, stream);
+                const fileInfo = { filepath, name: stream.filename, ext: parsedPath.ext };
+                if (stream) await sendToWormhole(stream);
+
+                if (!fileInfo) {
+                    throw '请上传文件';
+                }
+
+                const newFile = {
+                    spid: ctx.contractOptions.spid || null,
+                    tid: ctx.contractOptions.tid || null,
+                    contract_type: ctx.contract_type,
+                    cid,
+                    uid: ctx.session.sessionUser.accountId,
+                    filename: fileInfo.name,
+                    fileext: fileInfo.ext,
+                    filesize: stream.fields.size,
+                    filepath: fileInfo.filepath,
+                    upload_time: new Date(),
+                };
+
+                // 写入数据库(只有这一条记录)
+                if (!hasExistingCid || attachToContract) {
+                    const fileResult = await ctx.service.contractAtt.saveFileMsgToDb([newFile]);
+                    newFile.id = fileResult.insertId;
+                    newFile.username = ctx.session.sessionUser.name;
+                }
+
+                // 重新查出该cid下的附件,按filepath精确匹配出刚上传的这一份(避免拿到该cid下其他历史文件)
+                newFile.viewpath = ctx.helper.getPreviewPath(newFile.fileext, ctx.app.config.fujianOssPath + newFile.filepath);
+                const aiDify = new AiDify({ apiKey: ctx.difyInfo.apiKey.contract });
+                const transResult = await aiDify.contractTrans(newFile);
+                transResult.res = JSON.parse(transResult.res);
+                // 如果不勾选"附加到合同"并且没有cid,则要删除oss文件
+                if (!attachToContract && hasExistingCid) {
+                    await ctx.app.fujianOss.delete(ctx.app.config.fujianOssFolder + newFile.filepath);
+                }
+                const responseData = {
+                    ...transResult,
+                    cid,
+                    attached: attachToContract,
+                };
+                if ((!hasExistingCid || attachToContract) && newFile.id) responseData.file = newFile;
+                ctx.body = { err: 0, msg: '', data: responseData };
+            } catch (err) {
+                stream && (await sendToWormhole(stream));
+                this.log(err);
+                ctx.body = { err: 1, msg: err.toString(), data: null };
+            }
+        }
     }
 
     return ContractController;

+ 273 - 16
app/controller/cost_controller.js

@@ -87,8 +87,12 @@ module.exports = app => {
             }
         }
 
-        async getTypeStages(ctx, stage_type) {
-            const stages = await this.ctx.service.costStage.getAllStages(ctx.tender.id, stage_type, 'DESC');
+        async checkDutyRela(ctx) {
+            ctx.tender.costDuty = await ctx.service.costStage.getStageByOrder(ctx.tender.id, 'duty', 1);
+        }
+
+        async getTypeStages(ctx, stage_type, sort = 'DESC') {
+            const stages = await this.ctx.service.costStage.getAllStages(ctx.tender.id, stage_type, sort);
             for (const s of stages) {
                 if (s.audit_status !== audit.common.status.checked) {
                     await this.ctx.service.costStage.loadUser(s);
@@ -107,9 +111,12 @@ module.exports = app => {
         }
         async ledger(ctx) {
             try {
+                await this.checkDutyRela(ctx);
                 const stage_type = this.ctx.service.costStage.stageType.ledger.key;
                 const stages = await this.getTypeStages(ctx, stage_type);
+                const colSet = this.ctx.service.subProject.getColSet('cost_' + stage_type);
                 const renderData = {
+                    colSet,
                     stage_type,
                     auditType: audit.auditType,
                     stages,
@@ -124,8 +131,12 @@ module.exports = app => {
         }
         async book(ctx) {
             try {
+                await this.checkDutyRela(ctx);
                 const stage_type = this.ctx.service.costStage.stageType.book.key;
                 const stages = await this.getTypeStages(ctx, stage_type);
+                for (const s of stages) {
+                    s.relaStage = await this.ctx.service.costStage.getStage(s.rela_stage.sid);
+                }
                 const validLedgerStages = await this.ctx.service.costStage.getAllCheckedStages(ctx.tender.id, 'ledger', 'DESC');
                 for (const vls of validLedgerStages) {
                     vls.beenRela = stages.findIndex(x => { return x.rela_stage.sid === vls.id; }) >= 0;
@@ -147,6 +158,7 @@ module.exports = app => {
         }
         async analysis(ctx) {
             try {
+                await this.checkDutyRela(ctx);
                 const stage_type = this.ctx.service.costStage.stageType.analysis.key;
                 const stages = await this.getTypeStages(ctx, stage_type);
                 // todo 兼容以ledger做关联的情况,届时只需在标段或者项目上设置这个类型即可。
@@ -175,14 +187,17 @@ module.exports = app => {
 
         async addStage(ctx) {
             const stage_type = ctx.request.body.stage_type;
+            const hintType  = stage_type === 'duty' ? '数据' : '期'
             try {
-                if (!ctx.permission.cost[stage_type + '_add']) throw '您无权创建期';
+                if (!ctx.permission.cost[stage_type + '_add']) throw '您无权创建' + hintType;
                 const stage_date = ctx.request.body.date;
                 if (stage_type === 'ledger' && !stage_date) throw '请选择年月';
 
-                const stages = await ctx.service.costStage.getAllStages(ctx.tender.id, stage_type, 'DESC');
-                const unCompleteCount = stages.filter(s => { return s.status !== audit.common.status.checked; }).length;
-                if (unCompleteCount.length > 0) throw '最新一期未审批通过,请审批通过后再新增';
+                if (stage_type !== 'duty') {
+                    const stages = await ctx.service.costStage.getAllStages(ctx.tender.id, stage_type, 'DESC');
+                    const unCompleteCount = stages.filter(s => { return s.status !== audit.common.status.checked; }).length;
+                    if (unCompleteCount.length > 0) throw '最新一期未审批通过,请审批通过后再新增';
+                }
 
                 let newStage;
                 if (stage_type === 'ledger') {
@@ -202,14 +217,22 @@ module.exports = app => {
                     if (!relaStage) throw '选择的关联期不存在';
                     newStage = await ctx.service.costStage.add(ctx.tender.id, stage_type, relaStage.stage_date, { sid: relaStage.id, sorder: relaStage.stage_order });
                     await this.ctx.service.costStageAnalysis.reCalcProfit(newStage.id);
+                } else if (stage_type === 'duty') {
+                    newStage = await ctx.service.costStage.getStageByOrder(ctx.tender.id, stage_type, 1);
+                    if (newStage) throw '已新增责任成本,请勿重复新增';
+                    newStage = await ctx.service.costStage.add(ctx.tender.id, stage_type, '');
                 }
 
-                if (!newStage) throw '新增期失败';
+                if (!newStage) throw `新增${hintType}失败`;
                 ctx.redirect(`/sp/${ctx.subProject.id}/cost/tender/${ctx.tender.id}/${stage_type}/${newStage.stage_order}/stage`);
             } catch (err) {
                 ctx.log(err);
-                ctx.postError(err, '新增期失败');
-                ctx.redirect(`/sp/${ctx.subProject.id}/cost/tender/${ctx.tender.id}/${stage_type}`);
+                ctx.postError(err, `新增${hintType}失败`);
+                if (stage_type === 'duty') {
+                    ctx.redirect(`/sp/${ctx.subProject.id}/cost/tender/${ctx.tender.id}/ledger`)
+                } else {
+                    ctx.redirect(`/sp/${ctx.subProject.id}/cost/tender/${ctx.tender.id}/${stage_type}`);
+                }
             }
         }
         async delStage(ctx) {
@@ -247,6 +270,7 @@ module.exports = app => {
         async stage(ctx) {
             const stageTypeInfo = ctx.service.costStage.stageType[ctx.costStage.stage_type];
             try {
+                await this.checkDutyRela(ctx);
                 await this._getStageAuditViewData(ctx);
                 // 流程审批人相关数据
                 const accountList = await ctx.service.projectAccount.getAllSubProjectAccount(ctx.subProject);
@@ -278,6 +302,7 @@ module.exports = app => {
                     });
                     payStages = stages.map(x => { return { order: x.order, date: x.s_time }; });
                 }
+                const colSet = this.ctx.service.subProject.getColSet('cost_' + ctx.costStage.stage_type);
                 const renderData = {
                     auditConst: audit.common,
                     auditType: audit.auditType,
@@ -290,6 +315,7 @@ module.exports = app => {
                     shenpi_status: ctx.tender.info.shenpi[stageTypeInfo.shenpi_status] || 1,
                     contractTypes,
                     contractUsed,
+                    colSet,
                 };
                 await this.layout(`cost/${stageTypeInfo.key}.ejs`, renderData, `cost/${stageTypeInfo.key}_modal.ejs`);
             } catch (err) {
@@ -320,7 +346,6 @@ module.exports = app => {
                                 : await ctx.service.costStageDetail.getEditData(ctx.costStage);
                             break;
                         case 'detailCompare':
-                            // todo
                             responseData.data.detailCompare = await ctx.service.costStageDetail.getCompareData(ctx.costStage);
                             break;
                         case 'auditFlow':
@@ -379,7 +404,7 @@ module.exports = app => {
                             ledgerC.forEach(l => { delete l.postil; delete l.memo; });
                             const bookC = await ctx.service.costStageBook.getCompareData(ctx.costStage);
                             ctx.helper.assignRelaData(ledgerC, [
-                                { data: bookC, fields: ['pre_in_tp', 'pre_in_excl_tax_tp', 'calc_his', 'postil', 'memo'], prefix: '', relaId: 'ledger_id' },
+                                { data: bookC, fields: ['pre_in_tp', 'pre_in_excl_tax_tp', 'in_tp', 'in_excl_tax_tp', 'calc_his', 'postil', 'memo'], prefix: '', relaId: 'ledger_id' },
                             ]);
                             responseData.data[f] = ledgerC;
                             break;
@@ -400,7 +425,7 @@ module.exports = app => {
                             detailC.forEach(l => { delete l.postil; delete l.memo; });
                             const bookDetailC = await ctx.service.costStageBook.getCompareData(ctx.costStage);
                             ctx.helper.assignRelaData(detailC, [
-                                { data: bookDetailC, fields: ['calc_his', 'postil', 'memo'], prefix: '', relaId: 'detail_id' },
+                                { data: bookDetailC, fields: ['in_tp', 'in_excl_tax_tp', 'calc_his', 'postil', 'memo'], prefix: '', relaId: 'detail_id' },
                             ]);
                             responseData.data[f] = detailC;
                             break;
@@ -443,12 +468,55 @@ module.exports = app => {
                                 : await ctx.service.costStageAnalysisDetail.getEditData(ctx.costStage);
                             break;
                         case 'detailCompare':
-                            // todo
                             responseData.data.detailCompare = await ctx.service.costStageAnalysisDetail.getCompareData(ctx.costStage);
                             break;
                         case 'auditFlow':
                             responseData.data[f] = await ctx.service.costStageAudit.getViewFlow(ctx.costStage);
                             break;
+                        case 'att':
+                            responseData.data[f] = await ctx.service.costStageFile.getData(ctx.costStage.id, 'DESC');
+                            break;
+                        case 'relaStageBills':
+                            const relaStage = ctx.costStage.relaStage;
+                            if (relaStage.stage_type === 'book') relaStage.relaStage = await this.ctx.service.costStage.getStage(relaStage.rela_stage.sid);
+                            responseData.data[f] = relaStage.relaStage
+                                ? await this.ctx.service.costStageLedger.getReadData(relaStage.relaStage)
+                                : await this.ctx.service.costStageLedger.getReadData(relaStage);
+                            const detail = relaStage.relaStage
+                                ? await this.ctx.service.costStageDetail.getReadData(relaStage.relaStage)
+                                : await this.ctx.service.costStageDetail.getReadData(relaStage);
+                            responseData.data[f].forEach(x => {
+                                const relaDetail = detail.filter(d => { return d.ledger_id === x.id; });
+                                x.detail_type = relaDetail.length === 0 ? '' : (relaDetail[0].is_deal ? '合同' : '一般');
+                            });
+                            break;
+                        default:
+                            responseData.data[f] = [];
+                            break;
+                    }
+                }
+
+                ctx.body = responseData;
+            } catch (err) {
+                this.log(err);
+                ctx.body = { err: 1, msg: err.toString(), data: null };
+            }
+        }
+        async _dutyLoad(ctx) {
+            try {
+                const data = JSON.parse(ctx.request.body.data);
+                const filter = data.filter.split(';');
+                const responseData = { err: 0, msg: '', data: {}, hpack: [] };
+                for (const f of filter) {
+                    switch (f) {
+                        case 'bills':
+                            responseData.data.bills = ctx.costStage.readOnly && !ctx.costStage.canCheck
+                                ? await ctx.service.costStageDuty.getReadData(ctx.costStage)
+                                : await ctx.service.costStageDuty.getEditData(ctx.costStage);
+                            break;
+                        case 'extra':
+                            responseData.data.extra = ctx.costStage.extra_info;
+                            break;
                         default:
                             responseData.data[f] = [];
                             break;
@@ -470,6 +538,58 @@ module.exports = app => {
             }
         }
 
+        async _dutyBillsBase(stage, type, data) {
+            if (isNaN(data.id) || data.id <= 0) throw '数据错误';
+            if (type !== 'add') {
+                if (isNaN(data.count) || data.count <= 0) data.count = 1;
+            }
+            switch (type) {
+                case 'add':
+                    return await this.ctx.service.costStageDuty.addDutyNode(stage, data.id, data.count);
+                case 'delete':
+                    return await this.ctx.service.costStageDuty.delete(stage.id, data.id, data.count);
+                case 'up-move':
+                    return await this.ctx.service.costStageDuty.upMoveNode(stage.id, data.id, data.count);
+                case 'down-move':
+                    return await this.ctx.service.costStageDuty.downMoveNode(stage.id, data.id, data.count);
+                case 'up-level':
+                    return await this.ctx.service.costStageDuty.upLevelNode(stage.id, data.id, data.count);
+                case 'down-level':
+                    return await this.ctx.service.costStageDuty.downLevelNode(stage.id, data.id, data.count);
+            }
+        }
+        async _dutyUpdate(ctx) {
+            try {
+                const data = JSON.parse(ctx.request.body.data);
+                if (!data.target) throw '数据错误';
+                const responseData = { err: 0, msg: '', data: {} };
+
+                if (data.target === 'ledger') {
+                    if (!data.postType || !data.postData) throw '数据错误';
+                    switch (data.postType) {
+                        case 'add':
+                        case 'delete':
+                        case 'up-move':
+                        case 'down-move':
+                        case 'up-level':
+                        case 'down-level':
+                            responseData.data = await this._dutyBillsBase(ctx.costStage, data.postType, data.postData);
+                            break;
+                        case 'update':
+                            responseData.data = await this.ctx.service.costStageDuty.updateCalc(ctx.costStage, data.postData);
+                            break;
+                        default:
+                            throw '未知操作';
+                    }
+                } else if (data.target === 'extra') {
+                    responseData.data = await this.ctx.service.costStage.saveExtraInfo(ctx.costStage, data.data);
+                }
+                ctx.body = responseData;
+            } catch (err) {
+                this.log(err);
+                ctx.body = this.ajaxErrorBody(err, '数据错误');
+            }
+        }
         async _billsBase(stage, type, data) {
             if (isNaN(data.id) || data.id <= 0) throw '数据错误';
             if (type !== 'add') {
@@ -547,14 +667,18 @@ module.exports = app => {
                 ctx.body = this.ajaxErrorBody(err, '数据错误');
             }
         }
-        async _analysisBillsBase(stage, type, data) {
+        async _analysisBillsBase(stage, type, data, addType) {
             if (isNaN(data.id) || data.id <= 0) throw '数据错误';
             if (type !== 'add') {
                 if (isNaN(data.count) || data.count <= 0) data.count = 1;
             }
             switch (type) {
                 case 'add':
-                    return await this.ctx.service.costStageAnalysis.addAnalysisNode(stage, data.id, data.count);
+                    if (addType === 'child') {
+                        return await this.ctx.service.costStageAnalysis.addAnalysisChild(stage, data.id, data.count);
+                    } else {
+                        return await this.ctx.service.costStageAnalysis.addAnalysisNode(stage, data.id, data.count);
+                    }
                 case 'delete':
                     return await this.ctx.service.costStageAnalysis.delete(stage.id, data.id, data.count);
                 case 'up-move':
@@ -582,7 +706,7 @@ module.exports = app => {
                         case 'down-move':
                         case 'up-level':
                         case 'down-level':
-                            responseData.data = await this._analysisBillsBase(ctx.costStage, data.postType, data.postData);
+                            responseData.data = await this._analysisBillsBase(ctx.costStage, data.postType, data.postData, data.addType);
                             break;
                         case 'update':
                             responseData.data = await this.ctx.service.costStageAnalysis.updateCalc(ctx.costStage, data.postData);
@@ -594,6 +718,8 @@ module.exports = app => {
                     responseData.data = await this.ctx.service.costStageAnalysisDetail.updateDatas(data.update);
                 } else if (data.target === 'importPay') {
                     responseData.data = await this.ctx.service.costStageAnalysis.importPay(ctx.costStage, data.pay_order, data.pay_tax);
+                } else if (data.target === 'importCost') {
+                    responseData.data = await this.ctx.service.costStageAnalysis.importCost(ctx.costStage, data.select_id, data.cost_id);
                 }
                 ctx.body = responseData;
             } catch (err) {
@@ -609,6 +735,17 @@ module.exports = app => {
                 ctx.ajaxErrorBody('未知期数据类型', '保存数据错误');
             }
         }
+        async setDecimal(ctx) {
+            try {
+                const data = JSON.parse(ctx.request.body.data);
+                const stageTypeInfo = ctx.service.costStage.stageType[ctx.costStage.stage_type];
+                const result = await this.ctx.service[stageTypeInfo.dataService].setDecimal(data.decimal);
+                ctx.body = { err: 0, msg: '', data: result };
+            } catch (err) {
+                this.log(err);
+                ctx.body = this.ajaxErrorBody(err, '设置小数位数错误');
+            }
+        }
 
         async uploadStageFile(ctx) {
             let stream;
@@ -788,6 +925,7 @@ module.exports = app => {
                     const code = ctx.request.body.code;
                     const cacheKey = 'smsCode:' + ctx.session.sessionUser.accountId;
                     const cacheCode = await app.redis.get(cacheKey);
+                    const pa = await ctx.service.projectAccount.getDataById(ctx.session.sessionUser.accountId);
                     if (cacheCode === null || code === undefined || cacheCode !== (code + pa.auth_mobile)) {
                         throw '验证码不正确!';
                     }
@@ -833,6 +971,125 @@ module.exports = app => {
                 ctx.body = { err: 1, msg: error.toString(), data: null };
             }
         }
+
+        async colSet(ctx) {
+            const stage_type = ctx.request.body.stage_type;
+            try {
+                const colSet = {};
+                for (const prop in ctx.request.body) {
+                    if (prop.indexOf('col_') === 0) colSet[prop.replace('col_', '')] = ctx.request.body[prop] === '1' ? 1 : 0;
+                }
+                await this.ctx.service.subProject.updateColSet('cost_' + stage_type, colSet);
+                ctx.redirect(ctx.request.header.referer);
+            } catch (err) {
+                ctx.log(err);
+                ctx.postError(err, '保存列设置失败');
+                ctx.redirect(ctx.request.header.referer);
+            }
+        }
+
+        async compare(ctx) {
+            const stageTypeInfo = ctx.service.costStage.stageType[ctx.costStage.stage_type];
+            try {
+                await this.checkDutyRela(ctx);
+                await this._getStageAuditViewData(ctx);
+                // 流程审批人相关数据
+                const accountList = await ctx.service.projectAccount.getAllSubProjectAccount(ctx.subProject);
+                const unitList = await ctx.service.constructionUnit.getAllDataByCondition({ where: { pid: ctx.session.sessionProject.id } });
+                const accountGroup = unitList.map(item => {
+                    const groupList = accountList.filter(item1 => item1.company === item.name);
+                    return { groupName: item.name, groupList };
+                }).filter(x => { return x.groupList.length > 0; });
+                // 是否已验证手机短信
+                const pa = await ctx.service.projectAccount.getDataById(ctx.session.sessionUser.accountId);
+                const calcFields = ctx.service[stageTypeInfo.dataService].calcFields;
+
+                const renderData = {
+                    auditConst: audit.common,
+                    auditType: audit.auditType,
+                    accountList,
+                    accountGroup,
+                    shenpiConst,
+                    authMobile: pa.auth_mobile,
+                    jsFiles: this.app.jsFiles.common.concat(this.app.jsFiles.cost.cost_compare),
+                    shenpi_status: ctx.tender.info.shenpi[stageTypeInfo.shenpi_status] || 1,
+                    calcFields,
+                };
+                await this.layout(`cost/compare.ejs`, renderData, `cost/compare_modal.ejs`);
+            } catch(err) {
+                ctx.log(err);
+                ctx.postError(err, '读取成本报审错误');
+                ctx.redirect(`/sp/${ctx.subProject.id}/cost/tender/${ctx.tender.id}/${stageTypeInfo.key}`);
+            }
+        }
+
+        async gather(ctx) {
+            try {
+                await this.checkDutyRela(ctx);
+                const ledgerStages = await this.getTypeStages(ctx, 'ledger', 'ASC');
+                const renderData = {
+                    ledgerStages,
+                    jsFiles: this.app.jsFiles.common.concat(this.app.jsFiles.cost.cost_gather)
+                };
+                await this.layout('cost/gather.ejs', renderData, 'cost/gather_modal.ejs');
+            } catch(err) {
+                ctx.log(err);
+                ctx.postError(err, '获取汇总分析页面错误');
+                ctx.redirect(`/sp/${ctx.subProject.id}/cost`);
+            }
+        }
+        async _ledgerGatherLoad(stage) {
+            const result = {};
+            result.bills = stage.audit_status !== audit.common.status.checked
+                ? await this.ctx.service.costStageLedger.getReadData(stage)
+                : await this.ctx.service.costStageLedger.getEditData(stage);
+            return result;
+        }
+        async _bookGatherLoad(stage) {
+            const result = {};
+            stage.relaStage = await this.ctx.service.costStage.getStage(stage.rela_stage.sid);
+            result.bills = await this.ctx.service.costStageLedger.getReadData(stage.relaStage);
+            result.bills.forEach(l => { delete l.postil; delete l.memo; });
+            const book = stage.audit_status !== audit.common.status.checked
+                ? await this.ctx.service.costStageBook.getReadData(stage)
+                : await this.ctx.service.costStageBook.getEditData(stage);
+            this.ctx.helper.assignRelaData(result.bills, [
+                { data: book, fields: ['in_tp', 'in_excl_tax_tp', 'postil', 'memo'], prefix: '', relaId: 'ledger_id' },
+                { data: book, fields: ['id'], prefix: 'book_', relaId: 'ledger_id' },
+            ]);
+            return result;
+        }
+        async _analysisGatherLoad(stage) {
+            const result = {};
+            result.bills = stage.audit_status !== audit.common.status.checked
+                ? await this.ctx.service.costStageAnalysis.getReadData(stage)
+                : await this.ctx.service.costStageAnalysis.getEditData(stage);
+            return result;
+        }
+        async loadGatherData(ctx) {
+            try {
+                const data = JSON.parse(ctx.request.body.data);
+                const result = {};
+
+                if (data.stages) {
+                    result.stages = [];
+                    for (const s of data.stages) {
+                        const stage = await this.ctx.service.costStage.getStage(s);
+                        const loadFun = `_${stage.stage_type}GatherLoad`;
+                        if (this[loadFun]) {
+                            const detail = await this[loadFun](stage);
+                            result.stages.push({ id: stage.id, stage_date: stage.stage_date, stage_order: stage.stage_order, detail });
+                        } else {
+                            throw '未知期数据类型';
+                        }
+                    }
+                }
+                ctx.body = { err: 0, msg: '', data: result }
+            } catch(err) {
+                ctx.log(err);
+                ctx.ajaxErrorBody(err, '获取汇总数据错误');
+            }
+        }
     }
 
     return CostController;

+ 12 - 0
app/controller/dashboard_controller.js

@@ -47,6 +47,7 @@ module.exports = app => {
             const allAuditCostStageLedger = await ctx.service.costStageAudit.getAuditStage('ledger', ctx.session.sessionUser.accountId);
             const allAuditCostStageBook = await ctx.service.costStageAudit.getAuditStage('book', ctx.session.sessionUser.accountId);
             const allAuditCostStageAnalysis = await ctx.service.costStageAudit.getAuditStage('analysis', ctx.session.sessionUser.accountId);
+            const allAuditCostStageDuty = await ctx.service.costStageAudit.getAuditStage('duty', ctx.session.sessionUser.accountId);
             const pa = await ctx.service.projectAccount.getDataById(ctx.session.sessionUser.accountId);
             const auditShenpiTenders = await ctx.service.ledgerAudit.getDonesByAudit(ctx.session.sessionUser.accountId);
             const auditShenpiStages = await ctx.service.stageAudit.getDonesByAudit(ctx.session.sessionUser.accountId);
@@ -66,6 +67,7 @@ module.exports = app => {
             const auditShenpiCostStageLedger = await ctx.service.costStageAudit.getDonesByAudit('ledger', ctx.session.sessionUser.accountId);
             const auditShenpiCostStageBook = await ctx.service.costStageAudit.getDonesByAudit('book', ctx.session.sessionUser.accountId);
             const auditShenpiCostStageAnalysis = await ctx.service.costStageAudit.getDonesByAudit('analysis', ctx.session.sessionUser.accountId);
+            const auditShenpiCostStageDuty = await ctx.service.costStageAudit.getDonesByAudit('duty', ctx.session.sessionUser.accountId);
             const dashboardStatus = {
                 all: 0,
                 dashboard: 0,
@@ -90,6 +92,7 @@ module.exports = app => {
                     costStageLedger: 0,
                     costStageBook: 0,
                     costStageAnalysis: 0,
+                    costStageDuty: 0,
                 },
             };
             let noticeList = [];
@@ -133,6 +136,8 @@ module.exports = app => {
                 noticeList.push(...ctx.helper.addKeyValue4ObjArray(spCostStageBook, 'shenpi_type', 'costStageBook'));
                 const spCostStageAnalysis = subProject.page_show.cost ? ctx.helper._.filter(auditShenpiCostStageAnalysis, { spid: subProject.id }) : [];
                 noticeList.push(...ctx.helper.addKeyValue4ObjArray(spCostStageAnalysis, 'shenpi_type', 'costStageAnalysis'));
+                const spCostStageDuty = subProject.page_show.cost ? ctx.helper._.filter(auditShenpiCostStageDuty, { spid: subProject.id }) : [];
+                noticeList.push(...ctx.helper.addKeyValue4ObjArray(spCostStageDuty, 'shenpi_type', 'costStageDuty'));
             }
             const noticeDayList = [];
             noticeList = ctx.helper._.orderBy(noticeList, ['shenpi_time'], ['desc']);
@@ -180,6 +185,8 @@ module.exports = app => {
             dashboardShenpis.push(...auditCostStageBook);
             const auditCostStageAnalysis = await this.auditSet(ctx, allAuditCostStageAnalysis, subProjects, dashboardStatus, 'costStageAnalysis');
             dashboardShenpis.push(...auditCostStageAnalysis);
+            const auditCostStageDuty = await this.auditSet(ctx, allAuditCostStageDuty, subProjects, dashboardStatus, 'costStageDuty');
+            dashboardShenpis.push(...auditCostStageDuty);
             // console.log(ctx.helper._.orderBy(dashboardShenpis, ['start_audit', 'shenpi_time'], ['desc', 'desc']));
             const projectData = await ctx.service.project.getDataById(ctx.session.sessionProject.id);
             // 获取销售人员数据
@@ -335,6 +342,7 @@ module.exports = app => {
                         case 'costStageLedger':
                         case 'costStageBook':
                         case 'costStageAnalysis':
+                        case 'costStageDuty':
                             if (!sp.page_show.cost) closeType = true;
                             calcTime = t.create_time;
                             break;
@@ -390,6 +398,7 @@ module.exports = app => {
             const auditCostStageLedger = ctx.subProject.page_show.cost ? await ctx.service.costStageAudit.getAuditStage('ledger', ctx.session.sessionUser.accountId, ctx.subProject.id) : [];
             const auditCostStageBook = ctx.subProject.page_show.cost ? await ctx.service.costStageAudit.getAuditStage('book', ctx.session.sessionUser.accountId, ctx.subProject.id) : [];
             const auditCostStageAnalysis = ctx.subProject.page_show.cost ? await ctx.service.costStageAudit.getAuditStage('analysis', ctx.session.sessionUser.accountId, ctx.subProject.id) : [];
+            const auditCostStageDuty = ctx.subProject.page_show.cost ? await ctx.service.costStageAudit.getAuditStage('duty', ctx.session.sessionUser.accountId, ctx.subProject.id) : [];
             const pa = await ctx.service.projectAccount.getDataById(ctx.session.sessionUser.accountId);
             const noticeList = await ctx.service.noticePush.getNotice(ctx.session.sessionProject.id, pa.id, ctx.subProject.id);
             const projectData = await ctx.service.project.getDataById(ctx.session.sessionProject.id);
@@ -427,6 +436,7 @@ module.exports = app => {
                 shenpi_count.push({ count: await ctx.service.costStageAudit.getCountByChecked('ledger', ctx.session.sessionUser.accountId, ctx.subProject.id), name: '成本报审' });
                 shenpi_count.push({ count: await ctx.service.costStageAudit.getCountByChecked('book', ctx.session.sessionUser.accountId, ctx.subProject.id), name: '财务账面' });
                 shenpi_count.push({ count: await ctx.service.costStageAudit.getCountByChecked('analysis', ctx.session.sessionUser.accountId, ctx.subProject.id), name: '成本分析' });
+                shenpi_count.push({ count: await ctx.service.costStageAudit.getCountByChecked('duty', ctx.session.sessionUser.accountId, ctx.subProject.id), name: '责任成本' });
             }
             // shenpi_count.push({ count: await ctx.service.advanceAudit.getCountByChecked(ctx.session.sessionUser.accountId), name: '预付款' });
             const total_count = ctx.app._.sumBy(shenpi_count, 'count');
@@ -448,6 +458,7 @@ module.exports = app => {
                 ctx.subProject.page_show.cost ? await ctx.service.costStageAudit.getLastEndTimeByChecked('ledger', ctx.session.sessionUser.accountId, ctx.subProject.id) : null,
                 ctx.subProject.page_show.cost ? await ctx.service.costStageAudit.getLastEndTimeByChecked('book', ctx.session.sessionUser.accountId, ctx.subProject.id) : null,
                 ctx.subProject.page_show.cost ? await ctx.service.costStageAudit.getLastEndTimeByChecked('analysis', ctx.session.sessionUser.accountId, ctx.subProject.id) : null,
+                ctx.subProject.page_show.cost ? await ctx.service.costStageAudit.getLastEndTimeByChecked('duty', ctx.session.sessionUser.accountId, ctx.subProject.id) : null,
             ];
             const last_time = ctx.app._.max(shenpi_lastime);
             // console.log(ctx.app._.max(shenpi_lastime), ctx.helper.calcDayNum(last_time));
@@ -471,6 +482,7 @@ module.exports = app => {
                 auditCostStageLedger,
                 auditCostStageBook,
                 auditCostStageAnalysis,
+                auditCostStageDuty,
                 shenpi_count,
                 total_count,
                 last_day: ctx.helper.calcDayNum(last_time),

+ 143 - 60
app/controller/datacollect_controller.js

@@ -58,17 +58,38 @@ module.exports = app => {
                     return ctx.app._.indexOf(noTids, item.id) === -1;
                 });
                 const tenderidList = ctx.helper._.map(tenderList, 'id');
-                const noticeList = await ctx.service.noticePush.getNoticeByDataCollect(ctx.session.sessionProject.id, tenderidList);
                 // 获取分类
                 const categoryData = await this.ctx.service.category.getListByCategoryLevel(ctx.subProject.id);
                 // 默认坐标,否则则取办事处坐标
                 const projectData = await ctx.service.project.getDataById(ctx.session.sessionProject.id);
                 ctx.subProject.data_collect_pages = ctx.subProject.data_collect_pages ? ctx.subProject.data_collect_pages.split(',') : [];
+                // 判断ctx.subProject.data_collect_pages如果只有7或者只有8或者只有7,8,则不用获取其它数据
+                const pages = ctx.subProject.data_collect_pages;
+                const only_cost =
+                    (pages.length === 1 && (pages[0] === '7' || pages[0] === '8')) ||
+                    (pages.length === 2 &&
+                        pages.includes('7') &&
+                        pages.includes('8'));
+                let noticeList = [];
+                if (!only_cost) {
+                    noticeList = await ctx.service.noticePush.getNoticeByDataCollect(ctx.session.sessionProject.id, tenderidList);
+                }
                 if (ctx.params.index) {
                     ctx.subProject.data_collect = parseInt(ctx.params.index);
+                    ctx.session.sessionSubProj = {
+                        data_collect: ctx.subProject.data_collect,
+                    };
+                } else {
+                    ctx.session.sessionSubProj = {
+                        data_collect: ctx.subProject.data_collect,
+                    };
                 }
+                // const is_cost = ctx.subProject.data_collect_pages.includes('7') && ctx.subProject.data_collect === 7;
                 const is_dz2 = ['P0505', 'P0506', 'P1201', 'P1202', 'GY18Y', 'GYJJ1', 'P1103', 'KLG25', 'I2U5B', 'SEN48'].indexOf(ctx.session.sessionProject.code) !== -1
                     && ctx.subProject.data_collect_pages.includes('6') && ctx.subProject.data_collect === 6;
+                const is_dzcb = ['P0505', 'P0506', 'P1201', 'P1202', 'P1002', 'GY18Y', 'Y3T2Q'].indexOf(ctx.session.sessionProject.code) !== -1 && ctx.subProject.data_collect_pages.includes('7') && ctx.subProject.data_collect === 7;// 成本管理定制大屏
+                const is_dztz = ['P0505', 'P0506', 'P1201', 'P1202', 'P1002', 'Y3T2Q'].indexOf(ctx.session.sessionProject.code) !== -1 && ctx.subProject.data_collect_pages.includes('8') && ctx.subProject.data_collect === 8;// 投资总控定制大屏
+                // const shj_url = 'http://112.74.42.187:3300/#/project/viewer?projectId=b03i54j0j8ai';
                 const renderData = {
                     projectData,
                     noticeList,
@@ -82,8 +103,13 @@ module.exports = app => {
                     acAdvance: auditConst.advance,
                     pushType: auditConst.pushType,
                     jsFiles: this.app.jsFiles.common.concat(this.app.jsFiles.datacollect.index),
+                    // shj_url,
                 };
-                if (is_dz2) {
+                if (is_dzcb) {
+                    await this.layout('datacollect/index_cost.ejs', renderData);
+                } else if (is_dztz) {
+                    await this.layout('datacollect/index_tz.ejs', renderData);
+                } else if (is_dz2) {
                     // 判断并更新common_json
                     if (!ctx.subProject.common_json) {
                         ctx.subProject.common_json = await ctx.service.subProject.updateCommonJsonDaping06(ctx.subProject, projectData);
@@ -97,6 +123,8 @@ module.exports = app => {
                     }
                     renderData.dpCategory = dpCategory;
                     await this.layout('datacollect/index4GY18Y.ejs', renderData);
+                // } else if (is_dz3) {
+                //     ctx.redirect(shj_url);
                 } else {
                     await this.layout('datacollect/index.ejs', renderData);
                 }
@@ -112,72 +140,127 @@ module.exports = app => {
                 const responseData = { err: 0, msg: '', data: {} };
                 // const tenderidList = [];
                 // const noticeList = await ctx.service.noticePush.getNoticeByDataCollect(ctx.session.sessionProject.id, tenderidList);
-                const dcTenders = await ctx.service.datacollectTender.getList(ctx.session.sessionProject.id, ctx.subProject.id);
-                const noTids = ctx.app._.map(dcTenders, 'tid');
-                let tenderList = await ctx.service.tender.getList('', null, 1);
-                // let tenderList = await this.ctx.service.tender.getBuildList('', null, 1);
-                tenderList = ctx.app._.filter(tenderList, function(item) {
-                    return ctx.app._.indexOf(noTids, item.id) === -1;
-                });
+                const have_cost = ctx.session.sessionSubProj.data_collect === 7 || ctx.session.sessionSubProj.data_collect === 8;
                 const thisMonth = new Date();
                 const [startMonth, endMonth] = ctx.helper.getStartEndMonth(thisMonth);
                 // 统计方法总时长
                 // let sumStageByDataCollectTime = 0;
-                for (const t of tenderList) {
-                    // const tenderInfo = await ctx.service.tenderInfo.getTenderInfo(t.id);
-                    // t.contract_price = tenderInfo.deal_param.contractPrice;
-                    // let bCalcTp = t.ledger_status === auditConst.ledger.status.checkNo || t.ledger_status === auditConst.ledger.status.uncheck;
-                    // t.advance_tp = await ctx.service.advance.getSumAdvance(t.id);
-                    //
-                    // if (t.ledger_status === auditConst.ledger.status.checked) {
-                    //     t.lastStage = await ctx.service.stage.getLastestStage(t.id);
-                    //     if (t.lastStage) await ctx.service.stage.checkStageGatherDataByDataCollect(t.lastStage, true);
-                    //     t.completeStage = await ctx.service.stage.getLastestCompleteStage(t.id);
-                    //     if ((!bCalcTp) && t.measure_type === measureType.gcl.value) {
-                    //         bCalcTp = t.lastStage && t.lastStage.status !== auditConst.stage.status.checked && !t.lastStage.readOnly;
-                    //     }
-                    // }
-                    // if (bCalcTp) {
-                    //     const sum = await this.ctx.service.ledger.addUp({ tender_id: t.id/* , is_leaf: true*/ });
-                    //     t.total_price = sum.total_price;
-                    //     t.deal_tp = sum.deal_tp;
-                    // }
-                    // 用标段管理的方法获取t数据
-                    await this.ctx.service.tenderCache.loadTenderCache(t, '');
-                    t.total_price = t.ledger_tp && t.ledger_tp.total_price ? t.ledger_tp.total_price : 0;
-                    [t.change_tp, t.change_p_tp, t.change_n_tp, t.change_valuation_tp, t.change_unvaluation_tp] = await ctx.service.change.getChangeTp(t.id);
-                    // t.material_tp = await ctx.service.material.getSumMaterial(t.id);
-                    // 获取本标段 本月计量期审批通过数目,变更令审批通过数目,台账修订通过数目,材料调差通过数目
-                    t.month_stage_num = await ctx.service.stageAudit.getNumByMonth(t.id, startMonth, endMonth);
-                    t.month_change_num = await ctx.service.changeAudit.getNumByMonth(t.id, startMonth, endMonth);
-                    t.month_revise_num = await ctx.service.reviseAudit.getNumByMonth(t.id, startMonth, endMonth);
-                    t.month_material_num = await ctx.service.materialAudit.getNumByMonth(t.id, startMonth, endMonth);
-                    // 获取标段计量月统计及截止月累计计量
-                    // 这个慢,统计下面这个方法时长
-                    const stageList = await ctx.service.stage.getStageByDataCollect(t.id, t.stage_tp);
-                    // const stageList = [];
-                    const month_stage = [];
-                    for (const s of stageList) {
-                        const monthOneStage = ctx.app._.find(month_stage, { yearmonth: s.s_time });
-                        if (monthOneStage) {
-                            monthOneStage.tp = ctx.helper.add(monthOneStage.tp, s.tp);
-                            monthOneStage.end_yf_tp = ctx.helper.add(monthOneStage.end_yf_tp, s.yf_tp);
-                            monthOneStage.end_sf_tp = ctx.helper.add(monthOneStage.end_sf_tp, s.sf_tp);
-                        } else {
-                            const data = {
-                                yearmonth: s.s_time,
-                                tp: s.tp,
-                                end_tp: s.end_tp,
-                                end_yf_tp: s.yf_tp,
-                                end_sf_tp: s.sf_tp,
-                            };
-                            month_stage.push(data);
+                const only_tz = ctx.session.sessionSubProj.data_collect === 8;
+                if (!only_tz) {
+                    const dcTenders = await ctx.service.datacollectTender.getList(ctx.session.sessionProject.id, ctx.subProject.id);
+                    const noTids = ctx.app._.map(dcTenders, 'tid');
+                    let tenderList = await ctx.service.tender.getList('', null, 1);
+                    // let tenderList = await this.ctx.service.tender.getBuildList('', null, 1);
+                    tenderList = ctx.app._.filter(tenderList, function(item) {
+                        return ctx.app._.indexOf(noTids, item.id) === -1;
+                    });
+                    for (const t of tenderList) {
+                        // 用标段管理的方法获取t数据
+                        if (!have_cost) {
+                            await this.ctx.service.tenderCache.loadTenderCache(t, '');
+                            t.total_price = t.ledger_tp && t.ledger_tp.total_price ? t.ledger_tp.total_price : 0;
+                            [t.change_tp, t.change_p_tp, t.change_n_tp, t.change_valuation_tp, t.change_unvaluation_tp] = await ctx.service.change.getChangeTp(t.id);
+                            // t.material_tp = await ctx.service.material.getSumMaterial(t.id);
+                            // 获取本标段 本月计量期审批通过数目,变更令审批通过数目,台账修订通过数目,材料调差通过数目
+                            t.month_stage_num = await ctx.service.stageAudit.getNumByMonth(t.id, startMonth, endMonth);
+                            t.month_change_num = await ctx.service.changeAudit.getNumByMonth(t.id, startMonth, endMonth);
+                            t.month_revise_num = await ctx.service.reviseAudit.getNumByMonth(t.id, startMonth, endMonth);
+                            t.month_material_num = await ctx.service.materialAudit.getNumByMonth(t.id, startMonth, endMonth);
+                            // 获取标段计量月统计及截止月累计计量
+                            // 这个慢,统计下面这个方法时长
+                            const stageList = await ctx.service.stage.getStageByDataCollect(t.id, t.stage_tp);
+                            // const stageList = [];
+                            const month_stage = [];
+                            for (const s of stageList) {
+                                const monthOneStage = ctx.app._.find(month_stage, { yearmonth: s.s_time });
+                                if (monthOneStage) {
+                                    monthOneStage.tp = ctx.helper.add(monthOneStage.tp, s.tp);
+                                    monthOneStage.end_yf_tp = ctx.helper.add(monthOneStage.end_yf_tp, s.yf_tp);
+                                    monthOneStage.end_sf_tp = ctx.helper.add(monthOneStage.end_sf_tp, s.sf_tp);
+                                } else {
+                                    const data = {
+                                        yearmonth: s.s_time,
+                                        tp: s.tp,
+                                        end_tp: s.end_tp,
+                                        end_yf_tp: s.yf_tp,
+                                        end_sf_tp: s.sf_tp,
+                                    };
+                                    month_stage.push(data);
+                                }
+                            }
+                            t.month_stage = month_stage;
+                        }
+                        if (have_cost) {
+                            // 取最新已审批完成的成本管理-成本分析期数据
+                            t.cost_analysis = await ctx.service.costStage.getLastCheckedStage(t.id, 'analysis');
+                            t.stage_num = await ctx.service.stage.getNumByChecked(t.id);
+                            t.cost_ledger_num = await ctx.service.costStage.getNumByChecked(t.id, 'ledger');
+                            t.cost_analysis_num = await ctx.service.costStage.getNumByChecked(t.id, 'analysis');
+                            t.cost_book_num = await ctx.service.costStage.getNumByChecked(t.id, 'book');
+                        }
+                    }
+                    responseData.data.tenderList = tenderList;
+                }
+                if (have_cost) {
+                    const data = {};
+                    const budget = await ctx.service.budget.getCurBudget(ctx.subProject.budget_id);
+                    budget.name = ctx.subProject.name || '';
+                    if (!budget) throw '未设置概预算标准';
+                    if (budget.final_id) {
+                        data.final = await ctx.service.budgetFinal.getAllDataByCondition({ where: { final_id: budget.final_id } });
+                        data.finalInfo = await ctx.service.budgetFinalList.getFinal(budget.final_id);
+                    } else {
+                        data.gu = await ctx.service.budgetGu.getData(budget.id);
+                        data.gai = await ctx.service.budgetGai.getData(budget.id);
+                        data.yu = await ctx.service.budgetYu.getData(budget.id);
+                        data.zb = await ctx.service.budgetZb.getData(budget.id);
+                        data.ctrl = await ctx.service.budgetCtrl.getData(budget.id);
+                        data.real = await ctx.service.budgetReal.getData(budget.id);
+                    }
+                    data.info = budget;
+                    responseData.data.budget = data;
+                }
+                if (only_tz) {
+                    const commonJson = ctx.subProject.common_json ? JSON.parse(ctx.subProject.common_json) : null;
+                    const daping08Set = commonJson && commonJson.daPing08_set ? commonJson.daPing08_set : ctx.helper._.cloneDeep(projectSettingConst.daPing08Set);
+                    const tenderList = {
+                        1: {
+                            a: null,
+                            b: null,
+                            // name: '',
+                            // contract_price: 0,
+                            // end_gather_tp: 0,
+                            // end_profit: 0,
+                        },
+                        2: {
+                            a: null,
+                            b: null,
+                        },
+                        3: {
+                            a: null,
+                            b: null,
+                        },
+                        4: {
+                            a: null,
+                            b: null,
+                        },
+                    };
+                    for (const t of daping08Set.tenders) {
+                        tenderList[t.id] = {};
+                        const tendera = t.a ? await ctx.service.tender.getDataById(t.a) : null;
+                        if (tendera) {
+                            await this.ctx.service.tenderCache.loadTenderCache(tendera, '');
+                            tenderList[t.id].a = tendera;
+                            const tenderb = t.b ? await ctx.service.tender.getDataById(t.b) : null;
+                            if (tenderb) {
+                                await this.ctx.service.tenderCache.loadTenderCache(tenderb, '');
+                                tenderList[t.id].b = tenderb;
+                            }
                         }
                     }
-                    t.month_stage = month_stage;
+                    responseData.data.tenderList = tenderList;
                 }
                 // const tenderMapList = await ctx.service.tenderMap.getAllDataByCondition({ where: { tid: tender.id } });
-                responseData.data.tenderList = tenderList;
                 ctx.body = responseData;
             } catch (err) {
                 this.log(err);

+ 245 - 1
app/controller/ledger_controller.js

@@ -667,7 +667,6 @@ module.exports = app => {
         //         await ctx.helper.saveStreamFile(stream, fileName);
 
         //         // 读取excel
-        //         console.log(ctx.query);
         //         const name = ctx.query.sheetName;
         //         if (!name) throw '未选择需要导入的工作簿';
         //         const wb = xlsx.readFile(fileName);
@@ -1186,6 +1185,251 @@ module.exports = app => {
                 ctx.body = responseData;
             }
         }
+
+        async _aiCompliePosCalcDetail(ctx, data) {
+            const pos = await this.ctx.service.pos.getDataById(data.pos);
+            if (!pos) throw '选择的计量单元不存在,请刷新页面后再试';
+
+            const validColSet = data.template.col_set.filter(x => { return !x.expr; });
+            const AIDify = require('../lib/ai_dify');
+            const aiDify = new AIDify({ apiKey: ctx.difyInfo.apiKey.image2PosCalcDetail, baseUrl: ctx.difyInfo.baseUrl });
+            const transResult = await aiDify.posCalcDetailRead(validColSet, data.billsName, data.keyword, data.files);
+            const rows = transResult.res.split('\n').filter(x => { return !!x; });
+            rows.shift();
+            const pcdData = rows.map(x => {
+                const rd = x.split('|');
+                const nd = { pid: pos.id, lid: pos.lid };
+                for (const [i, c] of validColSet.entries()) {
+                    if (c.type === 'num') {
+                        nd[c.field] = ctx.helper._.toNumber(rd[i]);
+                    } else {
+                        nd[c.field] = rd[i];
+                    }
+                }
+                return nd;
+            });
+            const responseData = await ctx.service.posCalcDetail.updateDatas({ add: pcdData });
+            ctx.body = { err: 0, msg: '', data: responseData };
+        }
+        async _aiComplieSteelPos(ctx, bills, data) {
+            const validColSet = data.template.col_set.filter(x => { return !x.expr; });
+            const AIDify = require('../lib/ai_dify');
+            const aiDify = new AIDify({ apiKey: ctx.difyInfo.apiKey.image2Pos, baseUrl: ctx.difyInfo.baseUrl });
+            const transResult = await aiDify.posCalcDetailRead(validColSet, data.billsName, data.keyword, data.files);
+            const parts = transResult.res.split('\n==++==\n').filter(x => { return !!x; });
+            const pos = [];
+            const posInfoReg = new RegExp('{{.*}}', 'igm');
+            for (const p of parts) {
+                const newPos = { lid: bills.id };
+                const nameStr = p.match(posInfoReg)[0];
+                if (nameStr) {
+                    const pjson = nameStr.substr(1, nameStr.length - 2);
+                    const pinfo = JSON.parse(pjson);
+                    newPos.name = pinfo.pname;
+                }
+                const detailStr = p.substr(nameStr.length, p.length - nameStr.length);
+                const [detail, ancGcl] = detailStr.split(`\n==+ancGcl+==\n`);
+                const rows = detail.split('\n').filter(x => { return !!x; });
+                rows.shift();
+                newPos.calcDetail = rows.map(x => {
+                    const rd = x.split('|');
+                    const nd = { pid: pos.id, lid: pos.lid };
+                    for (const [i, c] of validColSet.entries()) {
+                        if (c.type === 'num') {
+                            nd[c.field] = ctx.helper._.toNumber(rd[i]);
+                        } else {
+                            nd[c.field] = rd[i];
+                        }
+                    }
+                    return nd;
+                });
+                const agRows = ancGcl.split('\n').filter(x => { return !!x; });
+                agRows.shift();
+                newPos.ancGcl = agRows.map(x => {
+                    const rd = x.split('|');
+                    return { pid: pos.id, lid: pos.lid, name: rd[0] || '', unit: rd[1] || 0, quantity:  ctx.helper._.toNumber(rd[2]) || 0 };
+                });
+                pos.push(newPos);
+            }
+            const responseData = await ctx.service.pos.savePosData({ updateType: 'add-with-detail', updateData: { lid: bills.id, pos} }, ctx.tender.id);
+            ctx.body = { err: 0, msg: '', data: responseData };
+        }
+        async _aiComplieConcretePos(ctx, bills, data) {
+            const validColSet = data.template.col_set.filter(x => { return !x.expr; });
+            const AIDify = require('../lib/ai_dify');
+            const aiDify = new AIDify({ apiKey: ctx.difyInfo.apiKey.image2PosQty, baseUrl: ctx.difyInfo.baseUrl });
+            const transResult = await aiDify.posCalcDetailRead(validColSet, data.billsName, data.keyword, data.files);
+            const parts = transResult.res.split('\n==++==\n').filter(x => { return !!x; });
+            const pos = [];
+            for (const p of parts) {
+                const newPos = { lid: bills.id };
+                const pInfo = JSON.parse(p);
+                newPos.name = pInfo.name;
+                newPos.sgfh_qty = ctx.helper._.toNumber(pInfo.qty) || 0;
+            }
+            const responseData = await ctx.service.pos.savePosData({ updateType: 'add-with-detail', updateData: { lid: bills.id, pos} }, ctx.tender.id);
+            ctx.body = { err: 0, msg: '', data: responseData };
+        }
+        async _aiCompliePosAndDetail(ctx, data) {
+            const bills = await this.ctx.service.ledger.getDataById(data.bills);
+            if (!bills) throw '选择的清单不存在,请刷新页面后再试';
+
+            switch(data.billsType) {
+                case '钢筋':
+                case '钢绞线':
+                    return this._aiComplieSteelPos(ctx, bills, data);
+                case '混凝土':
+                    return this._aiComplieConcretePos(ctx, bills, data);
+                default: throw `暂不支持清单类型为${data.billsType}的清单,进行ai识别填写`
+
+            }
+        }
+        async aiComplie(ctx) {
+            let stream;
+            try {
+                const parts = ctx.multipart({ autoFields: true });
+
+                let index = 0;
+                const create_time = Date.parse(new Date()) / 1000;
+                stream = await parts();
+                const user = await ctx.service.projectAccount.getDataById(ctx.session.sessionUser.accountId);
+                const rela_id = parts.field.rela_id;
+                const rela_type = parts.field.rela_type;
+                const billsName = parts.field.rela_bills_name;
+                const rela_template = parts.field.rela_template;
+                const template = await ctx.service.calcTmpl.getTemplate(rela_template);
+                const keyword = parts.field.rela_keyword;
+                const billsType = parts.field.rela_billsType;
+
+                const uploadfiles = [];
+                while (stream !== undefined) {
+                    if (!stream.filename) throw '未发现上传文件!';
+
+                    const fileInfo = path.parse(stream.filename);
+                    const filepath = `app/public/upload/${ctx.tender.id}/temp/${ctx.moment().format('YYYYMMDD')}/${create_time + '_' + index + fileInfo.ext}`;
+
+                    // 保存文件
+                    await ctx.app.fujianOss.put(ctx.app.config.fujianOssFolder + filepath, stream);
+                    await sendToWormhole(stream);
+
+                    // 插入到stage_pay对应的附件列表中
+                    uploadfiles.push({
+                        rela_id, rela_type,
+                        filename: fileInfo.name,
+                        fileext: fileInfo.ext,
+                        filesize: Array.isArray(parts.field.size) ? parts.field.size[index] : parts.field.size,
+                        filepath,
+                    });
+                    ++index;
+                    if (Array.isArray(parts.field.size) && index < parts.field.size.length) {
+                        stream = await parts();
+                    } else {
+                        stream = undefined;
+                    }
+                }
+                const result = await ctx.service.tempFile.addFiles(ctx.tender.id, uploadfiles, user);
+
+                switch(rela_type) {
+                    case 'pos':
+                        await this._aiCompliePosCalcDetail(ctx, { billsName, template, pos: rela_id, files: result, keyword });
+                        break;
+                    case 'bills':
+                        await this._aiCompliePosAndDetail(ctx, { billsName, template, bills: rela_id, files: result, keyword, billsType });
+                        break;
+                    default:
+                        throw '暂不提供该ai识别内容';
+                }
+            } catch (err) {
+                ctx.log(err);
+                if (stream) await sendToWormhole(stream);
+                ctx.ajaxErrorBody(err, 'ai识别填写失败');
+            }
+        }
+
+        async exportLedgerData(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',
+                        'quantity', 'total_price', 'drawing_code', 'memo', 'features',
+                        'dgn_qty1', 'dgn_qty2', 'deal_qty', 'deal_tp',
+                        'sgfh_qty', 'sgfh_tp', 'sgfh_expr',
+                        'sjcl_qty', 'sjcl_tp', 'sjcl_expr',
+                        'qtcl_qty', 'qtcl_tp', 'qtcl_expr',
+                        'ex_memo1', 'ex_memo2', 'ex_memo3', 'ex_qty1', 'ex_tp1', 'is_new_price',
+
+                    ],
+                    where: { tender_id: ctx.tender.id }
+                });
+                const posData = await ctx.service.pos.getAllDataByCondition({
+                    columns: [
+                        'id', 'lid', 'name', 'position', 'porder', 'drawing_code',
+                        'quantity', 'sgfh_qty', 'sjcl_qty', 'qtcl_qty', 'sgfh_expr', 'sjcl_expr', 'qtcl_expr',
+                        'ex_qty1', 'ex_memo1', 'ex_memo2', 'ex_memo3',
+                    ],
+                    where: { tid: ctx.tender.id }
+                });
+
+                const Cpd = require('../lib/crypt').cpd;
+                const cpd = new Cpd();
+                const filename = `${ctx.tender.data.name}-台账数据.cpd`;
+                const filepath = path.join(this.ctx.app.baseDir, 'temp', filename);
+                await cpd.encrypt({ ledger: ledgerData, pos: posData, type: 'ledger', measure_type: ctx.tender.data.measure_type }, 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 importLedgerData(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);
+                if (data.type !== 'ledger') throw '导入的数据非台账数据';
+                if (data.measure_type !== ctx.tender.data.measure_type) throw '导入数据的台账类型不同,不可导入';
+
+                await ctx.service.ledger.importCpdData(data.ledger, data.pos);
+                ctx.body = { err: 0, mgs: '', data: '' };
+            } catch (err) {
+                // 失败需要消耗掉stream 以防卡死
+                if (stream) await sendToWormhole(stream);
+                ctx.log(err);
+                ctx.ajaxErrorBody(err, '导入文件失败');
+            }
+        }
     }
 
     return LedgerController;

+ 1 - 1
app/controller/profile_controller.js

@@ -430,7 +430,7 @@ module.exports = app => {
                         const account = JSON.parse(dskAccount.dsk_account);
                         const compilation = await dsk.getCompilation(data.mobile);
                         response.data = {};
-                        response.data.compilation = compilation;
+                        response.data.compilation = compilation && compilation.length > 0 ? ctx.helper._.filter(compilation, { type: 'highway' }) : [];
                         if (data.getProject) {
                             const compilationInfo = data.compilationId ? ctx.helper._.find(compilation, { ID: data.compilationId }) : null;
                             const compilationId = compilationInfo ? compilationInfo.ID : (compilation.length > 0 ? compilation[0].ID : null);

+ 15 - 0
app/controller/report_controller.js

@@ -1411,6 +1411,21 @@ module.exports = app => {
             await this._indexForContract(ctx, sourceTypeConst.sourceType.contract, -600, preUrl, thisUrl);
         }
 
+        async indexForCostManager(ctx) {
+            // 成本管理报表入口
+            try {
+                const ledge_stage_type = this.ctx.service.costStage.stageType.ledger.key;
+                const analysis_stage_type = this.ctx.service.costStage.stageType.analysis.key;
+                const book_stage_type = this.ctx.service.costStage.stageType.book.key;
+                const ledge_stages = await this.getTypeStages(ctx, ledge_stage_type);
+                const analysis_stages = await this.getTypeStages(ctx, analysis_stage_type);
+                const book_stages = await this.getTypeStages(ctx, book_stage_type);
+                // await this._getCostManagerViewData(ctx);
+            } catch (err) {
+                //
+            }
+        }
+
         /**
          * 报表打印页面
          *

+ 1 - 1
app/controller/stage_controller.js

@@ -2476,7 +2476,7 @@ module.exports = app => {
                 ctx.body = await fs.readFileSync(filepath);
             } catch(err) {
                 ctx.log(err);
-                ctx.postError(err, '导出计量法数据失败');
+                ctx.postError(err, '导出计量数据失败');
             }
         }
 

+ 11 - 0
app/controller/sub_proj_setting_controller.js

@@ -444,6 +444,8 @@ module.exports = app => {
                 const tenders = await ctx.service.tender.getList('', null, 1);
                 const dcTenders = await ctx.service.datacollectTender.getList(ctx.subProject.project_id, ctx.subProject.id);
                 const is_dz2 = ['P0505', 'P0506', 'P1201', 'P1202', 'GY18Y', 'GYJJ1', 'P1103', 'KLG25', 'I2U5B', 'SEN48'].indexOf(ctx.session.sessionProject.code) !== -1 ? 6 : false;
+                const is_dzcb = ['P0505', 'P0506', 'P1201', 'P1202', 'GY18Y', 'Y3T2Q'].indexOf(ctx.session.sessionProject.code) !== -1 ? 7 : false;// 成本管理定制大屏
+                const is_dztz = ['P0505', 'P0506', 'P1201', 'P1202', 'GY18Y', 'Y3T2Q'].indexOf(ctx.session.sessionProject.code) !== -1 ? 8 : false;// 投资总控定制大屏
                 const renderData = {
                     dataCollectAudits,
                     accountList,
@@ -453,6 +455,8 @@ module.exports = app => {
                     dcTenders,
                     is_dz1: ['P0505', 'MI22U'].indexOf(ctx.session.sessionProject.code) !== -1 ? 5 : false,
                     is_dz2,
+                    is_dzcb,
+                    is_dztz,
                     selfCategoryLevel: await ctx.service.projectAccount.getSelfCategoryLevel(this.ctx.session.sessionUser.accountId),
                 };
                 if (is_dz2) {
@@ -464,6 +468,10 @@ module.exports = app => {
                     const commonJson = ctx.subProject.common_json ? JSON.parse(ctx.subProject.common_json) : null;
                     renderData.daPing06Set = commonJson && commonJson.daPing06_set ? commonJson.daPing06_set : ctx.helper._.cloneDeep(projectSettingConst.daPing06Set);
                 }
+                if (is_dztz) {
+                    const commonJson = ctx.subProject.common_json ? JSON.parse(ctx.subProject.common_json) : null;
+                    renderData.daPing08Set = commonJson && commonJson.daPing08_set ? commonJson.daPing08_set : ctx.helper._.cloneDeep(projectSettingConst.daPing08Set);
+                }
                 await this.layout('sp_setting/datacollect.ejs', renderData, 'sp_setting/datacollect_modal.ejs');
             } catch (error) {
                 ctx.log(error);
@@ -529,6 +537,9 @@ module.exports = app => {
                     case 'save-dp06':
                         responseData.data = await ctx.service.subProject.saveCommonJson(ctx.subProject.id, 'daPing06_set', data.daPing06Set);
                         break;
+                    case 'save-dp08':
+                        responseData.data = await ctx.service.subProject.saveCommonJson(ctx.subProject.id, 'daPing08_set', data.daPing08Set);
+                        break;
                     default:
                         throw '参数有误';
                 }

+ 2 - 1
app/controller/template_controller.js

@@ -243,7 +243,8 @@ module.exports = app => {
                 const Cpd = require('../lib/crypt').cpd;
                 const cpd = new Cpd();
                 const data = await cpd.decrypt(filepath);
-                if (!data.col_set || !data.multi_header) throw '模板文件格式错误';
+                // if (!data.col_set || !data.multi_header) throw '模板文件格式错误';
+                if (!data.col_set) throw '模板文件格式错误';
                 if (template.type !== data.type) throw '导入的模板文件与当前模板类型不一致,不可导入';
 
                 const result =await ctx.service.calcTmpl.saveTemplate({ update: { id: template.id, col_set: data.col_set, multi_header: data.multi_header } });

+ 67 - 1
app/controller/tender_controller.js

@@ -1003,6 +1003,7 @@ module.exports = app => {
         }
         async shenpiSet(ctx) {
             try {
+                ctx.tender.costDuty = await this.ctx.service.costStage.getStageByOrder(ctx.tender.id, 'duty', 1);
                 const specShenpiType = this._getSpecShenpiUrl(ctx);
                 // 获取所有项目参与者
                 const accountList = await ctx.service.projectAccount.getAllSubProjectAccount(ctx.subProject);
@@ -1113,7 +1114,7 @@ module.exports = app => {
                 await ctx.service.tenderInfo.saveTenderInfo(ctx.tender.id, { shenpi: postData });
                 let auditList = [];
                 let groupList = [];
-                const spType =  shenpiConst.sp_type[data.code]|| shenpiConst.cost_sp_type[data.code];
+                const spType = shenpiConst.sp_type[data.code] || shenpiConst.cost_sp_type[data.code];
                 if (data.status === shenpiConst.sp_status.gdspl) {
                     groupList = await ctx.service.shenpiGroup.getGroupList(ctx.tender.id, spType) || [];
                     if (groupList && groupList.length > 0) {
@@ -1819,6 +1820,71 @@ module.exports = app => {
                 ctx.ajaxErrorBody(err, '保存计算式失败');
             }
         }
+
+        async aiImage2Excel(ctx) {
+            let stream;
+            try {
+                const parts = ctx.multipart({ autoFields: true });
+
+                let index = 0;
+                const create_time = Date.parse(new Date()) / 1000;
+                stream = await parts();
+                const user = await ctx.service.projectAccount.getDataById(ctx.session.sessionUser.accountId);
+                const rela_id = parts.field.rela_id;
+                const rela_type = parts.field.rela_type;
+
+                const uploadfiles = [];
+                while (stream !== undefined) {
+                    if (!stream.filename) throw '未发现上传文件!';
+
+                    const fileInfo = path.parse(stream.filename);
+                    const filepath = `app/public/upload/${ctx.tender.id}/temp/${ctx.moment().format('YYYYMMDD')}/${create_time + '_' + index + fileInfo.ext}`;
+
+                    // 保存文件
+                    await ctx.app.fujianOss.put(ctx.app.config.fujianOssFolder + filepath, stream);
+                    await sendToWormhole(stream);
+
+                    // 插入到stage_pay对应的附件列表中
+                    uploadfiles.push({
+                        rela_id, rela_type,
+                        filename: fileInfo.name,
+                        fileext: fileInfo.ext,
+                        filesize: Array.isArray(parts.field.size) ? parts.field.size[index] : parts.field.size,
+                        filepath,
+                    });
+                    ++index;
+                    if (Array.isArray(parts.field.size) && index < parts.field.size.length) {
+                        stream = await parts();
+                    } else {
+                        stream = undefined;
+                    }
+                }
+
+                const result = await ctx.service.tempFile.addFiles(ctx.tender.id, uploadfiles, user);
+                const AIDify = require('../lib/ai_dify');
+                const aiDify = new AIDify({ apiKey: ctx.difyInfo.apiKey.image2Excel, baseUrl: ctx.difyInfo.baseUrl });
+                const transResult = await aiDify.imageTransExcel(result);
+                ctx.body = { err: 0, msg: '', data: transResult.res };
+            } catch (err) {
+                ctx.log(err);
+                // 失败需要消耗掉stream 以防卡死
+                if (stream) await sendToWormhole(stream);
+                ctx.ajaxErrorBody(err, '图片转换失败');
+            }
+        }
+        async aiBillsIdentify (ctx) {
+            try {
+                const data = JSON.parse(ctx.request.body.data);
+                if (data.name === undefined || data.unit === undefined) throw '参数错误';
+                const AIDify = require('../lib/ai_dify');
+                const aiDify = new AIDify({ apiKey: ctx.difyInfo.apiKey.billsIdentify, baseUrl: ctx.difyInfo.baseUrl });
+                const result = await aiDify.billsIdentify(data.name, data.unit);
+                ctx.body = { err: 0, msg: '', data: result.res };
+            } catch(err) {
+                ctx.log(err);
+                ctx.ajaxErrorBody(err, '识别清单类型失败');
+            }
+        }
     }
 
     return TenderController;

+ 2 - 4
app/controller/wap_controller.js

@@ -21,7 +21,7 @@ const path = require('path');
 const sendToWormhole = require('stream-wormhole');
 const moment = require('moment');
 const auditType = require('../const/audit').auditType;
-const AiInspect = require('../lib/ai_inspect');
+const AiDify = require('../lib/ai_dify');
 const uuid = require('node-uuid');
 const nlsToken = require('../lib/nls_token');
 const streamToArray = require('stream-to-array');
@@ -1436,9 +1436,7 @@ module.exports = app => {
                 if (!user_input) {
                     throw '参数有误';
                 }
-                const ai = new AiInspect({
-                    chatflowId: conversationId,
-                });
+                const ai = new AiDify({ apiKey: ctx.difyInfo.apiKey.inspection, chatflowId: conversationId });
                 const result = await ai.ask(user_input);
                 let answerText = result.answer || '';
                 let jsonPart = null;

+ 4 - 0
app/extend/context.js

@@ -90,6 +90,10 @@ module.exports = {
         return this.app.config.stashOssPath;
     },
 
+    get difyInfo() {
+        return this.app.config.difyInfo;
+    },
+
     print(str) {
         this.getLogger('out').info(str);
     },

+ 144 - 0
app/lib/ai_dify.js

@@ -0,0 +1,144 @@
+'use strict';
+const axios = require('axios');
+const DOCUMENT_EXTENSIONS = ['pdf', 'doc', 'docx'];
+const IMAGE_EXTENSIONS = ['jpg', 'jpeg', 'png', 'webp'];
+
+class AiDify {
+    /**
+     * 构造函数
+     * @param {object} config
+     * @param {string} config.apiKey - Dify API Key
+     * @param {string} config.chatflowId - ChatFlow ID
+     * @param {string} [config.baseUrl=https://api.dify.ai/v1] - Dify API 接口地址
+     */
+    constructor({ apiKey = 'app-fiJr1pNxjdKXpetko8ANawnY', chatflowId = '', baseUrl = 'http://workflow.smartcost.com.cn/v1' }) {
+        if (!apiKey) {
+            throw new Error('apiKey 不能为空');
+        }
+
+        this.apiKey = apiKey;
+        this.chatflowId = chatflowId;
+        this.baseUrl = baseUrl;
+    }
+
+    /**
+     * 向 ChatFlow 发送请求
+     * @param {string} userInput - 用户输入内容
+     * @param {string} [userId='user_123'] - 用户 ID
+     * @returns {Promise<object>} - Dify 返回的对象
+     */
+    async ask(userInput, userId = 'user_123') {
+        const url = `${this.baseUrl}/chat-messages`;
+        const headers = {
+            'Authorization': `Bearer ${this.apiKey}`,
+            'Content-Type': 'application/json',
+        };
+
+        const body = {
+            inputs: {}, // 如果你的 ChatFlow 有 schema,可填写 key-value
+            query: userInput,
+            response_mode: 'blocking',
+            user: userId,
+            conversation_id: this.chatflowId,
+        };
+
+        try {
+            const res = await axios.post(url, body, { headers });
+            return res.data;
+        } catch (err) {
+            console.error('Dify 请求失败:', err.response || err.response.data || err.message);
+            throw new Error('Dify 请求失败');
+        }
+    }
+
+    async workflows(body) {
+        const url = `${this.baseUrl}/workflows/run`;
+        const headers = {
+            'Authorization': `Bearer ${this.apiKey}`,
+            'Content-Type': 'application/json',
+        };
+        try {
+            const res = await axios.post(url, body, { headers });
+            return res.data.data.outputs;
+        } catch (err) {
+            console.error('Dify 请求失败:', err.response || err.response.data || err.message);
+            throw new Error('Dify 请求失败');
+        }
+    }
+
+    async posCalcDetailRead(col, billsName, keyword, files, userId = `user_123`) {
+        const body = {
+            inputs: {
+                header: col.map(x => { return x.title + (x.unit ? `(${x.unit})` : '') }).join(','),
+                billsName,
+                keyword,
+                imgList: [],
+            },
+            user: userId,
+        };
+        for (const f of files) {
+            body.inputs.imgList.push({
+                type: "image",
+                transfer_method: "remote_url",
+                url: f.viewpath,
+            })
+        }
+        return await this.workflows(body);
+    }
+
+    async imageTransExcel(files, userId = `user_123`) {
+        const body = {
+            inputs: { imgList: [] },
+            user: userId,
+            response_mode: 'blocking',
+        };
+        for (const f of files) {
+            body.inputs.imgList.push({
+                type: "image",
+                transfer_method: "remote_url",
+                url: f.viewpath,
+            })
+        }
+        return await this.workflows(body);
+    }
+
+    async billsIdentify(name, unit, userId = `user_123`) {
+        const body = {
+            inputs: { billsName: name || '' + ( unit ? `(${unit})` : '') },
+            user: userId,
+            response_mode: 'blocking',
+        };
+        return await this.workflows(body);
+    }
+
+    /**
+     * 根据文件名后缀判断 Dify 的 type 字段(image / document)
+     */
+    resolveDifyFileType(filename = '') {
+        const ext = filename ? filename.split('.').pop().toLowerCase() : '';
+        if (IMAGE_EXTENSIONS.includes(ext)) return 'image';
+        if (DOCUMENT_EXTENSIONS.includes(ext)) return 'document';
+        // 无法判断时默认按document处理,让Dify那边自行兜底
+        return 'document';
+    }
+
+    async contractTrans(file, userId = 'user_123') {
+        const body = {
+            inputs: { fileList: [] },
+            user: userId,
+            response_mode: 'blocking',
+        };
+        if (!file.viewpath) {
+            throw new Error('文件缺少viewpath字段');
+        }
+        const fileType = this.resolveDifyFileType(file.filename);
+        body.inputs.fileList.push({
+            type: fileType,
+            transfer_method: 'remote_url',
+            url: file.viewpath,
+        });
+        return await this.workflows(body);
+    }
+}
+
+module.exports = AiDify;

+ 0 - 53
app/lib/ai_inspect.js

@@ -1,53 +0,0 @@
-'use strict';
-const axios = require('axios');
-
-class AiInspect {
-    /**
-     * 构造函数
-     * @param {object} config
-     * @param {string} config.apiKey - Dify API Key
-     * @param {string} config.chatflowId - ChatFlow ID
-     * @param {string} [config.baseUrl=https://api.dify.ai/v1] - Dify API 接口地址
-     */
-    constructor({ apiKey = 'app-fiJr1pNxjdKXpetko8ANawnY', chatflowId = '', baseUrl = 'http://workflow.smartcost.com.cn/v1' }) {
-        if (!apiKey) {
-            throw new Error('apiKey 不能为空');
-        }
-
-        this.apiKey = apiKey;
-        this.chatflowId = chatflowId;
-        this.baseUrl = baseUrl;
-    }
-
-    /**
-     * 向 ChatFlow 发送请求
-     * @param {string} userInput - 用户输入内容
-     * @param {string} [userId='user_123'] - 用户 ID
-     * @returns {Promise<object>} - Dify 返回的对象
-     */
-    async ask(userInput, userId = 'user_123') {
-        const url = `${this.baseUrl}/chat-messages`;
-        const headers = {
-            'Authorization': `Bearer ${this.apiKey}`,
-            'Content-Type': 'application/json',
-        };
-
-        const body = {
-            inputs: {}, // 如果你的 ChatFlow 有 schema,可填写 key-value
-            query: userInput,
-            response_mode: 'blocking',
-            user: userId,
-            conversation_id: this.chatflowId,
-        };
-
-        try {
-            const res = await axios.post(url, body, { headers });
-            return res.data;
-        } catch (err) {
-            console.error('Dify 请求失败:', err.response || err.response.data || err.message);
-            throw new Error('Dify 请求失败');
-        }
-    }
-}
-
-module.exports = AiInspect;

+ 141 - 15
app/lib/budget_final.js

@@ -169,21 +169,99 @@ class BudgetFinal {
         });
     }
 
-    async _loadTender(id) {
+    async _loadReal(budget) {
+        const helper = this.ctx.helper;
+        const real = await this.ctx.service.budgetReal.getData(budget.id);
+        const realTree = new BillsTree(this.ctx, { id: 'tree_id', pid: 'tree_pid', order: 'order', level: 'level', rootId: -1, calcFields: ['total_price'] });
+        realTree.loadDatas(real);
+        realTree.calculateAll();
+        this.finalTree.loadTree(realTree, function (cur, source) {
+            cur.base = true;
+            cur.real_dgn_qty1 = helper.add(cur.real_dgn_qty1, source.dgn_qty1);
+            cur.real_dgn_qty2 = helper.add(cur.real_dgn_qty2, source.dgn_qty2);
+            cur.real_tp = helper.add(cur.real_tp, source.total_price);
+        });
+    }
+
+    async _getValidStages(tenderId, sort = 'desc', checked = false) {
+        const accountId = this.ctx.session.sessionUser.accountId;
+        const condition = { tid: tenderId };
+        if (checked) condition.status = auditConst.stage.status.checked;
+        const stages = await this.ctx.service.stage.getAllDataByCondition({ where: condition, orders: [['order', sort]] });
+        return stages.filter(s => { return s.status !== auditConst.stage.status.uncheck || s.user_id === accountId; });
+    }
+    async _filterOrder(id, order, checked) {
+        const stages = await this._getValidStages(id, 'desc', checked);
+        const stage = this.ctx.helper._.find(stages, { order });
+        if (stage) await this.ctx.service.stage.doCheckStage(stage);
+        return { type: 'stage', stage, filter: `第${order}期` };
+    }
+    async _filterMonth(id, month, checked) {
+        const stages = await this._getValidStages(id, 'desc', checked);
+        const stage = this.ctx.helper._.find(stages, { s_time: month });
+        if (stage) await this.ctx.service.stage.doCheckStage(stage);
+        return { type: 'stage', stage, filter: month };
+    }
+    async _filterFinal(id, checked) {
+        const stages = await this._getValidStages(id, 'desc', checked);
+        const stage = stages[0];
+        if (stage) await this.ctx.service.stage.doCheckStage(stage);
+        return { type: 'stage', stage, filter: stage ? `第${stage.order}期` : '' };
+    }
+    async _filterCheckedFinal(id, checked) {
+        const stages = await this._getValidStages(id, 'desc', checked);
+        const checkedStages = stages.filter(x => { return x.status === status.checked; });
+        const stage = checkedStages[0];
+        if (stage && stage.status !== auditConst.stage.status.checked) await this.ctx.service.stage.doCheckStage(stage);
+        return { type: 'stage', stage, filter: stage ? `第${stage.order}期` : '' };
+    }
+    async _filterUnderMonth(id, month, checked) {
+        const stages = await this._getValidStages(id, 'desc', checked);
+        let stage = this.ctx.helper._.find(stages, { s_time: month });
+        if (!stage) {
+            const checkTime = this.ctx.moment(month);
+            for (const s of stages) {
+                const rela_time = this.ctx.moment(s.s_time);
+                if (rela_time.isBefore(checkTime)) {
+                    stage = s;
+                    break;
+                }
+            }
+        }
+        if (stage) await this.ctx.service.stage.doCheckStage(stage);
+        return { type: 'stage', stage, filter: stage ? stage.s_time : '' };
+    }
+    async _filterStage(tender, stageInfo) {
+        switch(stageInfo.type) {
+            case 'stage':
+                return await this._filterOrder(tender, stageInfo.stage, stageInfo.checked);
+            case 'month':
+                return await this._filterMonth(tender, stageInfo.month, stageInfo.checked);
+            case 'final':
+                return await this._filterFinal(tender, stageInfo.checked);
+            case 'checked-final':
+                return await this._filterCheckedFinal(tender, stageInfo.checked);
+            case 'under-month':
+                return await this._filterUnderMonth(tender, stageInfo.month, stageInfo.checked);
+            default:
+                return await this._filterFinal(tender, stageInfo.checked);
+        }
+    }
+    async _loadTender(id, stageInfo) {
         const helper = this.ctx.helper;
         const bills = await this.ctx.service.ledger.getFinalData(id, ['id', 'ledger_id', 'ledger_pid', 'level', 'order', 'full_path', 'is_leaf',
             'code', 'b_code', 'name', 'unit', 'unit_price', 'dgn_qty1', 'dgn_qty2', 'total_price']);
 
         const dgnData = await this.ctx.service.stageBillsDgn.getDgnData(id);
         // 使用最新一期对比
-        const stage = await this.ctx.service.stage.getLastestStage(id);
-        if (!stage) {
+        const stageFilter = await this._filterStage(id, stageInfo);
+        if (!stageFilter.stage) {
             helper.assignRelaData(bills, [
                 { data: dgnData, fields: ['deal_dgn_qty1', 'deal_dgn_qty2', 'c_dgn_qty1', 'c_dgn_qty2'], prefix: '', relaId: 'id' },
             ]);
             this.final.tender_info.push({ id, stageCount: 0 });
-        } else if (stage.status === auditConst.stage.status.checked) {
-            const finalBills = await this.ctx.service.stageBillsFinal.getFinalData({id}, stage.order);
+        } else if (stageFilter.stage.status === auditConst.stage.status.checked) {
+            const finalBills = await this.ctx.service.stageBillsFinal.getFinalData({ id }, stageFilter.stage.order);
             helper.assignRelaData(bills, [
                 { data: finalBills, fields: ['contract_tp', 'qc_tp'], prefix: 'end_', relaId: 'lid' },
                 { data: dgnData, fields: ['deal_dgn_qty1', 'deal_dgn_qty2', 'c_dgn_qty1', 'c_dgn_qty2'], prefix: '', relaId: 'id' },
@@ -191,14 +269,13 @@ class BudgetFinal {
             bills.forEach(b => {
                 b.end_gather_tp = helper.add(b.end_qc_tp, b.end_contract_tp);
             });
-            this.final.tender_info.push({ id, stageOrder: stage.order });
+            this.final.tender_info.push({ id, stageOrder: stageFilter.stage.order });
         } else {
-            await this.ctx.service.stage.doCheckStage(stage);
-            const curBills = stage.readOnly
-                ? await this.ctx.service.stageBills.getAuditorStageData2(id, stage.id, stage.curTimes, stage.curOrder)
-                : await this.ctx.service.stageBills.getLastestStageData2(id, stage.id);
-            const preBills = stage.preCheckedStage ? await this.ctx.service.stageBillsFinal.getFinalData({id}, stage.preCheckedStage.order) : [];
-            const bpcData = await this.ctx.service.stageBillsPc.getAllDataByCondition({ where: { sid: stage.id } });
+            const curBills = stageFilter.stage.readOnly
+                ? await this.ctx.service.stageBills.getAuditorStageData2(id, stageFilter.stage.id, stageFilter.stage.curTimes, stageFilter.stage.curOrder)
+                : await this.ctx.service.stageBills.getLastestStageData2(id, stageFilter.stage.id);
+            const preBills = stageFilter.stage.preCheckedStage ? await this.ctx.service.stageBillsFinal.getFinalData({ id }, stageFilter.stage.preCheckedStage.order) : [];
+            const bpcData = await this.ctx.service.stageBillsPc.getAllDataByCondition({ where: { sid: stageFilter.stage.id } });
             helper.assignRelaData(bills, [
                 { data: curBills, fields: ['contract_tp', 'qc_tp'], prefix: '', relaId: 'lid' },
                 { data: preBills, fields: ['contract_tp', 'qc_tp'], prefix: 'pre_', relaId: 'lid' },
@@ -210,7 +287,7 @@ class BudgetFinal {
                 b.end_qc_tp = helper.sum([b.qc_tp, b.pre_qc_tp, b.qc_pc_tp]);
                 b.end_gather_tp = helper.sum([b.qc_tp, b.contract_tp, b.pre_qc_tp, b.pre_contract_tp, b.pc_tp]);
             });
-            this.final.tender_info.push({ id, stageOrder: stage.order, stageStatus: stage.status, stageFlow: stage.curTimes + '-' + stage.curOrder });
+            this.final.tender_info.push({ id, stageOrder: stageFilter.stage.order, stageStatus: stageFilter.stage.status, stageFlow: stageFilter.stage.curTimes + '-' + stageFilter.stage.curOrder });
         }
         const tree = new BillsTree(this.ctx, this.tenderSetting);
         tree.loadDatas(bills);
@@ -239,6 +316,48 @@ class BudgetFinal {
         });
     }
 
+    async _loadDeal(deal, stage) {
+        if (!deal || deal.length === 0) return;
+
+        this.final.contract = deal;
+        const helper = this.ctx.helper;
+        const treeData = await this.ctx.service.contractTree.getAllDataByCondition({ where: { id: deal, spid: this.ctx.subProject.id, contract_type: 1 } });
+        const contractData = await this.ctx.service.contract.getAllDataByCondition({ where: { id: deal, spid: this.ctx.subProject.id, contract_type: 1 } });
+
+        const paySqlParam = [];
+        const timeFilter = stage.type === 'under-month' ? `AND pay_time <= ? ` : '';
+        if (stage.type === 'under-month') {
+            paySqlParam.push(this.ctx.moment(stage.month, 'YYYY-MM').endOf('month').format('YYYY-MM-DD HH:mm:ss'));
+        }
+        const checkedFilter = stage.checked && this.ctx.subProject.page_show.openContractPaySubProjectShenpi ? ' AND status = 3 ' : '';
+        const paySql = `SELECT cid, SUM(yf_price) AS yf_tp FROM ${this.ctx.service.contractPay.tableName}` +
+            `    WHERE cid IN(${contractData.map(x => { return `'${x.id}'`}).join(', ')}) ` + timeFilter + checkedFilter +
+            '    GROUP BY cid';
+        const contractPayData = await this.ctx.service.contractPay.db.query(paySql, paySqlParam);
+        for (const c of contractData) {
+            const cp = contractPayData.find(x => { return x.cid === c.id; });
+            c.yf_tp = cp ? cp.yf_tp || 0 : 0;
+        }
+
+        const tree = new BillsTree(this.ctx, {
+            id: 'contract_id',
+            pid: 'contract_pid',
+            order: 'order',
+            level: 'level',
+            rootId: -1,
+            calcFields: ['total_price', 'yf_tp'],
+            calc: function (node, helper, decimal) {},
+        });
+        tree.loadDatas([...treeData, ...contractData]);
+        tree.calculateAll();
+        this.finalTree.loadTree(tree, function (cur, source) {
+            if (source.c_code) cur.code = source.c_code;
+            cur.total_price = helper.add(cur.total_price, source.total_price);
+            cur.final_contract_tp = helper.add(cur.final_contract_tp, source.yf_tp);
+            cur.final_tp = helper.add(cur.final_tp, source.yf_tp);
+        });
+    }
+
     async _afterLoad() {
         const helper = this.ctx.helper;
         const checkGaiExist = this.finalTree.datas.findIndex(x => {
@@ -270,6 +389,10 @@ class BudgetFinal {
             node.ctrl_dgn_qty = node.ctrl_dgn_qty1
                 ? (node.ctrl_dgn_qty2 ? node.ctrl_dgn_qty1 + '/' + node.ctrl_dgn_qty2 : node.ctrl_dgn_qty1 + '')
                 : (node.ctrl_dgn_qty2 ? '/' + node.ctrl_dgn_qty2 : '');
+            node.real_dgn_price = helper.div(node.real_tp, node.real_dgn_qty1, 2);
+            node.real_dgn_qty = node.real_dgn_qty1
+                ? (node.real_dgn_qty2 ? node.real_dgn_qty1 + '/' + node.real_dgn_qty2 : node.real_dgn_qty1 + '')
+                : (node.real_dgn_qty2 ? '/' + node.real_dgn_qty2 : '');
 
             node.final_dgn_price = helper.div(node.final_tp, node.final_dgn_qty1, 2);
             node.final_dgn_qty = node.final_dgn_qty1
@@ -312,6 +435,7 @@ class BudgetFinal {
                 yu_dgn_qty1: x.yu_dgn_qty1 || 0, yu_dgn_qty2: x.yu_dgn_qty2 || 0, yu_dgn_qty: x.yu_dgn_qty || '', yu_dgn_price: x.yu_dgn_price || 0, yu_tp: x.yu_tp || 0,
                 zb_dgn_qty1: x.zb_dgn_qty1 || 0, zb_dgn_qty2: x.zb_dgn_qty2 || 0, zb_dgn_qty: x.zb_dgn_qty || '', zb_dgn_price: x.zb_dgn_price || 0, zb_tp: x.zb_tp || 0,
                 ctrl_dgn_qty1: x.ctrl_dgn_qty1 || 0, ctrl_dgn_qty2: x.ctrl_dgn_qty2 || 0, ctrl_dgn_qty: x.ctrl_dgn_qty || '', ctrl_dgn_price: x.ctrl_dgn_price || 0, ctrl_tp: x.ctrl_tp || 0,
+                real_dgn_qty1: x.real_dgn_qty1 || 0, real_dgn_qty2: x.real_dgn_qty2 || 0, real_dgn_qty: x.real_dgn_qty || '', real_dgn_price: x.real_dgn_price || 0, real_tp: x.real_tp || 0,
 
                 dgn_qty1: x.dgn_qty1 || 0, dgn_qty2: x.dgn_qty2 || 0, total_price: x.total_price || 0,
                 dgn_price: x.dgn_price || 0, dgn_qty: x.dgn_qty || '',
@@ -326,19 +450,21 @@ class BudgetFinal {
         return data;
     }
 
-    async doFinal(budget, final) {
+    async doFinal(budget, final, stage, contract) {
         this.budget = budget;
         this.final = final;
         this.finalTree.checkField = budget.final_type.split('_');
 
         for (const t of final.tender) {
-            await this._loadTender(t);
+            await this._loadTender(t, stage);
         }
+        await this._loadReal(budget);
         await this._loadCtrl(budget);
         await this._loadZb(budget);
         await this._loadYu(budget);
         await this._loadGai(budget);
         await this._loadGu(budget);
+        await this._loadDeal(contract, stage);
         this._afterLoad();
         return this.getFinalData();
     }

+ 3 - 2
app/lib/ledger.js

@@ -362,7 +362,7 @@ class baseTree {
         if (fun) {
             fun(node);
         } else if (this.setting.calc) {
-            this.setting.calc(node, this.ctx.helper, this.ctx.tender.info.decimal);
+            this.setting.calc(node, this.ctx.helper, this.ctx.tender ? this.ctx.tender.info.decimal : {});
         }
     }
     calculateAll(fun) {
@@ -698,9 +698,10 @@ class pos {
     }
 
     resortLedgerPos(ledgerPos) {
+        const orderField = this.setting.orderField || 'porder';
         if (ledgerPos instanceof Array) {
             ledgerPos.sort(function (a, b) {
-                return a.porder - b.porder;
+                return a[orderField] - b[orderField];
             })
         }
     }

+ 12 - 0
app/lib/rm/budget.js

@@ -92,6 +92,14 @@ class rptMemPaymentSafe extends RptMemBase {
         // return showLevel ? tree.getDefaultDatasByLevel(this.ctx.tender.rpt_show_level) : tree.getDefaultDatas();
         return tree.getDefaultDatas();
     }
+    async budgetReal(bid, showLevel = false) {
+        const real = await this.ctx.service.budgetReal.getAllDataByCondition({ where: { bid } });
+        const tree = new ledger.billsTree(this.ctx, { id: 'tree_id', pid: 'tree_pid', order: 'order', level: 'level', rootId: -1, calcFields: ['total_price'] });
+        tree.loadDatas(real);
+        tree.calculateAll();
+        // return showLevel ? tree.getDefaultDatasByLevel(this.ctx.tender.rpt_show_level) : tree.getDefaultDatas();
+        return tree.getDefaultDatas();
+    }
     async budgetFinalInfo(bid) {
         const budget = this.ctx.budget && this.ctx.budget.id === bid
             ? this.ctx.budget
@@ -128,6 +136,8 @@ class rptMemPaymentSafe extends RptMemBase {
                 return this.budgetZb(this.ctx.budget.id);
             case 'mem_budget_ctrl':
                 return this.budgetCtrl(this.ctx.budget.id);
+            case 'mem_budget_real':
+                return this.budgetReal(this.ctx.budget.id);
             case 'mem_budget_final':
                 return this.budgetFinal(this.ctx.budget.id);
             case 'mem_budget_gu_filter':
@@ -140,6 +150,8 @@ class rptMemPaymentSafe extends RptMemBase {
                 return this.budgetZb(this.ctx.budget.id, true);
             case 'mem_budget_ctrl_filter':
                 return this.budgetCtrl(this.ctx.budget.id, true);
+            case 'mem_budget_real_filter':
+                return this.budgetReal(this.ctx.budget.id, true);
             case 'mem_budget_final_filter':
                 return this.budgetFinal(this.ctx.budget.id, true);
             default:

+ 4 - 0
app/lib/rm/tender.js

@@ -195,6 +195,8 @@ class rptMemPaymentSafe extends RptMemBase {
                 return params.budget_id ? budgetSource.budgetZb(params.budget_id) : budgetSource.tenderZb(params.tender_id);
             case 'mem_budget_ctrl':
                 return params.budget_id ? budgetSource.budgetCtrl(params.budget_id) : budgetSource.tenderCtrl(params.tender_id);
+            case 'mem_budget_real':
+                return params.budget_id ? budgetSource.budgetReal(params.budget_id) : budgetSource.tenderReal(params.tender_id);
             case 'mem_budget_final':
                 return params.budget_id ? budgetSource.budgetFinal(params.budget_id) : budgetSource.tenderFinal(params.tender_id);
             case 'mem_budget_gu_filter':
@@ -207,6 +209,8 @@ class rptMemPaymentSafe extends RptMemBase {
                 return params.budget_id ? budgetSource.budgetZb(params.budget_id, true) : budgetSource.tenderZb(params.tender_id, true);
             case 'mem_budget_ctrl_filter':
                 return params.budget_id ? budgetSource.budgetCtrl(params.budget_id, true) : budgetSource.tenderCtrl(params.tender_id, true);
+            case 'mem_budget_real_filter':
+                return params.budget_id ? budgetSource.budgetReal(params.budget_id, true) : budgetSource.tenderReal(params.tender_id, true);
             case 'mem_budget_final_filter':
                 return params.budget_id ? budgetSource.budgetFinal(params.budget_id, true) : budgetSource.tenderFinal(params.tender_id, true);
             case 'mem_pm_deal_pay':

+ 12 - 0
app/lib/rm/tender_budget.js

@@ -57,6 +57,13 @@ class reportMemoryBudget {
         tree.calculateAll();
         return showLevel ? tree.getDefaultDatasByLevel(this.ctx.tender.rpt_show_level) : tree.getDefaultDatas();
     }
+    async budgetReal(bid, showLevel = false) {
+        const real = await this.ctx.service.budgetReal.getAllDataByCondition({ where: { bid } });
+        const tree = new ledger.billsTree(this.ctx, { id: 'tree_id', pid: 'tree_pid', order: 'order', level: 'level', rootId: -1, calcFields: ['total_price'] });
+        tree.loadDatas(real);
+        tree.calculateAll();
+        return showLevel ? tree.getDefaultDatasByLevel(this.ctx.tender.rpt_show_level) : tree.getDefaultDatas();
+    }
     async budgetFinal(bid, showLevel = false) {
         const budget = this.ctx.budget && this.ctx.budget.id === bid
             ? this.ctx.budget
@@ -112,6 +119,11 @@ class reportMemoryBudget {
         return this.budget ? await this.budgetCtrl(this.budget.id, filter) : [];
     }
 
+    async tenderReal(tid, filter = false) {
+        await this._getTenderBudget(tid);
+        return this.budget ? await this.budgetReal(this.budget.id, filter) : [];
+    }
+
     async tenderFinal(tid, filter = false) {
         await this._getTenderBudget(tid);
         console.log(this.budget);

+ 257 - 0
app/lib/rptCustomData.js

@@ -9,6 +9,7 @@
  */
 const auditConst = require('../const/audit');
 const Ledger = require('./ledger');
+const mergeChar = ';';
 
 /**
  * 季华项目 定制报表
@@ -712,7 +713,263 @@ class fjHelper {
     }
 }
 
+/**
+ * 华晶项目 定制报表
+ * 台账复核表:根据【台账顺序】列出所有清单、计量单元及明细、附属工程量及明细
+ */
+class huajingHelper {
+    constructor(ctx) {
+        this.ctx = ctx;
+        this.dataType = { jl: '计量', fs: '附属' };
+    }
+    async loadLedgerData(tid) {
+        this.ledger = new Ledger.billsTree(this.ctx, {
+            id: 'ledger_id',
+            pid: 'ledger_pid',
+            order: 'order',
+            level: 'level',
+            rootId: -1,
+            keys: ['id', 'tender_id', 'ledger_id'],
+            stageId: 'id',
+            calcFields: [],
+            calc: function (node, helper, decimal) {},
+        });
+        const ledgerData = await this.ctx.service.ledger.getData(tid);
+        const ledgerExtraData = await this.ctx.service.ledgerExtra.getAllDataByCondition({ where: { tid } });
+        this.ctx.helper.assignRelaData(ledgerData, [
+            { data: ledgerExtraData, fields: ['calc_template'], prefix: '', relaId: 'id' },
+        ]);
+        this.ledger.loadDatas(ledgerData);
+
+        this.pos = new Ledger.pos({ id: 'id', ledgerId: 'lid' });
+        const posData = await this.ctx.service.pos.getAllDataByCondition({ where: { tid }});
+        this.pos.loadDatas(posData);
+
+        this.posCalcDetail = new Ledger.pos({ id: 'id', ledgerId: 'pid', orderField: 'pcd_order' });
+        const posCalcDetailData = await this.ctx.service.posCalcDetail.getAllDataByCondition({ where: { tid } });
+        this.posCalcDetail.loadDatas(posCalcDetailData);
+
+        this.ancGcl = new Ledger.pos({ id: 'id', ledgerId: 'pid', orderField: 'g_order' });
+        const ancGclData = await this.ctx.service.ancillaryGcl.getAllDataByCondition({ where: { tid } });
+        this.ancGcl.loadDatas(ancGclData);
+
+        this.ancGclDetail = new Ledger.pos({ id: 'id', ledgerId: 'ag_id', orderField: 'agd_order' });
+        const ancGclDetailData = await this.ctx.service.ancillaryGclDetail.getAllDataByCondition({ where: { tid } });
+        this.ancGclDetail.loadDatas(ancGclDetailData);
+
+        this.calcTemplate = await this.ctx.service.calcTmpl.getAllTemplateDetail(this.ctx.session.sessionProject.id, 'posCalc');
+    }
+    CheckPeg(text) {
+        const pegReg = /[a-zA-Z]*[kK][0-9]+[++][0-9]{3}([.][0-9]+)?/;
+        return pegReg.test(text);
+    }
+    getPegNode(node) {
+        if (node) {
+            if (this.CheckPeg(node.name)) {
+                return node;
+            } else {
+                const parent = this.ledger.getParent(node);
+                return parent ? this.getPegNode(parent) : null;
+            }
+        }
+    }
+    getNodeByLevel(node, level) {
+        let cur = node;
+        while (cur && cur.level > level) {
+            cur = this.ledger.getParent(cur);
+        }
+        return cur;
+    }
+    getDwgc(peg, xmj) {
+        if (peg) {
+            return peg.name;
+        } else {
+            const node = this.getNodeByLevel(xmj, 2);
+            return node ? node.name : '';
+        }
+    }
+    getFbgc(peg, xmj) {
+        if (peg && peg.id !== xmj.id) {
+            const node = this.getNodeByLevel(xmj, peg.level + 1);
+            return node ? node.name : '';
+        } else {
+            const node = this.getNodeByLevel(xmj, 3);
+            return node ? node.name : '';
+        }
+    }
+    getFxgc (peg, xmj) {
+        if (!peg) {
+            const node = this.getNodeByLevel(xmj, 4);
+            return node ? node.name : '';
+        } else if (peg.id === xmj.id) {
+            if (xmj.level > 4) {
+                let value = '';
+                for (let level = 4; level < xmj.level; level++) {
+                    const node = this.getNodeByLevel(xmj, level);
+                    value = value === '' ? node.name : value + mergeChar + node.name;
+                }
+                return value;
+            } else {
+                return '';
+            }
+        } else {
+            if (peg.level + 2 < xmj.level) {
+                let value = '';
+                for (let level = peg.level + 2; level < xmj.level; level++) {
+                    const node = this.getNodeByLevel(xmj, level);
+                    value = value === '' ? node.name : value + mergeChar + node.name;
+                }
+                return value;
+            } else {
+                return '';
+            }
+        }
+    }
+    newCacheLeafXmj(leafXmj) {
+        const peg = this.getPegNode(leafXmj);
+        const cacheLX = {
+            id: leafXmj.id,
+            code: leafXmj.code,
+            jldy: leafXmj.name,
+            fbgc: this.getFbgc(peg, leafXmj),
+            fxgc: this.getFxgc(peg, leafXmj),
+            dwgc: this.getDwgc(peg, leafXmj),
+            drawing_code: leafXmj.drawing_code,
+        };
+        this.leafXmjs.push(cacheLX);
+        return cacheLX;
+    }
+    getCacheLeafXmj(leafXmj) {
+        if (!this.leafXmjs) this.leafXmjs = [];
+
+        const cacheLX = this.leafXmjs.find(lx => { return lx.id === leafXmj.id; });
+        if (!cacheLX) {
+            return this.newCacheLeafXmj(leafXmj);
+        } else {
+            return cacheLX;
+        }
+    }
+    convertCalcHeader(target, calcTemplate) {
+        for (const [i, col] of calcTemplate.col_set.entries()) {
+            target['detail_' + i] = col.title + (col.unit ? `(${col.unit})` : '');
+        }
+    }
+    convertCalcData(target, source, calcTemplate) {
+        for (const [i, col] of calcTemplate.col_set.entries()) {
+            target['detail_' + i] = source[col.field];
+        }
+    }
+    convertAncGclDetail(gclBaseData, pos, ancGcl, details, calcTemplate) {
+        const calc = [];
+        for (const d of details) {
+            calc.push(d.qty);
+            const dData = {
+                ...gclBaseData,
+                pos_id: pos.id, pos_name: pos.name,
+                anc_gcl_id: ancGcl.id, anc_gcl_name: ancGcl.name,
+                anc_gcl_detail_id: d.id, data_type: this.dataType.fs,
+            };
+            this.convertCalcData(dData, d, calcTemplate);
+            this.ledgerCheckData.push(dData);
+        }
+        return calc.join('+');
+    }
+    convertAncGcl(gclBaseData, pos, ancGcl) {
+        for (const ag of ancGcl) {
+            const agData = {
+                ...gclBaseData,
+                pos_id: pos.id, pos_name: pos.name,
+                anc_gcl_id: ag.id, anc_gcl_name: ancGcl.name, quantity: ag.quantity, data_type: this.dataType.fs,
+            };
+            const calcTemplate = this.calcTemplate.find(x => { return x.id === ag.calc_template; });
+            this.ledgerCheckData.push(agData);
+
+            const details = this.ancGclDetail.getLedgerPos(ag.id);
+            if (calcTemplate) this.convertCalcHeader(agData, calcTemplate);
+            if (calcTemplate && details && details.length > 0) {
+                agData.expr = this.convertAncGclDetail(gclBaseData, pos, ag, details, calcTemplate);
+            } else {
+                agData.expr = ag.expr;
+            }
+        }
+    }
+    convertPosCalcDetail(gclBaseData, pos, details, calcTemplate) {
+        const calc = [];
+        for (const d of details) {
+            calc.push(d.qty);
+            const dData = {
+                ...gclBaseData,
+                pos_id: pos.id, pos_name: pos.name,
+                pos_calc_detail_id: d.id, data_type: this.dataType.jl,
+            };
+            this.convertCalcData(dData, d, calcTemplate);
+            this.ledgerCheckData.push(dData);
+        }
+        return calc.join('+');
+    }
+    convertPosRange(gclBaseData, posRange, calcTemplate) {
+        const calc = [];
+        for (const p of posRange) {
+            calc.push(p.sgfh_qty);
+            const posData = {
+                ...gclBaseData,
+                pos_id: p.id, pos_name: p.name, quantity: p.sgfh_qty, data_type: this.dataType.jl,
+            };
+            this.ledgerCheckData.push(posData);
+            const details = this.posCalcDetail.getLedgerPos(p.id);
+            if (calcTemplate) this.convertCalcHeader(posData, calcTemplate);
+            if (calcTemplate && details && details.length > 0) {
+                posData.expr = this.convertPosCalcDetail(gclBaseData, p, details, calcTemplate);
+            } else {
+                posData.expr = p.sgfh_expr;
+            }
+            const ancGcl = this.ancGcl.getLedgerPos(p.id);
+            if (ancGcl && ancGcl.length > 0) this.convertAncGcl(gclBaseData, p, ancGcl);
+        }
+        return calc.join('+');
+    }
+    convertGclNode(node, leafXmj) {
+        const cacheLeafXmj = this.getCacheLeafXmj(leafXmj);
+        const posRange = this.pos.getLedgerPos(node.id);
+        const gclBaseData = {
+            id: node.id, tid: node.tender_id, leaf_xmj_id: leafXmj.id,
+            b_code: node.b_code, name: node.name, unit: node.unit, unit_price: node.unit_price,
+            drawing_code: node.drawing_code,
+            g_jldy: cacheLeafXmj.jldy, g_fbgc: cacheLeafXmj.fbgc, g_fxgc: cacheLeafXmj.fxgc, g_dwgc: cacheLeafXmj.dwgc, g_bwmx: cacheLeafXmj.bwmx,
+        };
+        const gclData = { ...gclBaseData, quantity: node.sgfh_qty, data_type: this.dataType.jl } ;
+        this.ledgerCheckData.push(gclData);
+        if (posRange && posRange.length > 0) {
+            const calcTemplate = node.calc_template ? this.calcTemplate.find(x => { return x.id === node.calc_template; }) : null;
+            gclData.expr = this.convertPosRange(gclBaseData, posRange, calcTemplate);
+        } else {
+            gclData.expr = node.sgfh_expr;
+        }
+    }
+    recursiveConvertLedgerCheckData(nodes, leafXmj) {
+        for (const node of nodes) {
+            if (node.b_code) {
+                if (node.children.length > 0) {
+                    this.recursiveConvertLedgerCheckData(node.children, leafXmj);
+                } else {
+                    this.convertGclNode(node, leafXmj);
+                }
+            } else if (node.children.length > 0) {
+                this.recursiveConvertLedgerCheckData(node.children, node);
+            }
+        }
+    }
+    async getLedgerCheckData(tid) {
+        await this.ctx.service.tender.checkTender(tid);
+        await this.loadLedgerData(tid);
+        this.ledgerCheckData = [];
+        this.recursiveConvertLedgerCheckData(this.ledger.children, null);
+        return this.ledgerCheckData;
+    }
+}
+
 module.exports = {
     jhHelper,
     fjHelper,
+    huajingHelper,
 };

+ 25 - 3
app/lib/ybp.js

@@ -18,7 +18,7 @@ const key = CryptoJS__default.default.enc.Utf8.parse('7563850888ABCDEF'); // 十
 const iv = CryptoJS__default.default.enc.Utf8.parse('ABCDEF7563850888'); // 十六位十六进制数作为密钥偏移量
 
 const fs = require('fs');
-const ybpSpr = '|----|', rationSpr = '|====|';
+const ybpSpr = '|----|', rationSpr = '|====|', billsSpr = '|++++|', qtyCalcSpr = '|~~~~|';
 
 const Decrypt = function(str) {
     const encryptedHexStr = CryptoJS__default.default.enc.Hex.parse(str);
@@ -34,6 +34,19 @@ const Decrypt = function(str) {
 
 class ybp {
     parseSubject(str) {
+        let billsParts = [];
+        if (str.indexOf(billsSpr) >= 0) {
+            const parts = str.split(billsSpr);
+            str = parts[0];
+            billsParts = parts.slice(1);
+        }
+        let qtyCalcParts = [];
+        if (str.indexOf(qtyCalcSpr) >= 0) {
+            const parts = str.split(qtyCalcSpr);
+            str = parts[0];
+            qtyCalcParts = parts.slice(1);
+        }
+        let result;
         if (str.indexOf(rationSpr) >= 0) {
             const arr = str.split(rationSpr);
             const subject = JSON.parse(LzString.decompressFromBase64(arr[0]));
@@ -41,10 +54,19 @@ class ybp {
                 const ration = JSON.parse(LzString.decompressFromBase64(arr[i]));
                 subject.rations.push(...ration);
             }
-            return subject;
+            result = subject;
         } else {
-            return JSON.parse(LzString.decompressFromBase64(str));
+            result = JSON.parse(LzString.decompressFromBase64(str));
         }
+        if (billsParts.length > 0) {
+            if (!result.bills) result.bills = [];
+            for (const part of billsParts) {
+                const bills = JSON.parse(LzString.decompressFromBase64(part));
+                result.bills.push(...bills);
+            }
+        }
+        // todo 暂不考虑算量数据
+        return result;
     }
     decryptBuffer(buffer) {
         const arr = buffer.split(ybpSpr);

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

@@ -1665,6 +1665,9 @@ overflow-y: auto;
 .height-20{
   height: 19%;
 }
+.height-25 {
+    height: 24%;
+}
 .review_box{
   height: 100%;
   overflow: hidden;
@@ -2347,3 +2350,157 @@ animation:shake 1s .2s ease both;}
     background: #ccc;
     border-radius: 3px;
 }
+.ai-upload-area {
+    border: 2px dashed #d9d9d9;
+    border-radius: 8px;
+    padding: 24px;
+    text-align: center;
+    cursor: pointer;
+    transition: border-color 0.2s;
+    border-color: #007bff;
+    color: #007bff;
+}
+.ai-upload-area:hover,
+.ai-upload-area.dragover {
+    border-color: #0069d9;
+    background: #f3f8ff;
+}
+.ai-upload-result {
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    padding: 8px 0;
+    font-size: 14px;
+}
+
+.ai-notice {
+    background: #fff8ec;
+    border: 1px solid #ffe6b8;
+    border-radius: 6px;
+    padding: 10px 14px;
+    font-size: 13px;
+}
+.ai-notice-title {
+    font-weight: 500;
+    color: #b8780a;
+    margin-bottom: 4px;
+}
+.ai-notice-list {
+    margin: 0;
+    padding-left: 18px;
+    color: #6b6b6b;
+}
+.ai-notice-list li { margin-bottom: 2px; }
+.ai-notice-divider {
+    border-top: 1px solid #ffe6b8;
+    margin: 8px 0;
+}
+/* ai按钮css */
+.ai-btn {
+    position: relative;
+    border: 1px solid rgba(255,255,255,0.15);
+    color: #fff;
+    cursor: pointer;
+    overflow: hidden;
+    background: linear-gradient(
+            270deg,
+            #a855f7,
+            #ec4899,
+            #f59e0b,
+            #a855f7
+    );
+    background-size: 400% 400%;
+    animation: aiGradient 6s ease infinite;
+    box-shadow:
+            0 0 6px rgba(0, 245, 255, 0.5),
+            0 0 12px rgba(139, 92, 246, 0.35);
+
+    transition: all 0.3s ease;
+}
+.ai-btn::before {
+    content: '';
+    position: absolute;
+    top: 0;
+    left: -120%;
+    width: 80%;
+    height: 100%;
+    background: linear-gradient(
+            120deg,
+            transparent,
+            rgba(255,255,255,0.35),
+            transparent
+    );
+    transform: skewX(-25deg);
+    animation: shine 3s infinite;
+}
+.ai-btn:hover {
+    color: #fff;
+    transform: translateY(-2px);
+    box-shadow:
+            0 0 8px rgba(0, 245, 255, 0.8),
+            0 0 14px rgba(139, 92, 246, 0.6);
+}
+@keyframes aiGradient {
+    0% {
+        background-position: 0% 50%;
+    }
+    50% {
+        background-position: 100% 50%;
+    }
+    100% {
+        background-position: 0% 50%;
+    }
+}
+
+@keyframes shine {
+    0% {
+        left: -120%;
+    }
+    100% {
+        left: 150%;
+    }
+}
+
+.ai-section-label {
+    font-size: 13px;
+    font-weight: 500;
+    color: #555;
+    margin-bottom: 6px;
+}
+
+.ai-existing-files-list {
+    max-height: 160px;
+    overflow-y: auto;
+    border: 1px solid #eee;
+    border-radius: 6px;
+    padding: 4px 8px;
+}
+
+.ai-existing-file-item {
+    padding: 4px 0;
+    border-bottom: 1px solid #f5f5f5;
+}
+.ai-existing-file-item:last-child {
+    border-bottom: none;
+}
+.ai-existing-file-item label {
+    margin-bottom: 0;
+    cursor: pointer;
+}
+.attribute-scroll-area {
+    max-height: 180px;
+    overflow-y: auto;
+    overflow-x: hidden;
+    padding: 10px 10px 0;
+    border: 1px solid #dee2e6;
+    border-radius: 4px;
+    /*background: #fafafa;*/
+}
+
+.attribute-fields-group .btn {
+    text-align: left;
+}
+
+.attribute-fields-group .form-group {
+    margin-bottom: .75rem;
+}

BIN
app/public/images/juecedaping07.png


BIN
app/public/images/juecedaping08.png


+ 132 - 11
app/public/js/budget_compare.js

@@ -10,6 +10,7 @@ function customColDisplay () {
         { title: '施工图预算', fields: ['yu_dgn_qty', 'yu_dgn_price', 'yu_tp'], visible: true },
         { title: '招标预算', fields: ['zb_dgn_qty', 'zb_dgn_price', 'zb_tp'], visible: true },
         { title: '控制目标', fields: ['ctrl_dgn_qty', 'ctrl_dgn_price', 'ctrl_tp'], visible: false },
+        { title: '实际投资', fields: ['real_dgn_qty', 'real_dgn_price', 'real_tp'], visible: false },
         { title: '台账', fields: ['dgn_qty', 'dgn_price', 'total_price'], visible: true },
         { title: '预估决算', fields: ['final_dgn_qty', 'final_dgn_price', 'final_tp'], visible: true },
     ];
@@ -57,8 +58,9 @@ $(document).ready(() => {
         { key: 'yu', name: '施工图预算', dgn1: 'yu_dgn_qty1', dgn2: 'yu_dgn_qty2', tp: 'yu_tp'},
         { key: 'zb', name: '招标预算', dgn1: 'zb_dgn_qty1', dgn2: 'zb_dgn_qty2', tp: 'zb_tp'},
         { key: 'ctrl', name: '控制目标', dgn1: 'ctrl_dgn_qty1', dgn2: 'ctrl_dgn_qty2', tp: 'ctrl_tp'},
-        { key: 'tz', name: '台账', dgn1: 'dgn_qty1', dgn2: 'dgn_qty2', tp: 'total_price'},
-        { key: 'final', name: '预估决算', dgn1: 'final_dgn_qty1', dgn2: 'final_dgn_qty2', tp: 'final_tp'},
+        { key: 'real', name: '实际投资', dgn1: 'real_dgn_qty1', dgn2: 'real_dgn_qty2', tp: 'real_tp'},
+        { key: 'tz', name: '台账/合同', dgn1: 'dgn_qty1', dgn2: 'dgn_qty2', tp: 'total_price'},
+        { key: 'final', name: '计量/支付', dgn1: 'final_dgn_qty1', dgn2: 'final_dgn_qty2', tp: 'final_tp'},
     ];
     const spreadSetting = {
         cols: [
@@ -80,10 +82,13 @@ $(document).ready(() => {
             {title: '控制目标|数量1/数量2', colSpan: '3|1', rowSpan: '1|1', field: 'ctrl_dgn_qty', hAlign: 2, width: 80, bc_type: 'number'},
             {title: '|经济指标', colSpan: '|1', rowSpan: '|1', field: 'ctrl_dgn_price', hAlign: 2, width: 80, type: 'Number', bc_type: 'number'},
             {title: '|金额', colSpan: '|1', rowSpan: '|1', field: 'ctrl_tp', hAlign: 2, width: 80, type: 'Number', bc_type: 'number'},
-            {title: '台账|数量1/数量2', colSpan: '3|1', rowSpan: '1|1', field: 'dgn_qty', hAlign: 2, width: 80, bc_type: 'number', visible: false},
+            {title: '实际投资|数量1/数量2', colSpan: '3|1', rowSpan: '1|1', field: 'real_dgn_qty', hAlign: 2, width: 80, bc_type: 'number'},
+            {title: '|经济指标', colSpan: '|1', rowSpan: '|1', field: 'real_dgn_price', hAlign: 2, width: 80, type: 'Number', bc_type: 'number'},
+            {title: '|金额', colSpan: '|1', rowSpan: '|1', field: 'real_tp', hAlign: 2, width: 80, type: 'Number', bc_type: 'number'},
+            {title: '台账/合同|数量1/数量2', colSpan: '3|1', rowSpan: '1|1', field: 'dgn_qty', hAlign: 2, width: 80, bc_type: 'number', visible: false},
             {title: '|经济指标', colSpan: '|1', rowSpan: '|1', field: 'dgn_price', hAlign: 2, width: 80, type: 'Number', bc_type: 'number', visible: false},
             {title: '|金额', colSpan: '|1', rowSpan: '|1', field: 'total_price', hAlign: 2, width: 80, type: 'Number', bc_type: 'number', visible: false},
-            {title: '预估决算|数量1/数量2', colSpan: '3|1', rowSpan: '1|1', field: 'final_dgn_qty', hAlign: 2, width: 80, bc_type: 'number', visible: false},
+            {title: '计量/支付|数量1/数量2', colSpan: '3|1', rowSpan: '1|1', field: 'final_dgn_qty', hAlign: 2, width: 80, bc_type: 'number', visible: false},
             {title: '|经济指标', colSpan: '|1', rowSpan: '|1', field: 'final_dgn_price', hAlign: 2, width: 80, type: 'Number', bc_type: 'number', visible: false},
             {title: '|金额', colSpan: '|1', rowSpan: '|1', field: 'final_tp', hAlign: 2, width: 80, type: 'Number', bc_type: 'number', visible: false},
             {title: '数据对比', colSpan: '1', rowSpan: '2', field: 'stackedBar', hAlign: 0, width: 300, cellType: 'stackedBar', stackedBarCover: false, bc_type: 'grid', defaultVisible: true, visible: false, getTip: getStackedBarTip},
@@ -126,7 +131,7 @@ $(document).ready(() => {
             if (!phase2) phase2 = this.comparePhase2;
             const cp1 = comparePhase.find(x => { return x.key === phase1; });
             const cp2 = comparePhase.find(x => { return x.key === phase2; });
-            return `增减幅度=(${cp2.name}-${cp1.name})/${cp1.name}*100%`;
+            return `增减幅度=(${cp2.name}-${cp1.name})÷${cp1.name}*100%`;
         },
         setComparePhase(phase1, phase2) {
             compareObj.comparePhase1 = phase1;
@@ -218,6 +223,16 @@ $(document).ready(() => {
             });
             const setting = { id: 'tree_id', pid: 'tree_pid', order: 'order', level: 'level', rootId: -1, calcFields: ['total_price'] };
 
+            const realTree = createNewPathTree('ledger', setting);
+            realTree.loadDatas(result.real);
+            treeCalc.calculateAll(realTree);
+            compareTree.loadTree(realTree, function (cur, source) {
+                cur.base = true;
+                cur.real_dgn_qty1 = ZhCalc.add(cur.real_dgn_qty1, source.dgn_qty1);
+                cur.real_dgn_qty2 = ZhCalc.add(cur.real_dgn_qty2, source.dgn_qty2);
+                cur.real_tp = ZhCalc.add(cur.real_tp, source.total_price);
+            });
+
             const ctrlTree = createNewPathTree('ledger', setting);
             ctrlTree.loadDatas(result.ctrl);
             treeCalc.calculateAll(ctrlTree);
@@ -289,6 +304,10 @@ $(document).ready(() => {
                 node.ctrl_dgn_qty = node.ctrl_dgn_qty1
                     ? (node.ctrl_dgn_qty2 ? node.ctrl_dgn_qty1 + '/' + node.ctrl_dgn_qty2 : node.ctrl_dgn_qty1)
                     : (node.ctrl_dgn_qty2 ? '/' + node.ctrl_dgn_qty2 : '');
+                node.real_dgn_price = ZhCalc.div(node.real_tp, node.real_dgn_qty1, 2);
+                node.real_dgn_qty = node.real_dgn_qty1
+                    ? (node.real_dgn_qty2 ? node.real_dgn_qty1 + '/' + node.real_dgn_qty2 : node.real_dgn_qty1)
+                    : (node.real_dgn_qty2 ? '/' + node.real_dgn_qty2 : '');
             });
             compareTree.resortChildrenByCustom(function (x, y) {
                 const iCode = compareCode(x.code, y.code);
@@ -321,7 +340,7 @@ $(document).ready(() => {
             this.calcStackedBar(finalTree);
             this.reCalculateCompareData(finalTree);
             SpreadJsObj.loadSheetData(compareSheet, SpreadJsObj.DataType.Tree, finalTree);
-            if (sfSelect) sfSelect.reloadSelect(this.finalInfo.tender);
+            if (sfSelect) sfSelect.reloadSelect(this.finalInfo.tender, this.finalInfo.contract);
         },
         reCalculateCompareData(calcTree) {
             const tree = calcTree || compareSheet.zh_tree;
@@ -467,7 +486,7 @@ $(document).ready(() => {
             if (compareObj.finalInfo) {
                 this.selectTree.datas.forEach(x => {
                     x.selected = compareObj.finalInfo.tender.indexOf(x.tid + '') >= 0;
-                })
+                });
             }
             const sfSpreadSetting = {
                 cols: [
@@ -486,11 +505,37 @@ $(document).ready(() => {
                 selectedBackColor: '#fffacd',
                 readOnly: true,
             };
-            this.spread = SpreadJsObj.createNewSpread($('#sf-spread')[0]);
+            this.spread = SpreadJsObj.createNewSpread($('#sf-tender-spread')[0]);
             this.sheet = this.spread.getActiveSheet();
             SpreadJsObj.initSheet(this.sheet, sfSpreadSetting);
             SpreadJsObj.loadSheetData(this.sheet, SpreadJsObj.DataType.Tree, this.selectTree);
 
+            this.contractTree = createNewPathTree('base', { id: 'contract_id', pid: 'contract_pid', order: 'order', level: 'level', fullPath: 'full_path', rootId: -1 });
+            this.contractTree.loadDatas(contract);
+            if (compareObj.finalInfo) {
+                this.contractTree.datas.forEach(x => {
+                    x.selected = compareObj.finalInfo.contract.indexOf(x.id) >= 0;
+                });
+            }
+            this.contractSpread = SpreadJsObj.createNewSpread($('#sf-contract-spread')[0]);
+            this.contractSheet = this.contractSpread.getActiveSheet();
+            SpreadJsObj.initSheet(this.contractSheet, {
+                cols: [
+                    {title: '选择', colSpan: '1', rowSpan: '1', field: 'selected', hAlign: 1, width: 40, formatter: '@', cellType: 'checkbox'},
+                    {title: '编号', colSpan: '1', rowSpan: '1', field: 'code', hAlign: 0, width: 90, formatter: '@', cellType: 'tree'},
+                    {title: '合同编号', colSpan: '1', rowSpan: '1', field: 'c_code', hAlign: 0, width: 90, formatter: '@'},
+                    {title: '名称', colSpan: '1', rowSpan: '1', field: 'name', hAlign: 0, width: 180, formatter: '@'},
+                ],
+                emptyRows: 0,
+                headRows: 1,
+                headRowHeight: [32],
+                defaultRowHeight: 21,
+                headerFont: '12px 微软雅黑',
+                font: '12px 微软雅黑',
+                readOnly: true,
+            });
+            SpreadJsObj.loadSheetData(this.contractSheet, SpreadJsObj.DataType.Tree, this.contractTree);
+
             this.spread.bind(spreadNS.Events.ButtonClicked, function (e, info) {
                 if (!info.sheet.zh_setting) return;
 
@@ -502,6 +547,31 @@ $(document).ready(() => {
                 SpreadJsObj.reloadColData(info.sheet, 0);
             });
 
+            this.contractSpread.bind(spreadNS.Events.ButtonClicked, function (e, info) {
+                if (!info.sheet.zh_setting) return;
+
+                const col = info.sheet.zh_setting.cols[info.col];
+                if (col.field !== 'selected') return;
+
+                const node = SpreadJsObj.getSelectObject(info.sheet);
+                self.selectContract(node, !node[col.field]);
+                SpreadJsObj.reloadColData(info.sheet, 0);
+            });
+
+            $('[name=sf-tab]').click(function() {
+                const content = this.getAttribute('href');
+                setTimeout(function() {
+                    if (content === '#sf-contract') self.contractSpread.refresh();
+                    if (content === '#sf-tender') self.spread.refresh();
+                }, 200);
+            });
+            // 说是bootstrap v4有,但是不生效
+            // $('#sf-contract').on('shown.bs.tab', function() {
+            //     self.contractSpread.refresh();
+            // });
+            // $('#sf-tender').on('shown.bs.tab', function() {
+            //     self.spread.refresh();
+            // });
 
             $('#sf-select-all').click(function() {
                 for (const n of self.selectTree.nodes) {
@@ -512,18 +582,37 @@ $(document).ready(() => {
             $('#select-final-ok').click(() => {
                 const rela = self.getSelects();
                 if (rela.length === 0) return;
-                const data = { final_id: compareObj.curFinalId(), id: rela, final_type: $('[name=final_type]:checked').val() };
+                const checked = $('#tsm-filter-checked')[0].checked;
+                const stage = { type: $('[name=tsm-source]:checked').val(), checked };
+                if (stage.type === 'month') {
+                    stage.month = $('#gather-month').val();
+                    if (stage.month === '') {
+                        toastr.warning('请选择 汇总年月');
+                        return;
+                    }
+                } else if (stage.type === 'under-month') {
+                    stage.month = $('#gather-under-month').val();
+                    if (stage.month === '') {
+                        toastr.warning('请选择 汇总年月');
+                        return;
+                    }
+                }
+                const contracts = self.getSelectContracts();
+                const data = { final_id: compareObj.curFinalId(), id: rela, final_type: $('[name=final_type]:checked').val(), stage, contracts };
                 postData(window.location.pathname + '/final', data, function(result, msg) {
                     compareObj.loadFinalData(result, msg);
                     $('#select-final').modal('hide');
                 });
             });
         }
-        reloadSelect(select) {
+        reloadSelect(select, contract) {
             if (compareObj.finalInfo) {
                 this.selectTree.datas.forEach(x => {
                     x.selected = select.indexOf(x.tid + '') >= 0;
-                })
+                });
+                this.contractTree.datas.forEach(x => {
+                    x.selected = contract.indexOf(x.id) >= 0;
+                });
             }
             SpreadJsObj.reloadColData(this.sheet, 0);
         }
@@ -534,6 +623,19 @@ $(document).ready(() => {
                 p.selected = select;
             }
         }
+        selectContract(node, select) {
+            const posterity = this.contractTree.getPosterity(node);
+            posterity.unshift(node);
+            for (const p of posterity) {
+                if (p) p.selected = select;
+            }
+            if (select) {
+                const parents = this.contractTree.getAllParents(node);
+                for(const p of parents) {
+                    if (p) p.selected = select;
+                }
+            }
+        }
         getSelects() {
             const select = [];
             for (const n of this.selectTree.nodes) {
@@ -541,6 +643,10 @@ $(document).ready(() => {
             }
             return select;
         }
+        getSelectContracts() {
+            const select = this.contractTree.nodes.filter(x => { return !!x.selected; }).map(x => { return x.id; });
+            return select;
+        }
     }
 
     $('#select-final').on('shown.bs.modal', () => {
@@ -630,4 +736,19 @@ $(document).ready(() => {
         if (gridIndex >= 0) excelSetting.cols.splice(gridIndex, 1);
         SpreadExcelObj.exportSimpleXlsxSheet(excelSetting, compareSheet.zh_tree.nodes, $('.sidebar-title').attr('data-original-title') + "-造价对比.xlsx");
     });
+
+    $('[name=tsm-source]').click(function() {
+        $('[name=gather-type]').hide();
+        const type = this.value;
+        const gatherBy = $(`#gather-by-${type}`);
+        if (gatherBy.length > 0) {
+            $('#tsm-stage-info-detail').show();
+            $(`#gather-by-${type}`).show();
+        } else {
+            $('#tsm-stage-info-detail').hide();
+        }
+    });
+    $('.datepicker-here').datepicker({
+        autoClose: true,
+    });
 });

+ 10 - 0
app/public/js/budget_detail.js

@@ -879,6 +879,16 @@ $(document).ready(() => {
             $('#budget-set').modal('hide');
         })
     });
+    $('#budget-extra-info-ok').click(() => {
+        const updateData = {
+            optimize_rate: parseFloat($('#bei-optimize_rate').val()),
+            profit: parseFloat($('#bei-profit').val()),
+            btype: window.location.pathname.split('/')[4]
+        };
+        postData('extra-info', updateData, function(){
+            $('#budget-extra-info').modal('hide');
+        })
+    });
 
     $('#upload-ybp-file').click(function () {
         const file = $('#ybp-file')[0];

+ 523 - 78
app/public/js/contract_detail.js

@@ -153,7 +153,7 @@ $(document).ready(function() {
     };
     const contractTree = createNewPathTree('revise', treeSetting);
 
-    const contractDetail = ['c_code', 'name', 'total_price', 'pay_price', 'debit_price','yf_price', 'sf_price', 'party_a', 'party_a_user', 'party_b', 'party_b_user', 'sign_date', 'address', 'entity', 'bank', 'bank_account', 'remark', 'settle_code', 'tax', 'create_time'];
+    const contractDetail = ['c_code', 'name', 'price', 'supplement_price', 'total_price', 'pay_price', 'debit_price','yf_price', 'sf_price', 'party_a', 'party_a_user', 'party_b', 'party_b_user', 'sign_date', 'address', 'entity', 'bank', 'bank_account', 'remark', 'settle_code', 'tax', 'create_time'];
 
     // 清单 相关方法&绑定spreadjs事件
     const contractTreeSpreadObj = {
@@ -333,8 +333,10 @@ $(document).ready(function() {
             if ($('.bc-bar .nav li .active').attr('href') === '#htdetail') {
                 if (node.settle_code || (shenpi_status && node.status !== auditConst.status.uncheck && node.status !== auditConst.status.checkNo)) {
                     $('#edit_contract_btn').hide();
+                    $('#ai_edit_contract_btn').hide();
                 } else {
                     $('#edit_contract_btn').show();
+                    $('#ai_edit_contract_btn').show();
                 }
                 $('#save_contract_btn').hide();
                 $('#cancel_contract_btn').hide();
@@ -361,6 +363,7 @@ $(document).ready(function() {
                 }
             } else if ($('.bc-bar .nav li .active').attr('href') === '#htpay') {
                 $('#edit_contract_btn').hide();
+                $('#ai_edit_contract_btn').hide();
                 $('#save_contract_btn').hide();
                 $('#cancel_contract_btn').hide();
                 $('#add_contract_pay_btn').hide();
@@ -389,6 +392,7 @@ $(document).ready(function() {
                 $('a[href*="#cons-upfile"]').hide();
             } else if ($('.bc-bar .nav li .active').attr('href') === '#htfile') {
                 $('#edit_contract_btn').hide();
+                $('#ai_edit_contract_btn').hide();
                 $('#save_contract_btn').hide();
                 $('#cancel_contract_btn').hide();
                 $('#add_contract_pay_btn').hide();
@@ -396,6 +400,7 @@ $(document).ready(function() {
                 $('a[href*="#cons-upfile"]').show();
             } else if ($('.bc-bar .nav li .active').attr('href') === '#htsupplement') {
                 $('#edit_contract_btn').hide();
+                $('#ai_edit_contract_btn').hide();
                 $('#save_contract_btn').hide();
                 $('#cancel_contract_btn').hide();
                 $('#add_contract_pay_btn').hide();
@@ -498,6 +503,7 @@ $(document).ready(function() {
                     contractTreeSpreadObj.changeContractTab(node, true);
                 } else {
                     $('#edit_contract_btn').hide();
+                    $('#ai_edit_contract_btn').hide();
                     $('#save_contract_btn').hide();
                     $('#cancel_contract_btn').hide();
                     $('a[href*="#cons-upfile"]').hide();
@@ -524,6 +530,7 @@ $(document).ready(function() {
                 contractTreeSpreadObj.changeContractTab(node, true);
             } else {
                 $('#edit_contract_btn').hide();
+                $('#ai_edit_contract_btn').hide();
                 $('#save_contract_btn').hide();
                 $('#cancel_contract_btn').hide();
                 $('a[href*="#cons-unlock"]').hide();
@@ -669,6 +676,7 @@ $(document).ready(function() {
                 for (const attr of attributeSet) {
                     $('#htdetail_' + attr.field).text(attrInfo && attrInfo[attr.field] ? attrInfo[attr.field] : '');
                 }
+                $('#hidden_pay_condition').val(attrInfo && attrInfo.pay_condition ? attrInfo.pay_condition : '');
                 $('#htdetail_df_price').text(ZhCalc.sub(node.yf_price, node.sf_price) || '');
                 if (needPost) {
                     postData(window.location.pathname + '/update', {postType: 'get-contract', postData: node.id}, function (result) {
@@ -676,6 +684,7 @@ $(document).ready(function() {
                         contractTreeSpreadObj.refreshTree(contractSheet, refreshNode);
                         // contractTreeSpreadObj.setContractPays(result.pays, node);
                         contractTreeSpreadObj.setContractListHtml('pay', node, result.pays);
+                        node.files = result.files;
                         contractTreeSpreadObj.setContractFiles(result.files, node.id);
                         // contractTreeSpreadObj.setContractSupplements(result.supplements, node.id);
                         contractTreeSpreadObj.setContractListHtml('supplement', node, result.supplements);
@@ -689,6 +698,7 @@ $(document).ready(function() {
                 $('#htfile-table').hide();
                 $('#htsupplement-table').hide();
                 $('#edit_contract_btn').hide();
+                $('#ai_edit_contract_btn').hide();
                 $('#save_contract_btn').hide();
                 $('#cancel_contract_btn').hide();
                 $('a[href*="#cons-unlock"]').hide();
@@ -1835,34 +1845,58 @@ $(document).ready(function() {
         const node = SpreadJsObj.getSelectObject(contractSheet);
         console.log(node);
         if (node && node.c_code && (node.uid === user_id || permission_edit_contract) && (!shenpi_status || (shenpi_status && node.status === auditConst.status.uncheck || node.status === auditConst.status.checkNo))) {
-            $('#edit_contract_btn').hide();
-            $('#save_contract_btn').show();
-            $('#cancel_contract_btn').show();
-            for (const c of contractDetail) {
-                if ($('#htdetail_' + c) && $('#htdetail_' + c).hasClass('change-input-td')) {
-                    if (c === 'sign_date') {
-                        $('#htdetail_' + c).html('<input type="date" class="form-control form-control-sm" value="' + moment(node[c]).format('YYYY-MM-DD') + '">');
-                        continue;
-                    } else if (c === 'remark') {
-                        $('#htdetail_' + c).html('<textarea class="form-control form-control-sm" rows="3">' + node[c] + '</textarea>');
-                        continue;
-                    } else if (c === 'tax') {
-                        $('#htdetail_' + c).html('<input type="number" min="0" max="100" step="1" class="form-control form-control-sm" value="' + (node[c] ? node[c] : '') + '">');
-                        continue;
-                    }
-                    $('#htdetail_' + c).html('<input type="text" class="form-control form-control-sm" value="' + node[c] + '">');
-                }
-            }
-            const attrInfo = node.attribute_json ? JSON.parse(node.attribute_json) : null;
-            for (const attr of attributeSet) {
-                if (attr.type === 'long_text') {
-                    $('#htdetail_' + attr.field).html('<textarea class="form-control form-control-sm" rows="3">' + (attrInfo && attrInfo[attr.field] ? attrInfo[attr.field] : '') + '</textarea>');
+            editContract(node);
+        }
+    });
+
+    $('#ai_edit_contract_btn').on('click', function () {
+        const node = SpreadJsObj.getSelectObject(contractSheet);
+        console.log(node);
+        if (node && node.c_code && (node.uid === user_id || permission_edit_contract) && (!shenpi_status || (shenpi_status && node.status === auditConst.status.uncheck || node.status === auditConst.status.checkNo))) {
+            // files只要pdf,word和图片格式的
+            const files = _.filter(node.files, function (f) {
+                const ext = f.filename.toLowerCase().split('.').splice(-1)[0];
+                return ['pdf', 'doc', 'docx', 'jpg', 'jpeg', 'png', 'webp'].includes(ext);
+            });
+            openAiRecognizeModal({ mode: 'edit', cid: node.id, existingFiles: files });
+            editContract(node);
+        }
+    });
+
+    function editContract(node) {
+        $('#edit_contract_btn').hide();
+        $('#ai_edit_contract_btn').show();
+        $('#save_contract_btn').show();
+        $('#cancel_contract_btn').show();
+        for (const c of contractDetail) {
+            if ($('#htdetail_' + c) && $('#htdetail_' + c).hasClass('change-input-td')) {
+                if (c === 'sign_date') {
+                    $('#htdetail_' + c).html('<input type="date" class="form-control form-control-sm" value="' + moment(node[c]).format('YYYY-MM-DD') + '">');
+                    continue;
+                } else if (c === 'remark') {
+                    $('#htdetail_' + c).html('<textarea class="form-control form-control-sm" rows="3">' + node[c] + '</textarea>');
+                    continue;
+                } else if (c === 'tax') {
+                    $('#htdetail_' + c).html('<input type="number" min="0" max="100" step="1" class="form-control form-control-sm" value="' + (node[c] ? node[c] : '') + '">');
                     continue;
                 }
-                $('#htdetail_' + attr.field).html('<input type="' + attr.type + '" class="form-control form-control-sm" value="' + (attrInfo && attrInfo[attr.field] ? attrInfo[attr.field] : '') + '">');
+                $('#htdetail_' + c).html('<input type="text" class="form-control form-control-sm" value="' + node[c] + '">');
             }
         }
-    });
+        const attrInfo = node.attribute_json ? JSON.parse(node.attribute_json) : {};
+        let havePayCondition = false;
+        for (const attr of attributeSet) {
+            if (attr.field === 'pay_condition') havePayCondition = true;
+            if (attr.type === 'long_text') {
+                $('#htdetail_' + attr.field).html('<textarea class="form-control form-control-sm" rows="3">' + (attrInfo && attrInfo[attr.field] ? attrInfo[attr.field] : '') + '</textarea>');
+                continue;
+            }
+            $('#htdetail_' + attr.field).html('<input type="' + attr.type + '" class="form-control form-control-sm" value="' + (attrInfo && attrInfo[attr.field] ? attrInfo[attr.field] : '') + '">');
+        }
+        if (!havePayCondition) {
+            $('#hidden_pay_condition').val(attrInfo && attrInfo.pay_condition ? attrInfo.pay_condition : '');
+        }
+    }
 
     $('#save_contract_btn').on('click', function () {
         const node = SpreadJsObj.getSelectObject(contractSheet);
@@ -1882,14 +1916,15 @@ $(document).ready(function() {
                     data[c] = $('#htdetail_' + c + ' input').val();
                 }
             }
+            data.total_price = ZhCalc.add(node.supplement_price, parseFloat(data.price));
             if (data.c_code === '' || data.name === '') {
                 toastr.warning('合同编号和合同名称不能为空');
                 return;
             }
-            if (data.total_price === '') {
+            if (data.price === '') {
                 toastr.warning('合同金额不能为空');
                 return;
-            } else if (!/^\d+(\.\d+)?$/.test(data.total_price)) {
+            } else if (!/^\d+(\.\d+)?$/.test(data.price)) {
                 toastr.warning('合同金额只能输入数字');
                 return;
             }
@@ -1908,7 +1943,9 @@ $(document).ready(function() {
                 return;
             }
             const attrInfo = node.attribute_json ? JSON.parse(node.attribute_json) : {};
+            let havePayCondition = false;
             for (const attr of attributeSet) {
+                if (attr.field === 'pay_condition') havePayCondition = true;
                 if (attr.type === 'long_text') {
                     attrInfo[attr.field] = $('#htdetail_' + attr.field + ' textarea').val();
                     // 判断长度超过1000字提示
@@ -1934,16 +1971,22 @@ $(document).ready(function() {
                 }
                 attrInfo[attr.field] = $('#htdetail_' + attr.field + ' input').val();
             }
+            if (!havePayCondition) {
+                attrInfo.pay_condition = $('#hidden_pay_condition').val();
+            }
             data.attribute_json = JSON.stringify(attrInfo);
             // if (data.party_b === '') {
             //     toastr.warning('签订单位(乙方)不能为空');
             //     return;
             // }
+            console.log(data);
             // 更新至服务器
             postData(window.location.pathname + '/update', {postType: 'update-contract', postData: data}, function (result) {
                 toastr.success('已编辑成功');
-                const refreshNode = contractTree.loadPostData(result);
+                const refreshNode = contractTree.loadPostData({ update: result.contract });
                 contractTreeSpreadObj.refreshTree(contractSheet, refreshNode);
+                node.files = result.files;
+                contractTreeSpreadObj.setContractFiles(result.files, node.id);
                 const newNode = SpreadJsObj.getSelectObject(contractSheet);
                 contractTreeSpreadObj.changeContractTab(newNode, true);
             });
@@ -1954,6 +1997,7 @@ $(document).ready(function() {
         const node = SpreadJsObj.getSelectObject(contractSheet);
         if (node && node.c_code && (node.uid === user_id || permission_edit_contract)) {
             $('#edit_contract_btn').show();
+            $('#ai_edit_contract_btn').show();
             $('#save_contract_btn').hide();
             $('#cancel_contract_btn').hide();
             for (const c of contractDetail) {
@@ -1973,6 +2017,7 @@ $(document).ready(function() {
                 }
                 $('#htdetail_' + attr.field).text(attrInfo && attrInfo[attr.field] ? attrInfo[attr.field] : '');
             }
+            $('#hidden_pay_condition').val(attrInfo && attrInfo.pay_condition ? attrInfo.pay_condition : '');
         }
     });
 
@@ -2000,6 +2045,7 @@ $(document).ready(function() {
                 })
                 postDataWithFile(`${thisUrl}/${contractConst.typeMap[contract_type]}/${node.id}/file/upload`, formData, function (result) {
                     contractTreeSpreadObj.setContractFiles(result, node.id);
+                    node.files = result;
                     $('#file-modal').val('');
                     $('#file-cancel').click()
                 });
@@ -2013,6 +2059,7 @@ $(document).ready(function() {
         deleteAfterHint(function () {
             postData(`${thisUrl}/${contractConst.typeMap[contract_type]}/${node.id}/file/delete`, { id: fid }, function (result) {
                 contractTreeSpreadObj.setContractFiles(result, node.id);
+                node.files = result;
             });
         }, '确认删除该文件?');
     });
@@ -2188,6 +2235,17 @@ $(document).ready(function() {
             toastr.warning('已结算的合同不能删除补充合同');
             return;
         }
+        const supplementInfo = _.find(contractSupplements, { id: csid });
+        const oldPrice = supplementInfo ? supplementInfo.price : 0;
+        let supplement_price = ZhCalc.sum(_.map(contractSupplements, 'price'));
+        // data.price 汇总入supplement_price并去除原有值
+        supplement_price = ZhCalc.sub(supplement_price, oldPrice);
+        const total_price = ZhCalc.add(node.price, supplement_price);
+        if (parseFloat(total_price) < node.yf_price) {
+            const name = contract_type === contractConst.type.expenses ? '付' : '回';
+            toastr.error('不能删除,因删除后总合同金额汇总会小于累计应' + name + '金额 ' + node.yf_price);
+            return;
+        }
         deleteAfterHint(function () {
             postData(window.location.pathname + '/update', {postType: 'del-contract-supplement', postData: { select: node.id, supplement: csid }}, function (result) {
                 const refreshNode = contractTree.loadPostData(result.node);
@@ -2605,19 +2663,49 @@ $(document).ready(function() {
         });
     }
 
+    const edit_contract_input_key = ['c_code', 'name', 'tax', 'price', 'party_a', 'party_a_user', 'party_b', 'party_b_user', 'sign_date', 'address', 'entity', 'bank', 'bank_account'];
+    const signDate = $('#cons-add input[name="sign_date"]').datepicker().data('datepicker');
     if (permission_add) {
-        const signDate = $('#cons-add input[name="sign_date"]').datepicker().data('datepicker');
         $('#cons-add').on('show.bs.modal', function () {
-            $('#cons-add input[name="code"]').val('');
-            $('#cons-add input[name="name"]').val('');
-            $('#cons-add input[name="total_price"]').val('');
-            $('#cons-add input[name="party_a"]').val('');
-            $('#cons-add input[name="party_b"]').val('');
-            $('#cons-add input[name="sign_date"]').val('');
+            for (const key of edit_contract_input_key) {
+                $('#cons-add input[name="' + key + '"]').val('');
+            }
             signDate.selectDate('');
             $('#cons-add textarea[name="remark"]').val('');
+            // 附加属性也清空
+            for (const attr of attributeSet) {
+                if (attr.type === 'long_text') {
+                    $('#cons-add textarea[name="' + attr.field + '"]').val('');
+                    continue;
+                }
+                $('#cons-add input[name="' + attr.field + '"]').val('');
+            }
+            $('#cons-add textarea[name="pay_condition"]').val('');
+            // type选回第一个
+            $('#cons-add select[name="type"]').val($('#cons-add select[name="type"] option:first').val());
+            $('#contract-add-cid').val('');
+            $('#contract-add-fid').val('');
+            $('#contract-add-attach').prop('checked', false);
+            $('#ai-file-name').text('');
+            $('#ai-file-panel').hide();
         });
 
+        $('#contractAttrCollapse')
+            .on('show.bs.collapse', function () {
+                $(this)
+                    .prev()
+                    .find('.attr-collapse-icon')
+                    .removeClass('fa-chevron-down')
+                    .addClass('fa-chevron-up');
+            })
+            .on('hide.bs.collapse', function () {
+                $(this)
+                    .prev()
+                    .find('.attr-collapse-icon')
+                    .removeClass('fa-chevron-up')
+                    .addClass('fa-chevron-down');
+            });
+
         $('#add-contract').click(function () {
             const node = SpreadJsObj.getSelectObject(contractSheet);
             if (!node) {
@@ -2625,17 +2713,31 @@ $(document).ready(function() {
                 return;
             }
             console.log(node);
-            const data = {
-                code: $('#cons-add input[name="code"]').val(),
-                name: $('#cons-add input[name="name"]').val(),
-                total_price: $('#cons-add input[name="total_price"]').val(),
-                party_a: $('#cons-add input[name="party_a"]').val(),
-                party_b: $('#cons-add input[name="party_b"]').val(),
-                sign_date: $('#cons-add input[name="sign_date"]').val() || null,
-                remark: $('#cons-add textarea[name="remark"]').val(),
-                type: $('#cons-add select[name="type"]').val(),
-            }
-            if (!data.code) {
+            const data = {};
+            for (const key of edit_contract_input_key) {
+                data[key] = $('#cons-add input[name="'+ key +'"]').val();
+            }
+            const attribute_json = {};
+            let havePayCondition = false;
+            for (const attr of attributeSet) {
+                if (attr.field === 'pay_condition') havePayCondition = true;
+                if (attr.type === 'long_text') {
+                    attribute_json[attr.field] = $('#cons-add textarea[name="' + attr.field + '"]').val();
+                    continue;
+                }
+                attribute_json[attr.field] = $('#cons-add input[name="' + attr.field + '"]').val();
+            }
+            if (!havePayCondition) {
+                attribute_json.pay_condition = $('#cons-add textarea[name="pay_condition"]').val();
+            }
+            data.attribute_json = JSON.stringify(attribute_json);
+            const new_cid = $('#contract-add-cid').val();
+            data.cid = new_cid;
+            data.attach = $('#contract-add-attach').prop('checked');
+            data.fid = $('#contract-add-fid').val() ? parseInt($('#contract-add-fid').val()) : 0;
+            data.remark = $('#cons-add textarea[name="remark"]').val();
+            data.type = $('#cons-add select[name="type"]').val();
+            if (!data.c_code) {
                 toastr.error('请输入合同编号');
                 return;
             }
@@ -2643,12 +2745,12 @@ $(document).ready(function() {
                 toastr.error('请输入合同名称');
                 return;
             }
-            if (!data.total_price) {
+            if (!data.price) {
                 toastr.error('请输入合同金额');
                 return;
             }
             // 金额只能输入数字
-            if (!/^\d+(\.\d+)?$/.test(data.total_price)) {
+            if (!/^\d+(\.\d+)?$/.test(data.price)) {
                 toastr.error('合同金额只能输入数字');
                 return;
             }
@@ -2951,7 +3053,7 @@ $(document).ready(function() {
             address: $('#cons-addsupplement input[name="address"]').val(),
             content: $('#cons-addsupplement textarea[name="content"]').val(),
         }
-        if (!judgeSupplements(data)) {
+        if (!judgeSupplements(data, node)) {
             return;
         }
         console.log(node, data);
@@ -3001,7 +3103,7 @@ $(document).ready(function() {
             address: $('#cons-addsupplement input[name="address"]').val(),
             content: $('#cons-addsupplement textarea[name="content"]').val(),
         }
-        if (!judgeSupplements(data)) {
+        if (!judgeSupplements(data, node)) {
             return;
         }
         console.log(node, data);
@@ -3019,7 +3121,7 @@ $(document).ready(function() {
         })
     });
 
-    function judgeSupplements(data) {
+    function judgeSupplements(data, node) {
         let flag = true;
         if (!data.code) {
             toastr.error('请输入编号');
@@ -3029,7 +3131,6 @@ $(document).ready(function() {
             toastr.error('请输入名称');
             return false;
         }
-        console.log(data.price);
         if (data.price === '') {
             toastr.error('请输入金额');
             return false;
@@ -3039,6 +3140,17 @@ $(document).ready(function() {
             toastr.error('金额只能输入数字');
             return false;
         }
+        const oldPrice = data.id ? (_.find(contractSupplements, { id: data.id }) || {}).price : 0;
+        const newPrice = parseFloat(data.price);
+        let supplement_price = ZhCalc.sum(_.map(contractSupplements, 'price'));
+        // data.price 汇总入supplement_price并去除原有值
+        supplement_price = ZhCalc.add(ZhCalc.sub(supplement_price, oldPrice), newPrice);
+        const total_price = ZhCalc.add(node.price, supplement_price);
+        if (parseFloat(total_price) < node.yf_price) {
+            const name = contract_type === contractConst.type.expenses ? '付' : '回';
+            toastr.error('总合同金额汇总不能小于累计应' + name + '金额 ' + node.yf_price);
+            return;
+        }
         return flag;
     }
 
@@ -3194,24 +3306,24 @@ $(document).ready(function() {
             }
             shenpi_html += !reload ? `<td class="shenpi-td ${auditConst.auditProgressClass[pay.status]}">` : '';
             if ((pay.status === auditConst.status.uncheck || pay.status === auditConst.status.checkNo) && pay.uid === user_id) {
-                shenpi_html += `<a href="javascript:void(0);" node-cid="${pay.cid}" node-cpid="${pay.id}" class="btn ${auditConst.statusButtonClass[pay.status]} btn-sm show-sub-sp">${auditConst.statusButton[pay.status]}</a>`;
+                shenpi_html += `<a href="javascript:void(0);" node-cid="${pay.cid}" node-csid="${pay.id}" class="btn ${auditConst.statusButtonClass[pay.status]} btn-sm show-sub-sp">${auditConst.statusButton[pay.status]}</a>`;
             } else if ((pay.status === auditConst.status.checking || pay.status === auditConst.status.checkNoPre) && pay.curAuditors && pay.curAuditors.findIndex(x => { return x.aid === user_id; }) >= 0) {
-                shenpi_html += `<a href="javascript: void(0);" node-cid="${pay.cid}" node-cpid="${pay.id}" data-operate="checked" data-title="审批通过" class="btn btn-success btn-sm mr-2 show-sp-list">审批通过</a>`;
-                shenpi_html += `<a href="javascript: void(0);" node-cid="${pay.cid}" node-cpid="${pay.id}" data-operate="checkNo" data-title="审批退回" class="btn btn-warning btn-sm show-sp-list">审批退回</a>`;
+                shenpi_html += `<a href="javascript: void(0);" node-cid="${pay.cid}" node-csid="${pay.id}" data-operate="checked" data-title="审批通过" class="btn btn-success btn-sm mr-2 show-sp-list">审批通过</a>`;
+                shenpi_html += `<a href="javascript: void(0);" node-cid="${pay.cid}" node-csid="${pay.id}" data-operate="checkNo" data-title="审批退回" class="btn btn-warning btn-sm show-sp-list">审批退回</a>`;
             } else {
                 if (pay.status === auditConst.status.checked && pay.final_auditor_str) {
-                    shenpi_html += `<a href="javascript:void(0);" node-cid="${pay.cid}" node-cpid="${pay.id}" class="show-sp-list">${pay.final_auditor_str}</a>`;
+                    shenpi_html += `<a href="javascript:void(0);" node-cid="${pay.cid}" node-csid="${pay.id}" class="show-sp-list">${pay.final_auditor_str}</a>`;
                 } else if (pay.status === auditConst.status.checkNo && pay.curAuditors2 && pay.curAuditors2.length > 0) {
                     if (pay.curAuditors2[0].audit_type === auditType.key.common) {
-                        shenpi_html += `<a href="javascript:void(0);" node-cid="${pay.cid}" node-cpid="${pay.id}" class="show-sp-list">${pay.curAuditors2[0].name}${pay.curAuditors2[0].role !== '' && pay.curAuditors2[0].role !== null ? '-' + pay.curAuditors2[0].role : ''}</a>`;
+                        shenpi_html += `<a href="javascript:void(0);" node-cid="${pay.cid}" node-csid="${pay.id}" class="show-sp-list">${pay.curAuditors2[0].name}${pay.curAuditors2[0].role !== '' && pay.curAuditors2[0].role !== null ? '-' + pay.curAuditors2[0].role : ''}</a>`;
                     } else {
-                        shenpi_html += `<a href="javascript:void(0);" node-cid="${pay.cid}" node-cpid="${pay.id}" class="show-sp-list">${transFormToChinese(pay.curAuditors2[0].audit_order) + '审'}</a>`;
+                        shenpi_html += `<a href="javascript:void(0);" node-cid="${pay.cid}" node-csid="${pay.id}" class="show-sp-list">${transFormToChinese(pay.curAuditors2[0].audit_order) + '审'}</a>`;
                     }
                 } else if (pay.curAuditors.length > 0) {
                     if (pay.curAuditors[0].audit_type === auditType.key.common) {
-                        shenpi_html += `<a href="javascript:void(0);" node-cid="${pay.cid}" node-cpid="${pay.id}" class="show-sp-list">${pay.curAuditors[0].name}${pay.curAuditors[0].role !== '' && pay.curAuditors[0].role !== null ? '-' + pay.curAuditors[0].role : ''}</a>`;
+                        shenpi_html += `<a href="javascript:void(0);" node-cid="${pay.cid}" node-csid="${pay.id}" class="show-sp-list">${pay.curAuditors[0].name}${pay.curAuditors[0].role !== '' && pay.curAuditors[0].role !== null ? '-' + pay.curAuditors[0].role : ''}</a>`;
                     } else {
-                        shenpi_html += `<a href="javascript:void(0);" node-cid="${pay.cid}" node-cpid="${pay.id}" class="show-sp-list">${transFormToChinese(pay.curAuditors[0].audit_order) + '审'}</a>`;
+                        shenpi_html += `<a href="javascript:void(0);" node-cid="${pay.cid}" node-csid="${pay.id}" class="show-sp-list">${transFormToChinese(pay.curAuditors[0].audit_order) + '审'}</a>`;
                     }
                 }
                 shenpi_html += ` ${auditConst.auditProgress[pay.status]}`;
@@ -3225,7 +3337,8 @@ $(document).ready(function() {
         $('body').on('click', '.show-sub-sp', function () {
             console.log($(this).attr('node-cid'));
             let node = _.find(contractTree.nodes, { id: $(this).attr('node-cid') });
-            const cpid = $(this).attr('node-cpid');
+            const cpid = $(this).attr('node-cpid') || null;
+            const csid = $(this).attr('node-csid') || null;
             if (cpid) {
                 node = _.find(contractPays, { id: parseInt(cpid) });
                 $('#contract-add-group').hide();
@@ -3233,12 +3346,22 @@ $(document).ready(function() {
                 $('#change-pay-sp-group').val(node.sp_group);
                 $('#start-sp-cid').val(node.cid);
                 $('#start-sp-cpid').val(node.id);
+                $('#start-sp-csid').val('');
+            } else if (csid) {
+                node = _.find(contractSupplements, { id: parseInt(csid) });
+                $('#contract-add-group').show();
+                $('#pay-add-group').hide();
+                $('#change-sp-group').val(node.sp_group);
+                $('#start-sp-cid').val(node.cid);
+                $('#start-sp-csid').val(node.id);
+                $('#start-sp-cpid').val('');
             } else {
                 $('#contract-add-group').show();
                 $('#pay-add-group').hide();
                 $('#change-sp-group').val(node.sp_group);
                 $('#start-sp-cid').val(node.id);
                 $('#start-sp-cpid').val('');
+                $('#start-sp-csid').val('');
             }
             if (!node) {
                 toastr.error('合同数据获取失败');
@@ -3251,14 +3374,22 @@ $(document).ready(function() {
 
         $('body').on('click', '.show-sp-list', function () {
             let node = _.find(contractTree.nodes, { id: $(this).attr('node-cid') });
-            const cpid = $(this).attr('node-cpid');
+            const cpid = $(this).attr('node-cpid') || null;
+            const csid = $(this).attr('node-csid') || null;
             if (cpid) {
                 node = _.find(contractPays, { id: parseInt(cpid) });
                 $('#check-sp-cid').val(node.cid);
                 $('#check-sp-cpid').val(node.id);
+                $('#check-sp-csid').val('');
+            } else if (csid) {
+                node = _.find(contractSupplements, { id: parseInt(csid) });
+                $('#check-sp-cid').val(node.cid);
+                $('#check-sp-csid').val(node.id);
+                $('#check-sp-cpid').val('');
             } else {
                 $('#check-sp-cid').val(node.id);
                 $('#check-sp-cpid').val('');
+                $('#check-sp-csid').val('');
             }
             if (!node) {
                 toastr.error('合同数据获取失败');
@@ -3319,15 +3450,21 @@ $(document).ready(function() {
         function getNode(from = 'start') {
             const cid = $('#'+ from +'-sp-cid').val();
             const cpid = $('#'+ from +'-sp-cpid').val();
+            const csid = $('#'+ from +'-sp-csid').val();
+            let type = 'contract';
             let node = _.find(contractTree.nodes, { id: cid });
             if (cpid) {
                 node = _.find(contractPays, { id: parseInt(cpid) });
+                type = 'pay';
+            } else if (csid) {
+                node = _.find(contractSupplements, { id: parseInt(csid) });
+                type = 'supplement';
             }
             if (!node) {
                 toastr.error('合同数据获取失败');
                 return;
             }
-            return node;
+            return [node, type];
         }
 
         function setStartAuditsHtml(node) {
@@ -3568,20 +3705,20 @@ $(document).ready(function() {
 
         // 添加到审批流程中
         $('.search-user-list').on('click', 'dd', function () {
-            const node = getNode();
+            const [node, type] = getNode();
             const id = parseInt($(this).data('id'));
             if (id) {
-                postData(window.location.pathname + '/check', {postType: 'add-audit', id: node.id, auditorId: id}, function (result) {
+                postData(window.location.pathname + '/check', {postType: 'add-audit', id: node.id, type, auditorId: id}, function (result) {
                     setStartAuditChange(node, result);
                 });
             }
         });
         // 删除审批人
         $('body').on('click', '#auditors li a', function () {
-            const node = getNode();
+            const [node, type] = getNode();
             const li = $(this).parents('li');
             const data = {
-                postType: 'del-audit', id: node.id,
+                postType: 'del-audit', id: node.id, type,
                 auditorId: parseInt(li.attr('auditorId')),
             };
             postData(window.location.pathname + '/check', data, (result) => {
@@ -3590,15 +3727,15 @@ $(document).ready(function() {
         });
 
         $('#change-sp-group').change(function () {
-            const node = getNode();
-            postData(window.location.pathname + '/check', {postType: 'change-sp', id: node.id, sp_group: $(this).val()}, function (result) {
+            const [node, type] = getNode();
+            postData(window.location.pathname + '/check', {postType: 'change-sp', id: node.id, type, sp_group: $(this).val()}, function (result) {
                 setStartAuditChange(node, result);
             });
         });
 
         $('#change-pay-sp-group').change(function () {
-            const node = getNode();
-            postData(window.location.pathname + '/check', {postType: 'change-sp', id: node.id, sp_group: $(this).val()}, function (result) {
+            const [node, type] = getNode();
+            postData(window.location.pathname + '/check', {postType: 'change-sp', id: node.id, type, sp_group: $(this).val()}, function (result) {
                 setStartAuditChange(node, result);
             });
         });
@@ -3614,15 +3751,15 @@ $(document).ready(function() {
         }
 
         $('#start-sp-btn').click(function () {
-            const node = getNode();
-            postData(window.location.pathname + '/check', {postType: 'start', id: node.id}, function (result) {
-                setShenpiChange(node, result);
+            const [node, type] = getNode();
+            postData(window.location.pathname + '/check', {postType: 'start', id: node.id, type}, function (result) {
+                setShenpiChange(node, result, type);
                 $('#sub-sp').modal('hide');
             });
         });
 
         $('body').on('click', '#check-sp-btn', function () {
-            const node = getNode('check');
+            const [node, type] = getNode('check');
             const operate = $('#check-operate').val();
             console.log(node, operate);
             const checkType = operate === 'checked' ? auditConst.status.checked : parseInt($('#audit-list').find('input[name="checkType"]:checked').val());
@@ -3635,17 +3772,18 @@ $(document).ready(function() {
                 postType: 'check',
                 id: node.id,
                 checkType,
+                type,
                 opinion: $('#audit-list').find('textarea').val().replace(/\r\n/g, '<br/>').replace(/\n/g, '<br/>').replace(/\s/g, ' '),
             }
             console.log(data);
             postData(window.location.pathname + '/check', data, function (result) {
-                setShenpiChange(node, result);
+                setShenpiChange(node, result, type);
                 $('#sp-list').modal('hide');
             });
         });
 
-        function setShenpiChange(node, result) {
-            if (node.cid) {
+        function setShenpiChange(node, result, type) {
+            if (type === 'pay') {
                 contractPays.splice(contractPays.findIndex(item => item.id === node.id), 1, result);
                 const shenpi_html = setPayShenpiHtml(result, true);
                 $("#htpay-table tbody").find(`tr[data-cpid="${node.id}"]`).children('.shenpi-td').html(shenpi_html);
@@ -3656,6 +3794,17 @@ $(document).ready(function() {
                     $('#htpay-table tbody').find(`tr[data-cpid="${node.id}"]`).find('.pay-edit').show();
                     $('#htpay-table tbody').find(`tr[data-cpid="${node.id}"]`).find('.pay-del').show();
                 }
+            } else if (type === 'supplement') {
+                contractSupplements.splice(contractSupplements.findIndex(item => item.id === node.id), 1, result);
+                const shenpi_html = setSupplementShenpiHtml(result, true);
+                $("#htsupplement-table tbody").find(`tr[data-csid="${node.id}"]`).children('.shenpi-td').html(shenpi_html);
+                $("#htsupplement-table tbody").find(`tr[data-csid="${node.id}"]`).children('.shenpi-td').addClass(auditConst.auditProgressClass[result.status]);
+                $('#htsupplement-table tbody').find(`tr[data-csid="${node.id}"]`).find('.supplement-edit').hide();
+                $('#htsupplement-table tbody').find(`tr[data-csid="${node.id}"]`).find('.supplement-del').hide();
+                if (node.uid === user_id && (result.status === auditConst.status.uncheck || result.status === auditConst.status.checkNo)) {
+                    $('#htsupplement-table tbody').find(`tr[data-csid="${node.id}"]`).find('.supplement-edit').show();
+                    $('#htsupplement-table tbody').find(`tr[data-csid="${node.id}"]`).find('.supplement-del').show();
+                }
             } else {
                 const refreshNode = contractTree.loadPostData({update: result});
                 contractTreeSpreadObj.refreshTree(contractSheet, refreshNode);
@@ -3846,6 +3995,302 @@ $(document).ready(function() {
             if (stdXmj) stdXmj.spread.refresh();
         }
     });
+
+    $('#aiFillBtn').on('click', function () {
+        openAiRecognizeModal({ mode: 'create' });
+    });
+
+    // 弹窗状态:mode区分新建/编辑,cid是合同id(编辑时已有,新建时上传成功后才拿到)
+    const aiModalState = {
+        mode: 'create',       // 'create' | 'edit'
+        cid: null,
+        existingFiles: [],     // 编辑模式下,该合同已有的文件列表 [{ id, filename }, ...]
+    };
+
+    let aiSelectedFile = null; // 用户本次上传选择的新文件
+
+    /**
+     * 打开AI识别弹窗的统一入口
+     * @param {Object} options
+     *   mode: 'create' | 'edit'
+     *   cid: 编辑模式下传入当前合同id;新建模式不传
+     *   existingFiles: 编辑模式下传入该合同已有的文件列表 [{ id, filename }],新建模式不传或传空数组
+     *
+     * 调用示例:
+     *   新建合同弹窗的AI按钮: openAiRecognizeModal({ mode: 'create' })
+     *   编辑合同弹窗的AI按钮: openAiRecognizeModal({ mode: 'edit', cid: contractId, existingFiles: attList })
+     */
+    function openAiRecognizeModal(options) {
+        options = options || {};
+        aiModalState.mode = options.mode || 'create';
+        aiModalState.cid = options.cid || null;
+        aiModalState.existingFiles = options.existingFiles || [];
+
+        resetAiModalUI();
+        renderExistingFilesList();
+        $('#ai-recognize-modal').modal('show');
+    }
+
+    function resetAiModalUI() {
+        // 重置文件上传状态
+        clearUploadedFileUI();
+
+        // 重置单选状态
+        $('input[name="aiFileSource"]').prop('checked', false);
+
+        // 重置勾选项(默认勾选,但实际是否显示由renderExistingFilesList后续逻辑控制)
+        $('#aiAttachToContract').prop('checked', true);
+        $('#aiAttachCheckArea').hide();
+
+        // 重置识别按钮状态
+        $('#aiStartRecognize').prop('disabled', true);
+        resetSubmitBtnUI($('#aiStartRecognize'));
+    }
+
+    function renderExistingFilesList() {
+        const hasExisting = aiModalState.mode === 'edit' && aiModalState.existingFiles.length > 0;
+
+        if (!hasExisting) {
+            $('#aiExistingFilesArea').hide();
+            $('#aiUploadSectionLabel').hide();
+            $('#aiExistingFilesList').empty();
+            return;
+        }
+
+        $('#aiUploadSectionLabel').show();
+        const $list = $('#aiExistingFilesList').empty();
+
+        aiModalState.existingFiles.forEach(function (file) {
+            const radioId = 'aiExistingFile_' + file.id;
+            const $item = $(`
+      <div class="ai-existing-file-item">
+        <div class="form-check">
+          <input type="radio" name="aiFileSource" class="form-check-input" id="${radioId}" value="${file.id}">
+          <label class="form-check-label" for="${radioId}">
+            <i class="fa-solid fa-file-lines"></i> ${file.filename}
+          </label>
+        </div>
+      </div>
+    `);
+            $list.append($item);
+        });
+
+        $('#aiExistingFilesArea').show();
+    }
+
+// 选中"已有文件"中的某一项
+    $(document).on('change', 'input[name="aiFileSource"]', function () {
+        const selectedValue = $(this).val();
+
+        if (selectedValue === 'upload') {
+            $('#aiAttachCheckArea').show(); // 新建、编辑场景下,只要是"上传新文件"都显示这个勾选
+            $('#aiStartRecognize').prop('disabled', !aiSelectedFile);
+        } else {
+            // 用户选中了某个已有文件,清空本次上传的新文件状态
+            clearUploadedFileUI();
+            $('#aiAttachCheckArea').hide(); // 已有文件本身已经在合同文件里,不需要"是否添加"这个勾选
+            $('#aiStartRecognize').prop('disabled', false);
+        }
+    });
+
+// 点击上传区域,触发文件选择
+    $('#aiUploadArea').on('click', function (e) {
+        // file input 冒泡上来的点击,不处理
+        if (e.target.id === 'aiFileInput') {
+            return;
+        }
+
+        $('#aiFileInput')[0].click();
+    });
+
+// 拖拽上传
+    $('#aiUploadArea').on('dragover', function (e) {
+        e.preventDefault();
+        $(this).addClass('dragover');
+    }).on('dragleave', function () {
+        $(this).removeClass('dragover');
+    }).on('drop', function (e) {
+        e.preventDefault();
+        $(this).removeClass('dragover');
+        const file = e.originalEvent.dataTransfer.files[0];
+        if (file) handleAiFileSelected(file);
+    });
+
+// 选择文件
+    $('#aiFileInput').on('change', function () {
+        const file = this.files[0];
+        if (file) handleAiFileSelected(file);
+    });
+
+    function handleAiFileSelected(file) {
+        aiSelectedFile = file;
+        $('#aiFileName').text(file.name);
+        $('#aiUploadPlaceholder').hide();
+        $('#aiUploadResult').show();
+        $('#aiAttachCheckArea').show(); // 新建场景下,选择文件时也要显示这个勾选框
+
+        // 选中"上传新文件"这个隐藏radio,与已有文件选项互斥
+        $('#aiSourceUpload').prop('checked', true).trigger('change');
+    }
+
+// 移除已选文件
+    $('#aiFileRemove').on('click', function (e) {
+        e.stopPropagation();
+        clearUploadedFileUI();
+        $('#aiStartRecognize').prop('disabled', true);
+    });
+
+    function clearUploadedFileUI() {
+        aiSelectedFile = null;
+        $('#aiFileInput').val('');
+        $('#aiUploadResult').hide();
+        $('#aiUploadPlaceholder').show();
+    }
+
+// 点击「开始识别」
+    $('#aiStartRecognize').on('click', function () {
+        const $btn = $(this);
+        const selectedSource = $('input[name="aiFileSource"]:checked').val();
+        const usingExisting = selectedSource && selectedSource !== 'upload';
+        const usingUpload = selectedSource === 'upload' && aiSelectedFile;
+
+        if (!usingExisting && !usingUpload) return;
+
+        $btn.prop('disabled', true);
+        $('#aiRecognizeBtnText').text('识别中...');
+        $('#aiRecognizeSpinner').show();
+
+        if (usingExisting) {
+            // 使用已有文件 -> 走 editAndAi
+            postData(window.location.pathname + '/ai/edit', { id: aiModalState.cid, fid: selectedSource }, function (res) {
+                handleAiRecognizeSuccess(res);
+                resetSubmitBtnUI($btn);
+            }, function (err) {
+                handleAiRecognizeError(err);
+            });
+        } else {
+            // 上传新文件 -> 走 uploadAndAi
+            const formData = new FormData();
+            if (aiSelectedFile === undefined) {
+                toastr.error('未选择上传文件!');
+                resetSubmitBtnUI($btn);
+                return false;
+            }
+            const filesize = aiSelectedFile.size;
+            if (filesize > 50 * 1024 * 1024) {
+                toastr.error('文件大小过大!');
+                resetSubmitBtnUI($btn);
+                return false;
+            }
+            const fileext = aiSelectedFile.name.toLowerCase().split('.').splice(-1)[0];
+            if (['pdf', 'doc', 'docx', 'jpg', 'jpeg', 'png', 'webp'].indexOf(fileext) === -1) {
+                toastr.error('只能上传PDF、Word、图片格式的合同文件!');
+                resetSubmitBtnUI($btn);
+                return false;
+            }
+            if (aiModalState.cid) {
+                formData.append('cid', aiModalState.cid);
+            }
+            formData.append('attachToContract', $('#aiAttachToContract').is(':checked') ? '1' : '0');
+            formData.append('size', filesize);
+            formData.append('file', aiSelectedFile);
+            // 新建模式不传uuid,后端会自动生成并在返回结果里带回cid
+            postDataWithFile(window.location.pathname + '/ai/upload', formData, function (res) {
+                handleAiRecognizeSuccess(res);
+                resetSubmitBtnUI($btn);
+            }, function (err) {
+                handleAiRecognizeError(err);
+            });
+        }
+    });
+
+    function resetSubmitBtnUI($btn) {
+        $btn.prop('disabled', false);
+        $('#aiRecognizeBtnText').text('开始识别');
+        $('#aiRecognizeSpinner').hide();
+    }
+
+    function handleAiRecognizeSuccess(res) {
+        const result = res;
+        const data = result.res || {};
+        if (aiModalState.mode === 'create') {
+            for (const key of edit_contract_input_key) {
+                $('#cons-add input[name="' + key + '"]').val(data[key] || '');
+            }
+            signDate.selectDate(data && data.sign_date ? new Date(moment(data.sign_date).format('YYYY-MM-DD')) : '');
+            $('#cons-add textarea[name="pay_condition"]').val(data.pay_condition || '');
+            $('#contract-add-cid').val(result.cid);
+            if (result.file) {
+                $('#contract-add-fid').val(result.file.id || '');
+                $('#contract-add-attach').prop('checked', result.attached || false);
+                $('#ai-file-name').text(result.file.filename);   // 例如:合同解析结果.md
+                $('#ai-file-panel').show();
+            } else {
+                $('#contract-add-fid').val('');
+                $('#contract-add-attach').prop('checked', false);
+                $('#ai-file-name').text('');
+                $('#ai-file-panel').hide();
+            }
+        } else {
+            const node = SpreadJsObj.getSelectObject(contractSheet);
+            if (node && node.c_code && (node.uid === user_id || permission_edit_contract) && (!shenpi_status || (shenpi_status && node.status === auditConst.status.uncheck || node.status === auditConst.status.checkNo))) {
+                $('#edit_contract_btn').hide();
+                $('#save_contract_btn').show();
+                $('#cancel_contract_btn').show();
+                for (const c of contractDetail) {
+                    if ($('#htdetail_' + c) && $('#htdetail_' + c).hasClass('change-input-td')) {
+                        if (c === 'sign_date') {
+                            $('#htdetail_' + c).html('<input type="date" class="form-control form-control-sm" value="' + moment(data[c]).format('YYYY-MM-DD') + '">');
+                            continue;
+                        } else if (c === 'remark') {
+                            $('#htdetail_' + c).html('<textarea class="form-control form-control-sm" rows="3">' + node[c] + '</textarea>');
+                            continue;
+                        } else if (c === 'tax') {
+                            $('#htdetail_' + c).html('<input type="number" min="0" max="100" step="1" class="form-control form-control-sm" value="' + (data[c] ? data[c] : '') + '">');
+                            continue;
+                        }
+                        $('#htdetail_' + c).html('<input type="text" class="form-control form-control-sm" value="' + data[c] + '">');
+                    }
+                }
+                const attrInfo = node.attribute_json ? JSON.parse(node.attribute_json) : null;
+                let havePayCondition = false;
+                for (const attr of attributeSet) {
+                    if (attr.field === 'pay_condition') {
+                        havePayCondition = true;
+                        $('#htdetail_' + attr.field).html('<textarea class="form-control form-control-sm" rows="3">' + (data && data[attr.field] ? data[attr.field] : '') + '</textarea>');
+                        continue;
+                    } else if (attr.type === 'long_text') {
+                        $('#htdetail_' + attr.field).html('<textarea class="form-control form-control-sm" rows="3">' + (attrInfo && attrInfo[attr.field] ? attrInfo[attr.field] : '') + '</textarea>');
+                        continue;
+                    }
+                    $('#htdetail_' + attr.field).html('<input type="' + attr.type + '" class="form-control form-control-sm" value="' + (attrInfo && attrInfo[attr.field] ? attrInfo[attr.field] : '') + '">');
+                }
+                if (!havePayCondition) {
+                    $('#hidden_pay_condition').val(data && data.pay_condition ? data.pay_condition : '');
+                }
+            }
+            if (result.file) {
+                // 插入到files最前面
+                node.files.unshift(result.file);
+                contractTreeSpreadObj.setContractFiles(node.files, node.id);
+            }
+        }
+
+        $('#ai-recognize-modal').modal('hide');
+    }
+
+    function handleAiRecognizeError() {
+        toastr.error('识别失败,请重试或手动填写');
+    }
+
+// 弹窗关闭时重置状态(但不清空已回填的表单内容)
+    $('#ai-recognize-modal').on('hidden.bs.modal', function () {
+        clearUploadedFileUI();
+        $('input[name="aiFileSource"]').prop('checked', false);
+        $('#aiAttachCheckArea').hide();
+        $('#aiStartRecognize').prop('disabled', true);
+        resetSubmitBtnUI($('#aiStartRecognize'));
+    });
 });
 /**
  * 校验文件大小、格式

+ 340 - 0
app/public/js/cost_compare.js

@@ -0,0 +1,340 @@
+$(document).ready(() => {
+    autoFlashHeight();
+    const compareUtils = {
+        refreshSpreadSetting(roles, spreadSetting) {
+            spreadSetting.cols = [];
+            spreadSetting.fieldSufs = [];
+            for (const col of spreadSetting.baseCols) {
+                spreadSetting.cols.push(col);
+            }
+            for (const role of roles) {
+                spreadSetting.fieldSufs.push(role.active_order);
+                for (const ec of spreadSetting.extraCols) {
+                    const col = JSON.parse(JSON.stringify(ec));
+                    col.title = _.replace(col.title, '%s', role.name);
+                    col.field = col.field + '_' + role.active_order;
+                    spreadSetting.cols.push(col);
+                }
+            }
+        },
+        analysisCompareData(datas, roles, extraCols){
+            const findHis = function (role, history) {
+                let his = null;
+                for (const h of history) {
+                    if (h.audit_times < role.audit_times || (h.audit_times === role.audit_times && h.active_order <= role.active_order)) {
+                        his = h;
+                    } else {
+                        break;
+                    }
+                }
+                return his;
+            };
+            for (const d of datas) {
+                if (d.tree_id && !d.tree_is_leaf) continue;
+                if (!d.calc_his) d.calc_his = [];
+                d.calc_his.sort((x, y) => { return x.audit_times === y.audit_times ? x.active_order - y.active_order : x.audit_times - y.audit_times; });
+                for (const r of roles) {
+                    if (r.latest) {
+                        for (const col of extraCols){
+                            d[col.field + '_' + r.active_order] = d[col.field];
+                        }
+                    } else {
+                        const rHis = findHis(r, d.calc_his);
+                        if (rHis) {
+                            for (const col of extraCols){
+                                d[col.field + '_' + r.active_order] = rHis[col.field];
+                            }
+                        }
+                    }
+                }
+            }
+        },
+        filterSameRoles(roles) {
+            const result = [];
+            for (const af of roles) {
+                const naf = roles.find(x => { return x.audit_order === af.audit_order && x.active_order > af.active_order; });
+                if (!naf) result.push(af);
+            }
+            return result;
+        }
+    };
+    const converseSpreadSetting = function(setting) {
+        setting.baseCols = [];
+        setting.extraCols = [];
+        let firstExtraCol;
+        for (const col of setting.cols) {
+            if (calcFields.indexOf(col.field) < 0) {
+                if (firstExtraCol) continue;
+                col.rowSpan = (setting.headRows + 1) + '';
+                setting.baseCols.push(col);
+            } else {
+                if (!firstExtraCol) {
+                    firstExtraCol = col;
+                    col.title = '%s|' + col.title;
+                    col.rowSpan = '1|' + col.rowSpan;
+                } else {
+                    col.title = '|' + col.title;
+                    col.colSpan = '|' + col.colSpan;
+                    col.rowSpan = '|' + col.rowSpan;
+                }
+                setting.extraCols.push(col);
+            }
+        }
+        firstExtraCol.colSpan = setting.extraCols.length + '|' + firstExtraCol.colSpan;
+        setting.headRows = setting.headRows + 1;
+        setting.headRowHeight.unshift(setting.headRowHeight[0]);
+        setting.cols = [...setting.baseCols];
+        return setting;
+    };
+    class BillsObj {
+        constructor() {
+            this.spread = SpreadJsObj.createNewSpread($('#bills-spread')[0]);
+            this.sheet = this.spread.getActiveSheet();
+            this.treeSetting = {
+                id: 'tree_id',
+                pid: 'tree_pid',
+                order: 'tree_order',
+                level: 'tree_level',
+                isLeaf: 'tree_is_leaf',
+                fullPath: 'tree_full_path',
+                rootId: -1,
+                calcFields: ['yf_tp', 'sf_tp', 'pay_tp', 'cut_tp', 'yf_excl_tax_tp', 'sf_excl_tax_tp', 'end_yf_tp', 'end_sf_tp', 'end_pay_tp', 'end_cut_tp', 'end_yf_excl_tax_tp', 'end_sf_excl_tax_tp'],
+                keys: ['id', 'stage_id', 'tree_id'],
+            };
+            this.tree = createNewPathTree('ledger', this.treeSetting);
+            this.spreadSetting = converseSpreadSetting(bllsSpreadSetting);
+            sjsSettingObj.setFxTreeStyle(this.spreadSetting, sjsSettingObj.FxTreeStyle.phasePay);
+            this.ckBillsSpread = window.location.pathname + '-billsSelect';
+
+            this.initSpread();
+        }
+        initSpread() {
+            SpreadJsObj.initSheet(this.sheet, this.spreadSetting);
+            this.spread.bind(spreadNS.Events.SelectionChanged, this.selectionChanged);
+            this.spread.bind(spreadNS.Events.topRowChanged, this.topRowChanged);
+        }
+        loadRelaData() {
+            SpreadJsObj.saveTopAndSelect(this.sheet, this.ckBillsSpread);
+            const select = SpreadJsObj.getSelectObject(this.sheet);
+            detailObj.loadDetailData(select);
+        }
+        initSpreadSetting(roles = []) {
+            compareUtils.refreshSpreadSetting(roles, this.spreadSetting);
+            SpreadJsObj.initSheet(this.sheet, this.spreadSetting);
+        }
+        loadData(datas, roles) {
+            this.initSpreadSetting(roles);
+            compareUtils.analysisCompareData(datas, roles, this.spreadSetting.extraCols);
+            this.tree.loadDatas(datas);
+            this.tree.setting.calcFields = [];
+            for (const r of roles) {
+                for (const col of this.spreadSetting.extraCols) {
+                    this.tree.setting.calcFields.push(col.field + '_' + r.active_order);
+                }
+            }
+            treeCalc.calculateAll(this.tree);
+            SpreadJsObj.loadSheetData(this.sheet, SpreadJsObj.DataType.Tree, this.tree);
+            SpreadJsObj.loadTopAndSelect(this.sheet, this.ckBillsSpread);
+        }
+        // 事件
+        selectionChanged(e, info) {
+            if (info.newSelections) {
+                if (!info.oldSelections || info.newSelections[0].row !== info.oldSelections[0].row) {
+                    billsObj.loadRelaData();
+                }
+            }
+        }
+        topRowChanged(e, info) {
+            SpreadJsObj.saveTopAndSelect(info.sheet, billsObj.ckBillsSpread);
+        }
+    }
+    const billsObj = new BillsObj();
+    class DetailObj {
+        constructor() {
+            this.spread = SpreadJsObj.createNewSpread($('#detail-spread')[0]);
+            this.sheet = this.spread.getActiveSheet();
+            this.data = createAncillaryGcl({ id: 'id', masterId: 'ledger_id', sort: [['d_order', 'asc']] });
+            this.dealSpreadSetting = converseSpreadSetting(dealSpreadSetting);
+            this.commonSpreadSetting = converseSpreadSetting(commonSpreadSetting);
+
+            this.initSpread();
+        }
+        initSpread() {
+            SpreadJsObj.initSheet(this.sheet, this.commonSpreadSetting);
+        }
+        refreshSpreadSetting(roles, spreadSetting) {
+            spreadSetting.cols = [];
+            spreadSetting.fieldSufs = [];
+            for (const col of spreadSetting.baseCols) {
+                spreadSetting.cols.push(col);
+            }
+            for (const role of roles) {
+                spreadSetting.fieldSufs.push(role.active_order);
+                for (const ec of spreadSetting.extraCols) {
+                    const col = JSON.parse(JSON.stringify(ec));
+                    col.title = _.replace(col.title, '%s', role.name);
+                    col.field = col.field + '_' + role.active_order;
+                    spreadSetting.cols.push(col);
+                }
+            }
+        }
+        initSpreadSetting(roles = []) {
+            this.refreshSpreadSetting(roles, this.commonSpreadSetting);
+            this.refreshSpreadSetting(roles, this.dealSpreadSetting);
+            SpreadJsObj.initSheet(this.sheet, this.commonSpreadSetting);
+        }
+        loadData(datas, roles) {
+            this.initSpreadSetting(roles);
+            compareUtils.analysisCompareData(datas, roles, this.dealSpreadSetting.extraCols);
+            this.data.loadDatas(datas);
+            this.loadDetailData(SpreadJsObj.getSelectObject(billsObj.sheet));
+        }
+        refreshSheet() {
+            if (this.sheet.zh_data.length === 0) {
+                this.reloadDetailData();
+            } else {
+                SpreadJsObj.reLoadSheetData(this.sheet);
+            }
+        }
+        reloadDetailData() {
+            const data = this.billsNode ? this.data.getPartData(this.billsNode.id) || [] : [];
+            SpreadJsObj.loadSheetData(this.sheet, SpreadJsObj.DataType.Data, data);
+        }
+        loadDetailData(bills) {
+            detailObj.billsNode = bills;
+            const spreadSetting = bills && bills.is_deal ? this.dealSpreadSetting : this.commonSpreadSetting;
+            SpreadJsObj.initSheet(this.sheet, spreadSetting);
+            this.reloadDetailData();
+            SpreadJsObj.locateRow(this.sheet, 0, false);
+        }
+    }
+    const detailObj = new DetailObj();
+
+    const searchObj = $.ledgerSearch({
+        selector: '#search',
+        searchRangeStr: '编号/名称',
+        ledger: {
+            billsTree: billsObj.tree,
+            getLedgerPos: function(node) { return detailObj.data.getPartData(node.id); },
+            posId: 'id',
+        },
+        resultSpreadSetting: {
+            cols: [
+                {title: '编号', field: 'code', hAlign: 0, width: 90, formatter: '@'},
+                {title: '名称', field: 'name', width: 150, hAlign: 0, formatter: '@'},
+                {title: '单位', field: 'unit', width: 50, hAlign: 1, formatter: '@'},
+            ],
+            emptyRows: 0,
+            headRows: 1,
+            headRowHeight: [32],
+            headColWidth: [30],
+            defaultRowHeight: 21,
+            headerFont: '12px 微软雅黑',
+            font: '12px 微软雅黑',
+            selectedBackColor: '#fffacd',
+            readOnly: true,
+        },
+        locate: function(cur) {
+            if (!cur.lid) return;
+
+            SpreadJsObj.locateTreeNode(billsObj.sheet, cur.lid, true);
+            billsObj.loadRelaData();
+            if (cur.pid) {
+                const pIndex = detailObj.sheet.zh_data.findIndex(x => { return x.id === cur.pid; });
+                if (pIndex >= 0) SpreadJsObj.locateRow(detailObj.sheet, pIndex);
+            }
+        },
+    });
+    // 加载数据
+    postData('load', { filter: 'billsCompare;detailCompare;auditFlow' }, function(result) {
+        const filter = true;
+        const roles = filter ? compareUtils.filterSameRoles(result.auditFlow) : result.auditFlow;
+        billsObj.loadData(result.billsCompare, roles);
+        detailObj.loadData(result.detailCompare, roles);
+    });
+    // 展开收起标准清单
+    $('a', '#side-menu').bind('click', function (e) {
+        e.preventDefault();
+        const tab = $(this), tabPanel = $(tab.attr('content'));
+        // 展开工具栏、切换标签
+        if (!tab.hasClass('active')) {
+            // const close = $('.active', '#side-menu').length === 0;
+            $('a', '#side-menu').removeClass('active');
+            $('.tab-content .tab-select-show.tab-pane.active').removeClass('active');
+            tab.addClass('active');
+            tabPanel.addClass('active');
+            // $('.tab-content .tab-pane').removeClass('active');
+            showSideTools(tab.hasClass('active'));
+            if (tab.attr('content') === '#search') {
+                searchObj.spread.refresh();
+            }
+        } else { // 收起工具栏
+            tab.removeClass('active');
+            tabPanel.removeClass('active');
+            showSideTools(tab.hasClass('active'));
+        }
+        billsObj.spread.refresh();
+        detailObj.spread.refresh();
+    });
+
+    $.divResizer({
+        select: '#detail-spr',
+        callback: function () {
+            billsObj.spread.refresh();
+            detailObj.spread.refresh();
+        }
+    });
+    // 工具栏spr
+    $.divResizer({
+        select: '#right-spr',
+        callback: function () {
+            billsObj.spread.refresh();
+            detailObj.spread.refresh();
+            searchObj.spread.refresh();
+        }
+    });
+    // 导航Menu
+    $.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();
+            billsObj.spread.refresh();
+            detailObj.spread.refresh();
+        }
+    });
+    // 显示层次
+    (function (select, sheet) {
+        $(select).click(function () {
+            if (!sheet.zh_tree) return;
+            const tag = $(this).attr('tag');
+            const tree = sheet.zh_tree;
+            setTimeout(() => {
+                showWaitingView();
+                switch (tag) {
+                    case "1":
+                    case "2":
+                    case "3":
+                    case "4":
+                        tree.expandByLevel(parseInt(tag));
+                        SpreadJsObj.refreshTreeRowVisible(sheet);
+                        break;
+                    case "last":
+                        tree.expandByCustom(() => { return true; });
+                        SpreadJsObj.refreshTreeRowVisible(sheet);
+                        break;
+                }
+                closeWaitingView();
+            }, 100);
+        });
+    })('a[name=showLevel]', billsObj.sheet);
+});

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

@@ -0,0 +1,186 @@
+$(document).ready(() => {
+    autoFlashHeight();
+    const ledgerGatherSpreadSetting = {
+        cols: [
+            {title: '编号', colSpan: '1', rowSpan: '2', field: 'code', hAlign: 0, width: 230, formatter: '@', cellType: 'tree'},
+            {title: '名称', colSpan: '1', rowSpan: '2', field: 'name', hAlign: 0, width: 185, formatter: '@', cellType: 'autoTip'},
+        ],
+        baseCols: [
+            {title: '编号', colSpan: '1', rowSpan: '2', field: 'code', hAlign: 0, width: 230, formatter: '@', cellType: 'tree'},
+            {title: '名称', colSpan: '1', rowSpan: '2', field: 'name', hAlign: 0, width: 185, formatter: '@', cellType: 'autoTip'},
+        ],
+        extraCols: [
+            {title: '%s|付款金额', colSpan: '4|1', rowSpan: '1|1', field: 'pay_tp', hAlign: 2, width: 80, type: 'Number'},
+            {title: '|扣款金额', colSpan: '|1', rowSpan: '|1', field: 'cut_tp', hAlign: 2, width: 80, type: 'Number'},
+            {title: '|应付金额', colSpan: '|1', rowSpan: '|1', field: 'yf_tp', hAlign: 2, width: 80, type: 'Number'},
+            {title: '|实付金额', colSpan: '|1', rowSpan: '|1', field: 'sf_tp', hAlign: 2, width: 80, type: 'Number'},
+        ],
+        emptyRows: 0,
+        headRows: 2,
+        headRowHeight: [25, 25],
+        defaultRowHeight: 21,
+        headerFont: '12px 微软雅黑',
+        font: '12px 微软雅黑',
+        readOnly: true,
+    };
+    sjsSettingObj.setFxTreeStyle(ledgerGatherSpreadSetting, sjsSettingObj.FxTreeStyle.phasePay);
+
+    const ledgerGatherSpread = SpreadJsObj.createNewSpread($('#ledger-gather-spread')[0]);
+    const ledgerGatherSheet = ledgerGatherSpread.getActiveSheet();
+    SpreadJsObj.initSheet(ledgerGatherSheet, ledgerGatherSpreadSetting);
+
+    const ledgerGatherObj = {
+        tree: createNewPathTree('ledger', {
+            id: 'tree_id', pid: 'tree_pid', order: 'tree_order',
+            level: 'tree_level', isLeaf: 'tree_is_leaf', fullPath: 'tree_full_path',
+            rootId: -1, calcFields: [],
+        }),
+        stages: [],
+        splitStages (gatherType, gatherStageIds) {
+            const gatherStages = this.stages.filter(x => { return gatherStageIds.indexOf(x.id) >= 0; });
+            gatherStages.sort((a, b) => { return a.stage_order - b.stage_order; });
+            const result = { main: gatherStages[gatherStages.length - 1], gatherPart: [] };
+            for (const gs of gatherStages) {
+                if (gatherType === 'stage') {
+                    result.gatherPart.push({title: `第${gs.stage_order}期(${gs.stage_date})`, stages: [gs]});
+                } else {
+                    let title;
+                    if (gatherType === 'month') title = `${gs.stage_date}`;
+                    if (gatherType === 'quarter') title = `${gs.gather_year} 第${gs.gather_quarter}季度`;
+                    if (gatherType === 'year') title = `${gs.gather_year}`;
+
+                    let gp = result.gatherPart.find(x => { return x.title === title; });
+                    if (!gp) {
+                        gp = { title, stages: [] };
+                        result.gatherPart.push(gp);
+                    }
+                    gp.stages.push(gs);
+                }
+            }
+            return result;
+        },
+        gather (gatherType, gatherStageIds) {
+            if (gatherStageIds.length === 0) return;
+            const gatherInfo = this.splitStages(gatherType, gatherStageIds);
+
+            this.tree.setting.calcFields.length = 0;
+            this.tree.loadDatas(gatherInfo.main.detail.bills);
+            ledgerGatherSpreadSetting.cols.length = 0;
+            ledgerGatherSpreadSetting.cols.push(...ledgerGatherSpreadSetting.baseCols);
+            for (const [i, gp] of gatherInfo.gatherPart.entries()) {
+                const loadFields = [];
+                for (const ec of ledgerGatherSpreadSetting.extraCols) {
+                    const col = JSON.parse(JSON.stringify(ec));
+                    col.title = col.title.replace('%s', gp.title);
+                    loadFields.push({ sf: col.field, tf: col.field + '_' + i});
+                    col.field = col.field + '_' + i;
+                    ledgerGatherSpreadSetting.cols.push(col);
+                    this.tree.setting.calcFields.push(col.field);
+                    this.tree.nodes.forEach(x => { x[col.field] = 0; });
+                }
+                for (const s of gp.stages) {
+                    for (const b of s.detail.bills) {
+                        const node = this.tree.nodes.find(x => { return x.cost_id === b.cost_id; });
+                        if (!node) continue;
+                        for (const lf of loadFields) {
+                            node[lf.tf] = ZhCalc.add(node[lf.tf], b[lf.sf]);
+                        }
+                    }
+                }
+            }
+            treeCalc.calculateAll(this.tree);
+            SpreadJsObj.initSheet(ledgerGatherSheet, ledgerGatherSpreadSetting);
+            SpreadJsObj.loadSheetData(ledgerGatherSheet, SpreadJsObj.DataType.Tree, this.tree);
+            SpreadJsObj.locateRow(ledgerGatherSheet, 0);
+        },
+        loadStages (stages) {
+            for (const s of stages) {
+                s.gather_stage = s.stage_order;
+                s.gather_month = s.stage_date;
+                s.gather_time = moment(new Date(s.stage_date));
+                s.gather_quarter = s.gather_time.quarter();
+                s.gather_year = s.gather_time.year();
+                this.stages.push(s);
+            }
+        },
+        exportExcel() {
+            if (!this.tree || this.tree.nodes.length === 0) {
+                toastr.warning('无可导出数据');
+                return;
+            }
+            SpreadExcelObj.exportSimpleXlsxSheet(ledgerGatherSpreadSetting, this.tree.nodes, "成本管理-汇总分析.xlsx");
+        }
+    };
+
+    $('#select-qi-all').click(function() {
+        const check = this.checked;
+        $('input', 'tr[stage-id]').each((i, x) => {
+            x.checked = check;
+        })
+    });
+    $('#select-qi-ok').click(function() {
+        let loadData = [], gatherData = [], trs = $('tr[stage-id]');
+        for (let order = 0, iLength = trs.length; order < iLength; order++) {
+            const tr = trs[order];
+            if ($('input[type=checkbox]', tr)[0].checked) {
+                const sid = tr.getAttribute('stage-id');
+                const exist = ledgerGatherObj.stages.find(x => { return x.id === sid; });
+                if (!exist) loadData.push(sid);
+                gatherData.push(sid);
+            }
+        }
+        const gatherType = $('input[name=gather-type]:checked').val();
+        if (loadData.length > 0) {
+            postData(window.location.pathname + '/load', {stages: loadData}, function (result) {
+                ledgerGatherObj.loadStages(result.stages);
+                ledgerGatherObj.gather(gatherType, gatherData);
+                $('#select-qi').modal('hide');
+            }, null, true);
+        } else {
+            ledgerGatherObj.gather(gatherType, gatherData);
+            $('#select-qi').modal('hide');
+        }
+    });
+
+    $('#export-excel').click(function() {
+        ledgerGatherObj.exportExcel();
+    });
+    // 显示层次
+    (function (select, sheet) {
+        $(select).click(function () {
+            if (!sheet.zh_tree) return;
+            const tag = $(this).attr('tag');
+            const tree = sheet.zh_tree;
+            switch (tag) {
+                case "1":
+                case "2":
+                    tree.expandByLevel(parseInt(tag));
+                    SpreadJsObj.refreshTreeRowVisible(sheet);
+                    break;
+                case "last":
+                    tree.expandByCustom(() => { return true; });
+                    SpreadJsObj.refreshTreeRowVisible(sheet);
+                    break;
+            }
+        });
+    })('a[name=showLevel]', ledgerGatherSheet);
+    $.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');
+                $('.c-body table thead').css('left', '56px');
+            } else {
+                $('.panel-title').removeClass('fluid');
+                $('#sub-menu').addClass('panel-sidebar');
+                $('.c-body table thead').css('left', '176px');
+            }
+            autoFlashHeight();
+            ledgerGatherSpread.refresh();
+        }
+    });
+});

+ 177 - 34
app/public/js/cost_stage_analysis.js

@@ -53,6 +53,7 @@ $(document).ready(function() {
             $('a[name="base-opr"]').click(function () {
                 self.baseOpr(this.getAttribute('type'));
             });
+            $('#bills-spread').focusin(this.focusIn);
         }
         refreshOperationValid() {
             const setObjEnable = function (obj, enable) {
@@ -102,21 +103,21 @@ $(document).ready(function() {
             }
             const preNode = tree.getPreSiblingNode(first);
             const valid = !this.sheet.zh_setting.readOnly;
-            const topParent = tree.getTopParent(first);
-            const isFixed = first.tree_level === 1 || [2,3,4].indexOf(topParent.node_type) >= 0;
+            const isFixed = first.tree_level === 1;
 
-            setObjEnable($('a[name=base-opr][type=add]'), valid && first && !isFixed);
+            setObjEnable($('a[name=base-opr][type=add]'), valid && first && (first.tree_level !== 1 || first.node_type === 2));
             setObjEnable($('a[name=base-opr][type=delete]'), valid && first && sameParent && !isFixed && !specNode);
             setObjEnable($('a[name=base-opr][type=up-move]'), valid && first && sameParent && preNode && !isFixed);
             setObjEnable($('a[name=base-opr][type=down-move]'), valid && first && sameParent  && !tree.isLastSibling(last) && !isFixed);
             setObjEnable($('a[name=base-opr][type=up-level]'), valid && first && sameParent && tree.getParent(first) && !nodeUsed && first.tree_level > 2 && !isFixed);
             setObjEnable($('a[name=base-opr][type=down-level]'), valid && first && sameParent && preNode && !isFixed && !preNode.node_type);
         }
-        loadRelaData() {
+        loadRelaData(loadAtt = true) {
             this.refreshOperationValid();
             SpreadJsObj.saveTopAndSelect(this.sheet, this.ckBillsSpread);
             const select = SpreadJsObj.getSelectObject(this.sheet);
             detailObj.loadDetailData(select);
+            if (loadAtt) costFile.getCurAttHtml(select);
         }
         refreshTree(data) {
             const sheet = this.sheet;
@@ -253,7 +254,7 @@ $(document).ready(function() {
             }
 
             const updateData = {
-                target: 'ledger', postType: type,
+                target: 'ledger', postType: type, addType: type === 'add' ? (node.node_type === 2 ? 'child' : 'next') : '',
                 postData: {
                     id: node.tree_id,
                     count: type === 'add' ? addCount : count,
@@ -268,6 +269,7 @@ $(document).ready(function() {
                             sheet.setSelection(sel.row, sel.col, 1, sel.colCount);
                         }
                         self.refreshOperationValid();
+                        costFile.deleteFileByNodeId(result.delete.map(x => { return x.id; }));
                     });
                 });
             } else {
@@ -277,20 +279,25 @@ $(document).ready(function() {
                     if (['up-move', 'down-move'].indexOf(type) > -1) {
                         if (sel) {
                             sheet.setSelection(tree.nodes.indexOf(node), sel.col, sel.rowCount, sel.colCount);
-                            // SpreadJsObj.reloadRowsBackColor(sheet, [sel.row, tree.nodes.indexOf(node)]);
+                            SpreadJsObj.reloadRowsBackColor(sheet, [sel.row, tree.nodes.indexOf(node)]);
                         }
                     } else if (type === 'add') {
                         const sel = sheet.getSelections()[0];
                         if (sel) {
                             sheet.setSelection(tree.nodes.indexOf(refreshData.create[0]), sel.col, sel.rowCount, sel.colCount);
-                            // SpreadJsObj.reloadRowsBackColor(sheet, [sel.row, tree.nodes.indexOf(refreshData.create[0])]);
+                            SpreadJsObj.reloadRowsBackColor(sheet, [sel.row, tree.nodes.indexOf(refreshData.create[0])]);
                         }
+                        billsObj.loadRelaData();
                     }
                     self.refreshOperationValid();
                 });
             }
         }
         // 事件
+        focusIn() {
+            const select = SpreadJsObj.getSelectObject(billsObj.sheet);
+            costFile.getCurAttHtml(select);
+        }
         selectionChanged(e, info) {
             if (info.newSelections) {
                 if (!info.oldSelections || info.newSelections[0].row !== info.oldSelections[0].row) {
@@ -304,32 +311,27 @@ $(document).ready(function() {
         editStarting(e, info) {
             if (!info.sheet.zh_setting || !info.sheet.zh_tree) return;
 
-            const tree = info.sheet.zh_tree;
             const col = info.sheet.zh_setting.cols[info.col];
             const node = info.sheet.zh_tree.nodes[info.row];
             if (!node) {
                 info.cancel = true;
                 return;
             }
-            const topParent = tree.getTopParent(node);
+            const detailRange = detailObj.data.getPartData(node.id);
 
             switch (col.field) {
                 case 'code':
                 case 'name':
-                    info.cancel = node.tree_level === 1 || node.node_type > 0 || node.calc_type === 2;
+                    info.cancel = node.tree_level === 1 || node.node_type > 0;
                     break;
                 case 'in_excl_tax_tp':
-                    info.cancel = node.children && node.children.length > 0;
-                    if (!info.cancel) {
-                        info.cancel = node.tree_level === 1 || node.node_type > 1  || topParent.node_type === 2;
-                    }
+                    info.cancel = node.tree_level === 1 || (node.children && node.children.length > 0);
+                    if (!info.cancel) info.cancel = node.node_type > 1 || !!node.import_cost_id;
                     break;
                 case 'yf_excl_tax_tp':
                 case 'sf_excl_tax_tp':
-                    info.cancel = node.children && node.children.length > 0;
-                    if (!info.cancel) {
-                        info.cancel = node.tree_level === 1 || node.node_type > 0  || topParent.node_type === 2;
-                    }
+                    info.cancel = node.tree_level === 1 || (node.children && node.children.length > 0);
+                    if (!info.cancel) info.cancel = node.node_type > 0 || !!node.import_cost_id;
                     break;
                 case 'num_a':
                 case 'num_b':
@@ -347,11 +349,8 @@ $(document).ready(function() {
                 case 'num_n':
                 case 'num_o':
                 case 'num_p':
-                    info.cancel = node.children && node.children.length > 0;
-                    if (!info.cancel) {
-                        const detailRange = detailObj.data.getPartData(node.id);
-                        info.cancel = detailRange && detailRange.length > 0;
-                    }
+                    info.cancel = node.tree_level === 1 || (node.children && node.children.length > 0);
+                    if (!info.cancel) info.cancel = !!node.import_cost_id || (detailRange && detailRange.length > 0);
                     break;
             }
         }
@@ -492,7 +491,12 @@ $(document).ready(function() {
         constructor() {
             this.spread = SpreadJsObj.createNewSpread($('#detail-spread')[0]);
             this.sheet = this.spread.getActiveSheet();
-            this.data = createAncillaryGcl({ id: 'id', masterId: 'ledger_id', sort: [['d_order', 'asc']] });
+            this.data = createAncillaryGcl({
+                id: 'id', masterId: 'ledger_id', sort: [['d_order', 'asc']],
+                calcFun: function(node) {
+                    node.sf_percent = node.yf_excl_tax_tp ? ZhCalc.mul(ZhCalc.div(node.sf_excl_tax_tp, node.yf_excl_tax_tp), 100 , 2) : 0;
+                },
+            });
             this.dealSpreadSetting = dealSpreadSetting;
             this.dealSpreadSetting.readOnly = readOnly;
             this.dealSpreadSetting.tipsSum = true;
@@ -501,9 +505,11 @@ $(document).ready(function() {
             this.commonSpreadSetting.tipsSum = true;
 
             this.initSpread();
+            this.initOtherEvent();
         }
         initSpread() {
             SpreadJsObj.initSheet(this.sheet, this.commonSpreadSetting);
+            this.spread.bind(spreadNS.Events.SelectionChanged, this.selectionChanged);
             this.spread.bind(spreadNS.Events.ClipboardChanging, function (e, info) {
                 const copyText = SpreadJsObj.getFilterCopyText(info.sheet);
                 SpreadJsObj.Clipboard.setCopyData(copyText);
@@ -513,8 +519,12 @@ $(document).ready(function() {
             this.spread.bind(spreadNS.Events.ClipboardPasting, this.clipboardPasting);
             SpreadJsObj.addDeleteBind(this.spread, this.deletePress);
         }
+        initOtherEvent() {
+            $('#detail-spread').focusin(this.focusIn);
+        }
         loadData(datas) {
             this.data.loadDatas(datas);
+            this.data.calculateAll();
             this.loadDetailData(SpreadJsObj.getSelectObject(billsObj.sheet));
         }
         refreshSheet() {
@@ -541,7 +551,21 @@ $(document).ready(function() {
             this.reloadDetailData();
             SpreadJsObj.locateRow(this.sheet, 0, false);
         }
+        loadRelaData() {
+            const select = SpreadJsObj.getSelectObject(this.sheet);
+            if (select) costFile.getCurAttHtml(this.billsNode, select);
+        }
         // 事件
+        focusIn() {
+            detailObj.loadRelaData();
+        }
+        selectionChanged(e, info) {
+            if (info.newSelections) {
+                if (!info.oldSelections || info.newSelections[0].row !== info.oldSelections[0].row) {
+                    detailObj.loadRelaData();
+                }
+            }
+        }
         editEnded(e, info) {
             const setting = info.sheet.zh_setting;
             if (!setting) return;
@@ -692,6 +716,80 @@ $(document).ready(function() {
     }
     const detailObj = new DetailObj();
 
+    const importCost = (function() {
+        const spread = SpreadJsObj.createNewSpread($('#import-cost-spread')[0]);
+        const sheet = spread.getActiveSheet();
+        SpreadJsObj.initSheet(sheet, {
+            cols: [
+                { title: '选择', colSpan: '1', rowSpan: '1', field: 'selected', hAlign: 1, width: 40, formatter: '@', cellType: 'checkbox' },
+                { title: '编号', colSpan: '1', rowSpan: '1', field: 'code', hAlign: 0, width: 120, formatter: '@', cellType: 'tree' },
+                { title: '名称', colSpan: '1', rowSpan: '1', field: 'name', hAlign: 0, width: 200, formatter: '@' },
+                { title: '类型', colSpan: '1', rowSpan: '1', field: 'detail_type', hAlign: 1, width: 40, formatter: '@' },
+            ],
+            emptyRows: 0,
+            headRows: 1,
+            headRowHeight: [32],
+            defaultRowHeight: 21,
+            headerFont: '12px 微软雅黑',
+            font: '12px 微软雅黑',
+            readOnly: true,
+        });
+        const tree = createNewPathTree('ledger', {
+            id: 'tree_id', pid: 'tree_pid', order: 'tree_order', level: 'tree_level',
+            isLeaf: 'tree_is_leaf', fullPath: 'tree_full_path', rootId: -1,
+        });
+        spread.bind(spreadNS.Events.ButtonClicked, function(e, info) {
+            if (!info.sheet.zh_setting || !info.sheet.zh_tree) return;
+
+            const col = info.sheet.zh_setting.cols[info.col];
+            if (col.field !== 'selected') return;
+
+            const node = SpreadJsObj.getSelectObject(info.sheet);
+            if (!node.children || node.children.length === 0) {
+                const selected = tree.nodes.find(x => { return x.selected; });
+                if (selected && selected.detail_type !== node.detail_type && !node.selected) {
+                    toastr.warning('勾选节点类型不一致,不允许勾选');
+                    return;
+                }
+                node.selected = !node.selected;
+                SpreadJsObj.reLoadRowData(info.sheet, info.row, 1);
+            } else {
+                toastr.warning('父项不可勾选');
+            }
+        });
+        let curNode;
+        const initCostTree = function(data) {
+            tree.loadDatas(data);
+            SpreadJsObj.loadSheetData(sheet, SpreadJsObj.DataType.Tree, tree);
+        };
+        const show = function(bills) {
+            curNode = bills;
+            const orgSelect = curNode.import_cost_id.split(',');
+            tree.nodes.forEach(x => {
+                x.selected = orgSelect.indexOf(x.cost_id) >= 0;
+            });
+            SpreadJsObj.reloadColData(sheet, 0);
+            $('#import-cost').modal('show');
+        };
+        $('#import-cost').on('shown.bs.modal', function() {
+            spread.refresh();
+        });
+        $('#import-cost-ok').click(function() {
+            const updateData = { target: 'importCost', select_id: curNode.id, cost_id: tree.nodes.filter(x => { return x.selected; }).map(x => { return x.cost_id; }) };
+            if (updateData.cost_id.length === 0) {
+                toastr.warning('请选择要导入的成本节点');
+                return;
+            }
+            postData('update', updateData, function (result) {
+                const refreshNode = billsObj.tree.loadPostData(result);
+                billsObj.refreshTree(refreshNode);
+                detailObj.data.updateDatas(result.detail);
+                detailObj.loadDetailData(curNode);
+                $('#import-cost').modal('hide');
+            });
+        });
+        return { show, initCostTree };
+    })();
     // 清单右键菜单
     const billsContextMenuOptions = {
         selector: '#bills-spread',
@@ -743,9 +841,8 @@ $(document).ready(function() {
                 if (!tree) return true;
                 const first = sheet.zh_tree.nodes[row];
                 const valid = !sheet.zh_setting.readOnly;
-                const topParent = tree.getTopParent(first);
-                const isFixed = first.tree_level === 1 || [2,3,4].indexOf(topParent.node_type) >= 0;
-                return !valid || !first || isFixed;
+                const isFixed = first.tree_level === 1;
+                return !valid || !first || (isFixed && first.node_type !== 2);
             }
         };
         billsContextMenuOptions.items.delete = {
@@ -780,8 +877,7 @@ $(document).ready(function() {
                     }
                 }
                 const valid = !sheet.zh_setting.readOnly;
-                const topParent = tree.getTopParent(first);
-                const isFixed = first.tree_level === 1 || [2,3,4].indexOf(topParent.node_type) >= 0;
+                const isFixed = first.tree_level === 1;
                 return !valid || !first || !sameParent || nodeUsed || isFixed || specNode;
             }
         };
@@ -811,9 +907,8 @@ $(document).ready(function() {
                 if (!tree) return true;
                 const first = sheet.zh_tree.nodes[row];
                 const valid = !sheet.zh_setting.readOnly;
-                const topParent = tree.getTopParent(first);
-                const isFixed = first.tree_level === 1 || [2,3,4].indexOf(topParent.node_type) >= 0;
-                return !valid || !first || isFixed;
+                const isFixed = first.tree_level === 1;
+                return !valid || !first || (isFixed && first.node_type !== 2);
             },
             visible: function (key, opt) {
                 return !readOnly;
@@ -832,6 +927,18 @@ $(document).ready(function() {
                 return !node || (node.children && node.children.length > 0) || node.node_type !== 1;
             }
         };
+        billsContextMenuOptions.items.importCost = {
+            name: '导入成本数据',
+            callback: function() {
+                const node = SpreadJsObj.getSelectObject(billsObj.sheet);
+                importCost.show(node);
+            },
+            disabled: function() {
+                const node = SpreadJsObj.getSelectObject(billsObj.sheet);
+                const topNode = billsObj.tree.getTopParent(node);
+                return !node || (node.children && node.children.length > 0) || topNode.node_type !== 2;
+            }
+        };
         $('#import-pay-ok').click(function() {
             const updateData = { target: 'importPay', pay_order: $('#pay-order').val(), pay_tax: parseFloat($('#pay-tax').val()) };
             if (updateData.pay_tax < 0 || updateData.pay_tax > 100) {
@@ -843,7 +950,7 @@ $(document).ready(function() {
                 billsObj.refreshTree(refreshNode);
                 $('#import-pay').modal('hide');
             });
-        })
+        });
     }
     $.contextMenu(billsContextMenuOptions);
 
@@ -882,10 +989,46 @@ $(document).ready(function() {
             }
         },
     });
+    const costFile = $.ledger_att({
+        selector: '#fujian',
+        key: 'id',
+        subKey: 'id',
+        masterKey: 'rela_id',
+        subMasterKey: 'rela_sub_id',
+        uploadUrl: 'file/upload',
+        deleteUrl: 'file/delete',
+        checked: costStageComplete,
+        zipName: `附件.zip`,
+        readOnly: false,
+        fileIdType: 'string',
+        fileInfo: {
+            user_name: 'user_name',
+            user_id: 'user_id',
+            create_time: 'create_time',
+        },
+        getCurHint: function(node) {
+            return`${node.name || ''}`;
+        },
+        locate: function (att) {
+            if (!att) return;
+            SpreadJsObj.locateTreeNode(billsObj.sheet, att.node.tree_id, true);
+            detailObj.loadDetailData(att.node);
+            if (att.sub_node) SpreadJsObj.locateDataBy(detailObj.sheet, n => { return n.id === att.sub_node.id; });
+            costFile.getCurAttHtml(att.node, att.sub_node);
+        }
+    });
     // 加载数据
-    postData('load', { filter: 'bills;detail;att;tags' }, function(result) {
+    postData('load', { filter: 'bills;detail;att;tags;relaStageBills' }, function(result) {
         billsObj.loadData(result.bills);
         detailObj.loadData(result.detail);
+
+        for (const f of result.att) {
+            f.node = billsObj.tree.datas.find(x => { return x.id === f.rela_id; });
+            f.sub_node = f.rela_sub_id ? detailObj.data.getItem(f.rela_sub_id) : null;
+        }
+        costFile.loadDatas(result.att);
+        costFile.getCurAttHtml(SpreadJsObj.getSelectObject(billsObj.sheet));
+        importCost.initCostTree(result.relaStageBills);
     });
 
     // 展开收起标准清单

+ 49 - 14
app/public/js/cost_stage_book.js

@@ -16,25 +16,36 @@ $(document).ready(function() {
                 isLeaf: 'tree_is_leaf',
                 fullPath: 'tree_full_path',
                 rootId: -1,
-                calcFields: ['sf_tp', 'in_tp', 'in_excl_tax_tp'],
+                calcFields: ['yf_tp', 'sf_tp', 'in_tp', 'in_excl_tax_tp', 'pre_yf_tp', 'pre_sf_tp', 'pre_in_tp', 'pre_in_excl_tax_tp', 'end_yf_tp', 'end_sf_tp', 'end_in_tp', 'end_in_excl_tax_tp'],
                 keys: ['id', 'stage_id', 'tree_id'],
+                calcFun: function(node) {
+                    node.end_yf_tp = ZhCalc.add(node.yf_tp, node.pre_yf_tp);
+                    node.end_sf_tp = ZhCalc.add(node.sf_tp, node.pre_sf_tp);
+                    node.end_in_tp = ZhCalc.add(node.in_tp, node.pre_in_tp);
+                    node.end_in_excl_tax_tp = ZhCalc.add(node.in_excl_tax_tp, node.pre_in_excl_tax_tp);
+                },
             };
             this.tree = createNewPathTree('ledger', this.treeSetting);
             this.spreadSetting = {
                 cols: [
-                    {title: '编号', colSpan: '1', rowSpan: '1', field: 'code', hAlign: 0, width: 230, formatter: '@', cellType: 'tree', readOnly: true},
-                    {title: '名称', colSpan: '1', rowSpan: '1', field: 'name', hAlign: 0, width: 185, formatter: '@', cellType: 'autoTip', readOnly: true},
-                    {title: '单位', colSpan: '1', rowSpan: '1', field: 'unit', hAlign: 1, width: 50, formatter: '@', cellType: 'unit', readOnly: true},
-                    {title: '税率(%)', colSpan: '1', rowSpan: '1', field: 'tax', hAlign: 2, width: 80, type: 'Number', readOnly: true},
-                    {title: '实付金额', colSpan: '1', rowSpan: '1', field: 'sf_tp', hAlign: 2, width: 80, type: 'Number', readOnly: true},
-                    {title: '入账金额', colSpan: '1', rowSpan: '1', field: 'in_tp', hAlign: 2, width: 80, type: 'Number'},
-                    {title: '入账金额(不含税)', colSpan: '1', rowSpan: '1', field: 'in_excl_tax_tp', hAlign: 2, width: 100, type: 'Number', readOnly: true},
-                    {title: '本期批注', colSpan: '1', rowSpan: '1', field: 'postil', hAlign: 0, width: 100, formatter: '@', cellType: 'ellipsisAutoTip'},
-                    {title: '备注', colSpan: '1', rowSpan: '1', field: 'memo', hAlign: 0, width: 120, formatter: '@', cellType: 'ellipsisAutoTip'},
+                    {title: '编号', colSpan: '1', rowSpan: '2', field: 'code', hAlign: 0, width: 230, formatter: '@', cellType: 'tree', readOnly: true},
+                    {title: '名称', colSpan: '1', rowSpan: '2', field: 'name', hAlign: 0, width: 185, formatter: '@', cellType: 'autoTip', readOnly: true},
+                    {title: '单位', colSpan: '1', rowSpan: '2', field: 'unit', hAlign: 1, width: 50, formatter: '@', cellType: 'unit', readOnly: true},
+                    {title: '税率(%)', colSpan: '1', rowSpan: '2', field: 'tax', hAlign: 2, width: 80, type: 'Number', readOnly: true},
+                    {title: '本期|应付金额', colSpan: '4|1', rowSpan: '1|1', field: 'yf_tp', hAlign: 2, width: 80, type: 'Number', readOnly: true},
+                    {title: '|实付金额', colSpan: '|1', rowSpan: '|1', field: 'sf_tp', hAlign: 2, width: 80, type: 'Number', readOnly: true},
+                    {title: '|入账金额', colSpan: '|1', rowSpan: '|1', field: 'in_tp', hAlign: 2, width: 80, type: 'Number'},
+                    {title: '|入账金额\n(不含税)', colSpan: '|1', rowSpan: '|1', field: 'in_excl_tax_tp', hAlign: 2, width: 100, type: 'Number'},
+                    {title: '截止本期|应付金额', colSpan: '4|1', rowSpan: '1|1', field: 'end_yf_tp', hAlign: 2, width: 80, type: 'Number', readOnly: true},
+                    {title: '|实付金额', colSpan: '|1', rowSpan: '|1', field: 'end_sf_tp', hAlign: 2, width: 80, type: 'Number', readOnly: true},
+                    {title: '|入账金额', colSpan: '|1', rowSpan: '|1', field: 'end_in_tp', hAlign: 2, width: 80, type: 'Number', readOnly: true},
+                    {title: '|入账金额\n(不含税)', colSpan: '|1', rowSpan: '|1', field: 'end_in_excl_tax_tp', hAlign: 2, width: 100, type: 'Number', readOnly: true},
+                    {title: '本期批注', colSpan: '1', rowSpan: '2', field: 'postil', hAlign: 0, width: 100, formatter: '@', cellType: 'ellipsisAutoTip'},
+                    {title: '备注', colSpan: '1', rowSpan: '2', field: 'memo', hAlign: 0, width: 120, formatter: '@', cellType: 'ellipsisAutoTip'},
                 ],
                 emptyRows: 0,
-                headRows: 1,
-                headRowHeight: [32],
+                headRows: 2,
+                headRowHeight: [36, 36],
                 defaultRowHeight: 21,
                 headerFont: '12px 微软雅黑',
                 font: '12px 微软雅黑',
@@ -244,6 +255,7 @@ $(document).ready(function() {
 
             switch (col.field) {
                 case 'in_tp':
+                case 'in_excl_tax_tp':
                     info.cancel = (node.children && node.children.length > 0) || node.had_detail;
                     if (!info.cancel) {
                         const detailRange = detailObj.data.getPartData(node.id);
@@ -398,7 +410,7 @@ $(document).ready(function() {
                     {title: '税率(%)', colSpan: '1', rowSpan: '1', field: 'tax', hAlign: 2, width: 80, type: 'Number', readOnly: true},
                     {title: '实付金额', colSpan: '1', rowSpan: '1', field: 'sf_tp', hAlign: 2, width: 80, type: 'Number', readOnly: true},
                     {title: '入账金额', colSpan: '1', rowSpan: '1', field: 'in_tp', hAlign: 2, width: 80, type: 'Number'},
-                    {title: '入账金额(不含税)', colSpan: '1', rowSpan: '1', field: 'in_excl_tax_tp', hAlign: 2, width: 100, type: 'Number', readOnly: true},
+                    {title: '入账金额(不含税)', colSpan: '1', rowSpan: '1', field: 'in_excl_tax_tp', hAlign: 2, width: 100, type: 'Number'},
                     {title: '本期批注', colSpan: '1', rowSpan: '1', field: 'postil', hAlign: 0, width: 100, formatter: '@', cellType: 'ellipsisAutoTip'},
                     {title: '备注', colSpan: '1', rowSpan: '1', field: 'memo', hAlign: 0, width: 120, formatter: '@', cellType: 'ellipsisAutoTip'},
                 ],
@@ -422,7 +434,7 @@ $(document).ready(function() {
                     {title: '税率(%)', colSpan: '1', rowSpan: '1', field: 'tax', hAlign: 2, width: 80, type: 'Number', readOnly: true},
                     {title: '实付金额', colSpan: '1', rowSpan: '1', field: 'sf_tp', hAlign: 2, width: 80, type: 'Number', readOnly: true},
                     {title: '入账金额', colSpan: '1', rowSpan: '1', field: 'in_tp', hAlign: 2, width: 80, type: 'Number'},
-                    {title: '入账金额(不含税)', colSpan: '1', rowSpan: '1', field: 'in_excl_tax_tp', hAlign: 2, width: 100, type: 'Number', readOnly: true},
+                    {title: '入账金额(不含税)', colSpan: '1', rowSpan: '1', field: 'in_excl_tax_tp', hAlign: 2, width: 100, type: 'Number'},
                     {title: '本期批注', colSpan: '1', rowSpan: '1', field: 'postil', hAlign: 0, width: 100, formatter: '@', cellType: 'ellipsisAutoTip'},
                     {title: '备注', colSpan: '1', rowSpan: '1', field: 'memo', hAlign: 0, width: 120, formatter: '@', cellType: 'ellipsisAutoTip'},
                 ],
@@ -803,4 +815,27 @@ $(document).ready(function() {
             }, 100);
         });
     })('a[name=showLevel]', billsObj.sheet);
+
+    if (!readOnly) {
+        $('#cost-decimal').on('show.bs.modal', () => {
+            const decimalObjs = [$('#cost-tp'), $('#cost-excl-tax-tp')];
+            for (const obj of decimalObjs) {
+                obj.val(obj.attr('org'));
+            }
+        });
+        $('#cost-decimal-ok').click(() => {
+            const decimal = { tp: parseInt($('#cost-tp').val()), excl_tax_tp: parseInt($('#cost-excl-tax-tp').val()) };
+            postData('decimal', { decimal }, function(result) {
+                if (result.calc) {
+                    const refreshNode = billsObj.tree.loadPostData({ update: result.bills });
+                    billsObj.refreshTree(refreshNode);
+                    detailObj.data.updateDatas({ update: result.detail });
+                    detailObj.reloadDetailData();
+                }
+                $('#cost-tp').attr('org', decimal.tp);
+                $('#cost-excl-tax-tp').attr('org', decimal.excl_tax_tp);
+                $('#cost-decimal').modal('hide');
+            });
+        });
+    }
 });

+ 550 - 0
app/public/js/cost_stage_duty.js

@@ -0,0 +1,550 @@
+function transExpr(expr) {
+    return $.trim(expr).replace('\t', '').replace('=', '').replace('%', '/100');
+}
+
+$(document).ready(function() {
+    autoFlashHeight();
+    class BillsObj {
+        constructor() {
+            this.spread = SpreadJsObj.createNewSpread($('#bills-spread')[0]);
+            this.sheet = this.spread.getActiveSheet();
+            this.treeSetting = {
+                id: 'tree_id',
+                pid: 'tree_pid',
+                order: 'tree_order',
+                level: 'tree_level',
+                isLeaf: 'tree_is_leaf',
+                fullPath: 'tree_full_path',
+                rootId: -1,
+                calcFields: [
+                    'total_price',
+                ],
+                keys: ['id', 'stage_id', 'tree_id'],
+            };
+            this.tree = createNewPathTree('ledger', this.treeSetting);
+            this.spreadSetting = {
+                cols: [
+                    {title: '项目节编号', colSpan: '1', rowSpan: '2', field: 'code', hAlign: 0, width: 180, formatter: '@', cellType: 'tree'},
+                    {title: '清单编号', colSpan: '1', rowSpan: '2', field: 'b_code', hAlign: 0, width: 120, formatter: '@'},
+                    {title: '名称', colSpan: '1', rowSpan: '2', field: 'name', hAlign: 0, width: 230, formatter: '@'},
+                    {title: '单位', colSpan: '1', rowSpan: '2', field: 'unit', hAlign: 1, width: 50, formatter: '@', cellType: 'unit', comboEdit: true},
+                    {title: '单价', colSpan: '1', rowSpan: '2', field: 'unit_price', hAlign: 2, width: 80, type: 'Number'},
+                    {title: '数量', colSpan: '1', rowSpan: '2', field: 'quantity', hAlign: 2, width: 80, type: 'Number'},
+                    {title: '金额', colSpan: '1', rowSpan: '2', field: 'total_price', hAlign: 2, width: 80, type: 'Number'},
+                    {title: '图册号', colSpan: '1', rowSpan: '2', field: 'drawing_code', hAlign: 0, width: 100, formatter: '@'},
+                    {title: '备注', colSpan: '1', rowSpan: '2', field: 'memo', hAlign: 0, width: 100, formatter: '@'},
+                ],
+                emptyRows: 0,
+                headRows: 2,
+                headRowHeight: [25, 25],
+                defaultRowHeight: 21,
+                headerFont: '12px 微软雅黑',
+                font: '12px 微软雅黑',
+                readOnly: readOnly,
+                localCache: {
+                    key: 'cost_duty',
+                    colWidth: true,
+                },
+                headColWidth: [50],
+                tipsSum: true,
+            };
+            sjsSettingObj.setFxTreeStyle(this.spreadSetting, sjsSettingObj.FxTreeStyle.jz);
+
+            this.ckBillsSpread = window.location.pathname + '-billsSelect';
+
+            this.initSpread();
+            this.initOtherEvent();
+        }
+        initSpread() {
+            SpreadJsObj.initSheet(this.sheet, this.spreadSetting);
+            this.spread.bind(spreadNS.Events.SelectionChanged, this.selectionChanged);
+            this.spread.bind(spreadNS.Events.topRowChanged, this.topRowChanged);
+            this.spread.bind(spreadNS.Events.ClipboardChanging, function (e, info) {
+                const copyText = SpreadJsObj.getFilterCopyText(info.sheet);
+                SpreadJsObj.Clipboard.setCopyData(copyText);
+            });
+            if (readOnly) return;
+            this.spread.bind(spreadNS.Events.EditEnded, this.editEnded);
+            this.spread.bind(spreadNS.Events.EditStarting, this.editStarting);
+            this.spread.bind(spreadNS.Events.ClipboardPasting, this.clipboardPasting);
+            SpreadJsObj.addDeleteBind(this.spread, this.deletePress);
+        }
+        initOtherEvent() {
+            const self = this;
+            // 增删上下移升降级
+            $('a[name="base-opr"]').click(function () {
+                self.baseOpr(this.getAttribute('type'));
+            });
+            $('body').on('change', 'input[name=in_tp]', function() {
+                const data = {
+                    in_tp: parseFloat($('input[name=in_tp]').val()),
+                };
+                postData('update', { target: 'extra', data} , function(result) {
+                    billsObj.setExtra(result);
+                });
+            });
+        }
+        refreshOperationValid() {
+            const setObjEnable = function (obj, enable) {
+                if (enable) {
+                    obj.removeClass('disabled');
+                } else {
+                    obj.addClass('disabled');
+                }
+            };
+            const invalidAll = function () {
+                setObjEnable($('a[name=base-opr][type=add]'), false);
+                setObjEnable($('a[name=base-opr][type=delete]'), false);
+                setObjEnable($('a[name=base-opr][type=up-move]'), false);
+                setObjEnable($('a[name=base-opr][type=down-move]'), false);
+                setObjEnable($('a[name=base-opr][type=up-level]'), false);
+                setObjEnable($('a[name=base-opr][type=down-level]'), false);
+            };
+            const sel = this.sheet.getSelections()[0];
+            const row = sel ? sel.row : -1;
+            const tree = this.sheet.zh_tree;
+            if (!tree) {
+                invalidAll();
+                return;
+            }
+            const first = tree.nodes[row];
+            if (!first) {
+                invalidAll();
+                return;
+            }
+            let last = first, sameParent = true;
+            if (sel.rowCount > 1 && first) {
+                for (let r = 1; r < sel.rowCount; r++) {
+                    const rNode = tree.nodes[sel.row + r];
+                    if (!rNode) {
+                        sameParent = false;
+                        break;
+                    }
+                    if (rNode.tree_level > first.tree_level) continue;
+                    if ((rNode.tree_level < first.tree_level) || (rNode.tree_level === first.tree_level && rNode.tree_pid !== first.tree_pid)) {
+                        sameParent = false;
+                        break;
+                    }
+                    last = rNode;
+                }
+            }
+            const preNode = tree.getPreSiblingNode(first);
+            const valid = !this.sheet.zh_setting.readOnly;
+
+            setObjEnable($('a[name=base-opr][type=add]'), valid && first && first.tree_level > 1);
+            setObjEnable($('a[name=base-opr][type=delete]'), valid && first && first.tree_level > 1 && sameParent);
+            setObjEnable($('a[name=base-opr][type=up-move]'), valid && first && first.tree_level > 1 && sameParent && preNode);
+            setObjEnable($('a[name=base-opr][type=down-move]'), valid && first && first.tree_level > 1 && sameParent  && !tree.isLastSibling(last));
+            setObjEnable($('a[name=base-opr][type=up-level]'), valid && first && first.tree_level > 2 && sameParent && tree.getParent(first) && first.tree_level > 1);
+            setObjEnable($('a[name=base-opr][type=down-level]'), valid && first && first.tree_level > 1 && sameParent && preNode);
+        }
+        refreshTree(data) {
+            const sheet = this.sheet;
+            SpreadJsObj.massOperationSheet(sheet, function () {
+                const tree = sheet.zh_tree;
+                // 处理删除
+                if (data.delete) {
+                    data.delete.sort(function (a, b) {
+                        return b.deleteIndex - a.deleteIndex;
+                    });
+                    for (const d of data.delete) {
+                        sheet.deleteRows(d.deleteIndex, 1);
+                    }
+                }
+                // 处理新增
+                if (data.create) {
+                    const newNodes = data.create;
+                    if (newNodes) {
+                        newNodes.sort(function (a, b) {
+                            return a.index - b.index;
+                        });
+
+                        for (const node of newNodes) {
+                            sheet.addRows(node.index, 1);
+                            SpreadJsObj.reLoadRowData(sheet, tree.nodes.indexOf(node), 1);
+                        }
+                    }
+                }
+                // 处理更新
+                if (data.update) {
+                    const rows = [];
+                    for (const u of data.update) {
+                        rows.push(tree.nodes.indexOf(u));
+                    }
+                    SpreadJsObj.reLoadRowsData(sheet, rows);
+                }
+                // 处理展开
+                if (data.expand) {
+                    const expanded = [];
+                    for (const e of data.expand) {
+                        if (expanded.indexOf(e) === -1) {
+                            const posterity = tree.getPosterity(e);
+                            for (const p of posterity) {
+                                sheet.setRowVisible(tree.nodes.indexOf(p), p.visible);
+                                expanded.push(p);
+                            }
+                        }
+                    }
+                }
+            });
+            this.refreshExtraTable();
+        }
+        loadData(datas) {
+            this.tree.loadDatas(datas);
+            treeCalc.calculateAll(this.tree);
+            SpreadJsObj.loadSheetData(this.sheet, SpreadJsObj.DataType.Tree, this.tree);
+            SpreadJsObj.loadTopAndSelect(this.sheet, this.ckBillsSpread);
+            this.refreshOperationValid();
+        }
+        getDefaultSelectInfo() {
+            if (!this.tree) return;
+            const sel = this.sheet.getSelections()[0];
+            const node = this.sheet.zh_tree.nodes[sel.row];
+            if (!node) return;
+            let count = 1;
+            if (sel.rowCount > 1) {
+                for (let r = 1; r < sel.rowCount; r++) {
+                    const rNode = this.sheet.zh_tree.nodes[sel.row + r];
+                    if (rNode.tree_level > node.tree_level) continue;
+                    if ((rNode.tree_level < node.tree_level) || (rNode.tree_level === node.tree_level && rNode.tree_pid !== node.tree_pid)) {
+                        toastr.warning('请选择同一节点下的节点,进行该操作');
+                        return;
+                    }
+                    count += 1;
+                }
+            }
+            return [this.tree, node, count];
+        }
+        baseOpr(type, addCount = 1) {
+            const self = this;
+            const sheet = self.sheet;
+            const sel = sheet.getSelections()[0];
+            const [tree, node, count] = this.getDefaultSelectInfo();
+            if (!tree || !node || !count) return;
+
+            const updateData = {
+                target: 'ledger', postType: type,
+                postData: {
+                    id: node.tree_id,
+                    count: type === 'add' ? addCount : count,
+                }
+            };
+            if (type === 'delete') {
+                deleteAfterHint(function () {
+                    postData('update', updateData, function (result) {
+                        const refreshData = tree.loadPostData(result);
+                        self.refreshTree(refreshData);
+                        if (sel) {
+                            sheet.setSelection(sel.row, sel.col, 1, sel.colCount);
+                        }
+                        self.refreshOperationValid();
+                    });
+                });
+            } else {
+                postData('update', updateData, function (result) {
+                    const refreshData = tree.loadPostData(result);
+                    self.refreshTree(refreshData);
+                    if (['up-move', 'down-move'].indexOf(type) > -1) {
+                        if (sel) {
+                            sheet.setSelection(tree.nodes.indexOf(node), sel.col, sel.rowCount, sel.colCount);
+                            SpreadJsObj.reloadRowsBackColor(sheet, [sel.row, tree.nodes.indexOf(node)]);
+                        }
+                    } else if (type === 'add') {
+                        const sel = sheet.getSelections()[0];
+                        if (sel) {
+                            sheet.setSelection(tree.nodes.indexOf(refreshData.create[0]), sel.col, sel.rowCount, sel.colCount);
+                            SpreadJsObj.reloadRowsBackColor(sheet, [sel.row, tree.nodes.indexOf(refreshData.create[0])]);
+                        }
+                    }
+                    self.refreshOperationValid();
+                });
+            }
+        }
+        // 事件
+        selectionChanged(e, info) {
+            if (info.newSelections) {
+                if (!info.oldSelections || info.newSelections[0].row !== info.oldSelections[0].row) {
+                    billsObj.refreshOperationValid();
+                }
+            }
+        }
+        topRowChanged(e, info) {
+            SpreadJsObj.saveTopAndSelect(info.sheet, billsObj.ckBillsSpread);
+        }
+        editStarting(e, info) {
+            if (!info.sheet.zh_setting || !info.sheet.zh_tree) return;
+
+            const col = info.sheet.zh_setting.cols[info.col];
+            const node = info.sheet.zh_tree.nodes[info.row];
+            if (!node) {
+                info.cancel = true;
+                return;
+            }
+
+            switch (col.field) {
+                case 'quantity':
+                case 'unit_price':
+                    info.cancel = (node.children && node.children.length > 0);
+                    break;
+            }
+        }
+        editEnded(e, info) {
+            if (!info.sheet.zh_setting) return;
+
+            const node = SpreadJsObj.getSelectObject(info.sheet);
+            const data = { id: node.id, stage_id: node.stage_id, tree_id: node.tree_id };
+            // 未改变值则不提交
+            const col = info.sheet.zh_setting.cols[info.col];
+            const orgValue = node[col.field];
+            const newValue = trimInvalidChar(info.editingText);
+            if (orgValue == info.editingText || ((!orgValue || orgValue === '') && (newValue === ''))) return;
+
+            if (info.editingText) {
+                const text = newValue;
+                if (col.type === 'Number') {
+                    const num = _.toNumber(text);
+                    if (_.isFinite(num)) {
+                        data[col.field] = num;
+                    } else {
+                        try {
+                            data[col.field] = ZhCalc.mathCalcExpr(transExpr(text));
+                        } catch(err) {
+                            toastr.error('输入的表达式非法');
+                            SpreadJsObj.reLoadRowData(info.sheet, info.row);
+                            return;
+                        }
+                    }
+                } else {
+                    data[col.field] = text;
+                }
+            } else {
+                data[col.field] = col.type === 'Number' ? 0 : '';
+            }
+            // 更新至服务器
+            postData('update', {target: 'ledger', postType: 'update', postData: data}, function (result) {
+                const refreshNode = billsObj.tree.loadPostData(result);
+                billsObj.refreshTree(refreshNode);
+            }, function () {
+                SpreadJsObj.reLoadRowData(info.sheet, info.row, 1);
+            });
+        }
+        deletePress (sheet) {
+            if (!sheet.zh_setting) return;
+            const sel = sheet.getSelections()[0], datas = [];
+            for (let iRow = sel.row; iRow < sel.row + sel.rowCount; iRow++) {
+                let bDel = false;
+                const node = sheet.zh_tree.nodes[iRow];
+                const data = sheet.zh_tree.getNodeKeyData(node);
+                for (let iCol = sel.col; iCol < sel.col + sel.colCount; iCol++) {
+                    const col = sheet.zh_setting.cols[iCol];
+                    const style = sheet.getStyle(iRow, iCol);
+                    if (style.locked) continue;
+
+                    data[col.field] = col.type === 'Number' ? 0 : '';
+                    bDel = true;
+                }
+                if (bDel) datas.push(data);
+            }
+            if (datas.length > 0) {
+                postData('update', {target: 'ledger', postType: 'update', postData: datas}, function (result) {
+                    const refreshNode = sheet.zh_tree.loadPostData(result);
+                    billsObj.refreshTree(refreshNode);
+                }, function () {
+                    SpreadJsObj.reLoadRowData(sheet, sel.row, sel.rowCount);
+                });
+            }
+        }
+        clipboardPasting(e, info) {
+            info.cancel = true;
+            const tree = info.sheet.zh_tree, setting = info.sheet.zh_setting;
+            if (!setting || !tree) return;
+
+            const pasteData = info.pasteData.html
+                ? SpreadJsObj.analysisPasteHtml(info.pasteData.html)
+                : (info.pasteData.text === ''
+                    ? SpreadJsObj.Clipboard.getAnalysisPasteText()
+                    : SpreadJsObj.analysisPasteText(info.pasteData.text));
+            const datas = [], filterNodes = [];
+
+            let level, filterRow = 0;
+            for (let iRow = 0; iRow < info.cellRange.rowCount; iRow ++) {
+                const curRow = info.cellRange.row + iRow;
+                const node = tree.nodes[curRow];
+                if (!node) continue;
+
+                if (!level) level = node.level;
+                if (node.level < level) break;
+
+                let bPaste = false;
+                const data = info.sheet.zh_tree.getNodeKeyData(node);
+                for (let iCol = 0; iCol < info.cellRange.colCount; iCol++) {
+                    const curCol = info.cellRange.col + iCol;
+                    const colSetting = info.sheet.zh_setting.cols[curCol];
+                    if (datas.length > 0) if (datas[0][colSetting.field] === undefined) continue;
+
+                    const newText = trimInvalidChar(pasteData[iRow-filterRow][iCol]);
+                    if (colSetting.type === 'Number') {
+                        const num = _.toNumber(newText);
+                        if (!_.isFinite(num)) {
+                            try {
+                                data[colSetting.field] = ZhCalc.mathCalcExpr(transExpr(newText));
+                            } catch (err) {
+                                toastr.error('粘贴的计算式非法');
+                                SpreadJsObj.reLoadRowData(info.sheet, info.row);
+                                return;
+                            }
+                        } else {
+                            data[colSetting.field] = num;
+                        }
+                    } else {
+                        data[colSetting.field] = newText;
+                    }
+
+                    bPaste = true;
+                }
+                if (bPaste) {
+                    datas.push(data);
+                } else {
+                    filterNodes.push(node);
+                }
+            }
+            if (datas.length > 0) {
+                postData('update', {target: 'ledger', postType: 'update', postData: datas}, function (result) {
+                    const refreshNode = tree.loadPostData(result);
+                    if (refreshNode.update) refreshNode.update = refreshNode.update.concat(filterNodes);
+                    billsObj.refreshTree(refreshNode);
+                }, function () {
+                    SpreadJsObj.reLoadRowData(info.sheet, info.cellRange.row, info.cellRange.rowCount);
+                });
+            } else {
+                SpreadJsObj.reLoadRowData(info.sheet, info.cellRange.row, info.cellRange.rowCount);
+            }
+        }
+        // 附加信息
+        getSum() {
+            return ZhCalc.sum(this.tree.children.map(x => { return x.total_price; }));
+        }
+        refreshExtraTable() {
+            const html = [];
+            const in_tp = this.extra.in_tp || 0;
+            const out_tp = this.getSum();
+            const profit = ZhCalc.sub(in_tp, out_tp);
+            const profit_rate = in_tp ? ZhCalc.mul(ZhCalc.div(profit, in_tp), 100, 2) : 0;
+            html.push(`<tr><td>项目收入</td><td><input type="number" class="form-control form-control-sm nospin" name="in_tp" value="${in_tp}" ${readOnly ? 'readOnly' : ''}></td></tr>`);
+            html.push(`<tr><td>项目支出</td><td>${out_tp || 0}</td></tr>`);
+            html.push(`<tr><td>利润</td><td>${profit}</td></tr>`);
+            html.push(`<tr><td>利润率</td><td>${profit_rate}</td></tr>`);
+            $('#duty-extra').html(html.join(''));
+        }
+        setExtra(extra) {
+            this.extra = extra;
+            this.refreshExtraTable();
+        }
+    }
+    const billsObj = new BillsObj();
+
+    const searchObj = $.billsSearch({
+        selector: '#search',
+        searchRangeStr: '项目节编号/清单编号/名称',
+        searchSpread: billsObj.spread,
+        keyId: 'tree_id',
+        resultSpreadSetting: {
+            cols: [
+                {title: '项目节编号', field: 'code', hAlign: 0, width: 120, formatter: '@'},
+                {title: '清单编号', field: 'b_code', hAlign: 0, width: 80, formatter: '@'},
+                {title: '名称', field: 'name', width: 150, hAlign: 0, formatter: '@'},
+                {title: '单位', field: 'unit', width: 50, hAlign: 1, formatter: '@'},
+            ],
+            emptyRows: 0,
+            headRows: 1,
+            headRowHeight: [32],
+            headColWidth: [30],
+            defaultRowHeight: 21,
+            headerFont: '12px 微软雅黑',
+            font: '12px 微软雅黑',
+            selectedBackColor: '#fffacd',
+            readOnly: true,
+        },
+    });
+    // 加载数据
+    postData('load', { filter: 'bills;extra' }, function(result) {
+        billsObj.loadData(result.bills);
+        billsObj.setExtra(result.extra);
+    });
+
+    // 展开收起标准清单
+    $('a', '#side-menu').bind('click', function (e) {
+        e.preventDefault();
+        const tab = $(this), tabPanel = $(tab.attr('content'));
+        // 展开工具栏、切换标签
+        if (!tab.hasClass('active')) {
+            // const close = $('.active', '#side-menu').length === 0;
+            $('a', '#side-menu').removeClass('active');
+            $('.tab-content .tab-select-show.tab-pane.active').removeClass('active');
+            tab.addClass('active');
+            tabPanel.addClass('active');
+            // $('.tab-content .tab-pane').removeClass('active');
+            showSideTools(tab.hasClass('active'));
+            if (tab.attr('content') === '#search') {
+                searchObj.spread.refresh();
+            }
+        } else { // 收起工具栏
+            tab.removeClass('active');
+            tabPanel.removeClass('active');
+            showSideTools(tab.hasClass('active'));
+        }
+        billsObj.spread.refresh();
+    });
+
+    // 工具栏spr
+    $.divResizer({
+        select: '#right-spr',
+        callback: function () {
+            billsObj.spread.refresh();
+            searchObj.spread.refresh();
+        }
+    });
+    // 导航Menu
+    $.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();
+            billsObj.spread.refresh();
+        }
+    });
+    // 显示层次
+    (function (select, sheet) {
+        $(select).click(function () {
+            if (!sheet.zh_tree) return;
+            const tag = $(this).attr('tag');
+            const tree = sheet.zh_tree;
+            setTimeout(() => {
+                showWaitingView();
+                switch (tag) {
+                    case "1":
+                    case "2":
+                    case "3":
+                    case "4":
+                        tree.expandByLevel(parseInt(tag));
+                        SpreadJsObj.refreshTreeRowVisible(sheet);
+                        break;
+                    case "last":
+                        tree.expandByCustom(() => { return true; });
+                        SpreadJsObj.refreshTreeRowVisible(sheet);
+                        break;
+                }
+                closeWaitingView();
+            }, 100);
+        });
+    })('a[name=showLevel]', billsObj.sheet);
+});

+ 56 - 15
app/public/js/cost_stage_ledger.js

@@ -16,28 +16,46 @@ $(document).ready(function() {
                 isLeaf: 'tree_is_leaf',
                 fullPath: 'tree_full_path',
                 rootId: -1,
-                calcFields: ['yf_tp', 'sf_tp', 'pay_tp', 'cut_tp', 'yf_excl_tax_tp', 'sf_excl_tax_tp'],
+                calcFields: [
+                    'yf_tp', 'sf_tp', 'pay_tp', 'cut_tp', 'yf_excl_tax_tp', 'sf_excl_tax_tp',
+                    'pre_yf_tp', 'pre_sf_tp', 'pre_pay_tp', 'pre_cut_tp', 'pre_yf_excl_tax_tp', 'pre_sf_excl_tax_tp',
+                    'end_yf_tp', 'end_sf_tp', 'end_pay_tp', 'end_cut_tp', 'end_yf_excl_tax_tp', 'end_sf_excl_tax_tp'
+                ],
                 keys: ['id', 'stage_id', 'tree_id'],
+                calcFun: function(node) {
+                    node.end_yf_tp = ZhCalc.add(node.yf_tp, node.pre_yf_tp);
+                    node.end_yf_excl_tax_tp = ZhCalc.add(node.yf_excl_tax_tp, node.pre_yf_excl_tax_tp);
+                    node.end_sf_tp = ZhCalc.add(node.sf_tp, node.pre_sf_tp);
+                    node.end_sf_excl_tax_tp = ZhCalc.add(node.sf_excl_tax_tp, node.pre_sf_excl_tax_tp);
+                    node.end_pay_tp = ZhCalc.add(node.pay_tp, node.pre_pay_tp);
+                    node.end_cut_tp = ZhCalc.add(node.cut_tp, node.pre_cut_tp);
+                },
             };
             this.tree = createNewPathTree('ledger', this.treeSetting);
             this.spreadSetting = {
                 cols: [
-                    {title: '编号', colSpan: '1', rowSpan: '1', field: 'code', hAlign: 0, width: 230, formatter: '@', cellType: 'tree'},
-                    {title: '名称', colSpan: '1', rowSpan: '1', field: 'name', hAlign: 0, width: 185, formatter: '@', cellType: 'autoTip'},
-                    {title: '单位', colSpan: '1', rowSpan: '1', field: 'unit', hAlign: 1, width: 50, formatter: '@', cellType: 'unit'},
-                    {title: '税率(%)', colSpan: '1', rowSpan: '1', field: 'tax', hAlign: 2, width: 80, type: 'Number'},
-                    {title: '付款金额', colSpan: '1', rowSpan: '1', field: 'pay_tp', hAlign: 2, width: 80, type: 'Number'},
-                    {title: '扣款金额', colSpan: '1', rowSpan: '1', field: 'cut_tp', hAlign: 2, width: 80, type: 'Number'},
-                    {title: '应付金额', colSpan: '1', rowSpan: '1', field: 'yf_tp', hAlign: 2, width: 80, type: 'Number', readOnly: true},
-                    {title: '实付金额', colSpan: '1', rowSpan: '1', field: 'sf_tp', hAlign: 2, width: 80, type: 'Number'},
-                    {title: '应付金额(不含税)', colSpan: '1', rowSpan: '1', field: 'yf_excl_tax_tp', hAlign: 2, width: 100, type: 'Number', readOnly: true},
-                    {title: '实付金额(不含税)', colSpan: '1', rowSpan: '1', field: 'sf_excl_tax_tp', hAlign: 2, width: 100, type: 'Number', readOnly: true},
-                    {title: '本期批注', colSpan: '1', rowSpan: '1', field: 'postil', hAlign: 0, width: 100, formatter: '@', cellType: 'ellipsisAutoTip'},
-                    {title: '备注', colSpan: '1', rowSpan: '1', field: 'memo', hAlign: 0, width: 120, formatter: '@', cellType: 'ellipsisAutoTip'},
+                    {title: '编号', colSpan: '1', rowSpan: '2', field: 'code', hAlign: 0, width: 230, formatter: '@', cellType: 'tree'},
+                    {title: '名称', colSpan: '1', rowSpan: '2', field: 'name', hAlign: 0, width: 185, formatter: '@', cellType: 'autoTip'},
+                    {title: '单位', colSpan: '1', rowSpan: '2', field: 'unit', hAlign: 1, width: 50, formatter: '@', cellType: 'unit'},
+                    {title: '税率(%)', colSpan: '1', rowSpan: '2', field: 'tax', hAlign: 2, width: 80, type: 'Number'},
+                    {title: '本期|付款金额', colSpan: '4|1', rowSpan: '1|1', field: 'pay_tp', hAlign: 2, width: 80, type: 'Number', visible: colSet.cur ? true : false},
+                    {title: '|扣款金额', colSpan: '|1', rowSpan: '|1', field: 'cut_tp', hAlign: 2, width: 80, type: 'Number', visible: colSet.cur ? true : false},
+                    {title: '|应付金额', colSpan: '|1', rowSpan: '|1', field: 'yf_tp', hAlign: 2, width: 80, type: 'Number', readOnly: true, visible: colSet.cur ? true : false},
+                    {title: '|实付金额', colSpan: '|1', rowSpan: '|1', field: 'sf_tp', hAlign: 2, width: 80, type: 'Number', visible: colSet.cur ? true : false},
+                    {title: '本期(不含税)|应付金额', colSpan: '2|1', rowSpan: '1|1', field: 'yf_excl_tax_tp', hAlign: 2, width: 100, type: 'Number', readOnly: true, visible: colSet.cur_excl_tax ? true : false},
+                    {title: '|实付金额', colSpan: '|1', rowSpan: '|1', field: 'sf_excl_tax_tp', hAlign: 2, width: 100, type: 'Number', readOnly: true, visible: colSet.cur_excl_tax ? true : false},
+                    {title: '截止本期|付款金额', colSpan: '4|1', rowSpan: '1|1', field: 'end_pay_tp', hAlign: 2, width: 80, type: 'Number', readOnly: true, visible: colSet.end ? true : false},
+                    {title: '|扣款金额', colSpan: '|1', rowSpan: '|1', field: 'end_cut_tp', hAlign: 2, width: 80, type: 'Number', readOnly: true, visible: colSet.end ? true : false},
+                    {title: '|应付金额', colSpan: '|1', rowSpan: '|1', field: 'end_yf_tp', hAlign: 2, width: 80, type: 'Number', readOnly: true, visible: colSet.end ? true : false},
+                    {title: '|实付金额', colSpan: '|1', rowSpan: '|1', field: 'end_sf_tp', hAlign: 2, width: 80, type: 'Number', readOnly: true, visible: colSet.end ? true : false},
+                    {title: '截止本期(不含税)|应付金额', colSpan: '2|1', rowSpan: '1|1', field: 'end_yf_excl_tax_tp', hAlign: 2, width: 100, type: 'Number', readOnly: true, visible: colSet.end_excl_tax ? true : false},
+                    {title: '|实付金额', colSpan: '|1', rowSpan: '|1', field: 'end_sf_excl_tax_tp', hAlign: 2, width: 100, type: 'Number', readOnly: true, visible: colSet.end_excl_tax ? true : false},
+                    {title: '本期批注', colSpan: '1', rowSpan: '2', field: 'postil', hAlign: 0, width: 100, formatter: '@', cellType: 'ellipsisAutoTip'},
+                    {title: '备注', colSpan: '1', rowSpan: '2', field: 'memo', hAlign: 0, width: 120, formatter: '@', cellType: 'ellipsisAutoTip'},
                 ],
                 emptyRows: 0,
-                headRows: 1,
-                headRowHeight: [32],
+                headRows: 2,
+                headRowHeight: [25, 25],
                 defaultRowHeight: 21,
                 headerFont: '12px 微软雅黑',
                 font: '12px 微软雅黑',
@@ -321,6 +339,7 @@ $(document).ready(function() {
                         }
                         self.refreshOperationValid();
                         billsTag.afterDeleteBills(refreshData.delete);
+                        costFile.deleteFileBySubNodeId(refreshData.delete.map(x => { return x.id; }));
                     });
                 });
             } else {
@@ -1259,6 +1278,28 @@ $(document).ready(function() {
             }
         }
     });
+    if (!readOnly) {
+        $('#cost-decimal').on('show.bs.modal', () => {
+            const decimalObjs = [$('#cost-tp'), $('#cost-excl-tax-tp')];
+            for (const obj of decimalObjs) {
+                obj.val(obj.attr('org'));
+            }
+        });
+        $('#cost-decimal-ok').click(() => {
+            const decimal = { tp: parseInt($('#cost-tp').val()), excl_tax_tp: parseInt($('#cost-excl-tax-tp').val()) };
+            postData('decimal', { decimal }, function(result) {
+                if (result.calc) {
+                    const refreshNode = billsObj.tree.loadPostData({ update: result.bills });
+                    billsObj.refreshTree(refreshNode);
+                    detailObj.data.updateDatas({ update: result.detail });
+                    detailObj.reloadDetailData();
+                }
+                $('#cost-tp').attr('org', decimal.tp);
+                $('#cost-excl-tax-tp').attr('org', decimal.excl_tax_tp);
+                $('#cost-decimal').modal('hide');
+            });
+        });
+    }
 
     const costFile = $.ledger_att({
         selector: '#fujian',

+ 69 - 4
app/public/js/ledger.js

@@ -62,6 +62,7 @@ const checkOption = {
 };
 
 $(document).ready(function() {
+    const aiTrans = AITrans();
     const unit = ledgerSpreadSetting.cols.find(x => { return x.field === 'unit'; });
     if (unit) unit.comboEdit = true;
     let stdXmj, stdGcl, dealBills, searchLedger, featureDisplay, posDisplay;
@@ -499,11 +500,25 @@ $(document).ready(function() {
                             ctrlObj.baseOpr('up-move');
                         }
                     },
+                    'ai-trans': {
+                        name: 'AI识别',
+                        disabled: function (key, opt) {
+                            const pos = SpreadJsObj.getSelectObject(posSheet);
+                            return !pos;
+                        },
+                        callback: function(key, opt) {
+                            const pos = SpreadJsObj.getSelectObject(posSheet);
+                            aiTrans.show('pos-calc-detail', pos.id);
+                        },
+                        visible: function(key, opt) {
+                            return is_debug;
+                        }
+                    }
                 }
             });
         }
 
-        return { detail, spread, sheet, refresh, loadCurDetailData, reloadCurDetailData }
+        return { detail, spread, sheet, refresh, loadCurDetailData, reloadCurDetailData, afterPostData: ctrlObj.afterPostData }
     })();
     const ancGclDetail = (function() {
         const detail = createAncGclDetail({ id: 'id', masterId: 'ag_id', firstId: 'lid', secondId: 'pid', sort: [['agd_order', 'asc']]});
@@ -1717,6 +1732,8 @@ $(document).ready(function() {
                 $('#dqjiedian').find('.check-all-file').prop('checked', false);
             }
             SpreadJsObj.saveTopAndSelect(info.sheet, ckBillsSpread);
+            const curNode = SpreadJsObj.getSelectObject(info.sheet);
+            aiTrans.setRelaInfo('bills', curNode, curNode);
         },
         topRowChanged(e, info) {
             SpreadJsObj.saveTopAndSelect(info.sheet, ckBillsSpread);
@@ -2824,6 +2841,29 @@ $(document).ready(function() {
         };
         billsContextMenuOptions.items.sprImport = '----';
     }
+    billsContextMenuOptions.items.exportCpd = {
+        name: '导出台账数据',
+        icon: '',
+        callback: function (key, opt) {
+            window.open(window.location.pathname + '/cpd');
+        }
+    };
+    if (!readOnly) {
+        billsContextMenuOptions.items.importCpd = {
+            name: '导入台账数据',
+            icon: '',
+            callback: function (key, opt, menu, e) {
+                BaseImportFile.show({
+                    validList: ['.cpd'],
+                    url: window.location.pathname + '/cpd/load',
+                    afterImport: function() {
+                        window.location.reload();
+                    }
+                });
+            },
+        }
+    }
+    billsContextMenuOptions.items.cpdSpr = '----';
     billsContextMenuOptions.items.tag = {
         name: '书签',
         callback: function (key, opt, menu, e) {
@@ -3356,6 +3396,7 @@ $(document).ready(function() {
 
             const hint = {
                 expr: {type: 'warning', msg: '粘贴了表达式非法,已过滤'},
+                hasDetail: {type: 'warning', msg: '存在设计量明细,不可粘贴'}
             };
             if (info.sheet.zh_setting) {
                 const data = [];
@@ -3370,6 +3411,8 @@ $(document).ready(function() {
                 }
                 const lastOrder = sortData.length > 0 ? sortData[sortData.length - 1].porder + 1 : 1;
                 let filterExprFields = [];
+                const ledger = SpreadJsObj.getSelectObject(ledgerSpread.getActiveSheet());
+                if (ledger.calc_template) filterExprFields.push('sgfh_expr', 'sgfh_qty');
                 for (let iRow = 0; iRow < info.cellRange.rowCount; iRow++) {
                     let bPaste = false;
                     const curRow = info.cellRange.row + iRow;
@@ -3441,6 +3484,7 @@ $(document).ready(function() {
             posOperationObj.refreshOperationValid(posSpread.getActiveSheet());
             posCalcDetail.loadCurDetailData();
             ancGclObj.loadCurAncillaryGcl();
+            aiTrans.setRelaInfo('pos', SpreadJsObj.getSelectObject(info.sheet), SpreadJsObj.getSelectObject(ledgerSheet));
         },
         addPegs: function (pegs) {
             if (!pegs || pegs.length <= 0) return;
@@ -3480,6 +3524,16 @@ $(document).ready(function() {
             }
             return copyBlockList;
         },
+        afterPostData: function( result) {
+            pos.updateDatas(result.pos);
+            if (result.posCalcDetail) posCalcDetail.detail.updateDatas(result.posCalcDetail);
+            const ledgerResult = ledgerTree.loadPostData(result.ledger);
+            treeOperationObj.refreshTree(ledgerSheet, ledgerResult);
+            if (result.ancGcl) ancGcl.updateDatas(result.ancGcl);
+            if (result.ancGclDetail) ancGclDetail.detail.updateDatas(result.ancGclDetail);
+            removeLocalCache(copyBlockTag);
+            posOperationObj.loadCurPosData();
+        },
         pasteBlock: function(copyInfo) {
             const node = SpreadJsObj.getSelectObject(ledgerSheet);
             if (!node) {
@@ -3490,7 +3544,7 @@ $(document).ready(function() {
                 toastr.error('父节点不可插入计量单元');
                 SpreadJsObj.reLoadRowData(info.sheet, info.row);
                 return;
-            }else if (!node.b_code || node.b_code === '') {
+            } else if (!node.b_code || node.b_code === '') {
                 toastr.error('项目节不可插入计量单元');
                 SpreadJsObj.reLoadRowData(info.sheet, info.row);
                 return;
@@ -3499,7 +3553,7 @@ $(document).ready(function() {
             postData('/tender/' + getTenderId() + '/pos/update', data, function (result) {
                 pos.updateDatas(result.pos);
                 if (result.posCalcDetail) posCalcDetail.detail.updateDatas(result.posCalcDetail);
-                const ledgerResult = ledgerTree.loadPostData(data.ledger);
+                const ledgerResult = ledgerTree.loadPostData(result.ledger);
                 treeOperationObj.refreshTree(ledgerSheet, ledgerResult);
                 if (result.ancGcl) ancGcl.updateDatas(result.ancGcl);
                 if (result.ancGclDetail) ancGclDetail.detail.updateDatas(result.ancGclDetail);
@@ -3973,6 +4027,7 @@ $(document).ready(function() {
         },
         selectionChanged: function(e, info) {
             ancGclDetail.loadCurDetailData();
+            aiTrans.setRelaInfo('ancGcl', SpreadJsObj.getSelectObject(info.sheet), SpreadJsObj.getSelectObject(ledgerSheet));
         },
         removeCalcTemplate: function(node) {
             const ancGclUpdateData = { calcTmpl: { id: node.id, calc_template: '' } };
@@ -6455,7 +6510,17 @@ $(document).ready(function() {
             $('#binddskuser').modal('show');
         }
     });
+    $('.ai-btn').click(function() {
+        aiTrans.billsIdentify(function(result) {
+            if (result === '无') {
+                toastr.warning('该清单类型暂不支持ai识别');
+                return;
+            }
 
+            if (aiTrans.rela_type() === 'pos') aiTrans.transDetail(posCalcDetail.afterPostData);
+            if (aiTrans.rela_type() === 'bills') aiTrans.transDetail(posOperationObj.afterPostData);
+        });
+    });
 });
 // 生成当前节点列表
 function getNodeList(node) {
@@ -6481,7 +6546,7 @@ function getNodeList(node) {
   }
   $('#nodelist-table').html(html);
   $('#nodelist-table').on('click', 'tr', function() {
-      $('#nodelist-table tr').removeClass('bg-light')
+      $('#nodelist-table tr').removeClass('bg-light');
       $(this).addClass('bg-light')
   })
 }

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

@@ -270,6 +270,10 @@ $(document).ready(() => {
                 return data.mid === materialID;
             } else {
                 const mlInfo = _.find(materialListData, { mb_id: data.id });
+                if (data.mid === materialID && mlInfo === undefined) {
+                    const listGclInfo = _.find(gclList, { mb_id: data.id });
+                    return listGclInfo === undefined;
+                }
                 return data.mid === materialID && mlInfo === undefined;
             }
         },

+ 12 - 1
app/public/js/path_tree.js

@@ -2216,6 +2216,7 @@ const createAncillaryGcl = function (setting) {
                 if (!this.masterIndex[masterKey]) this.masterIndex[masterKey] = [];
                 this.masterIndex[masterKey].push(d);
                 if (resort.indexOf(masterKey) < 0) resort.push(masterKey);
+                if (this.setting.calcFun) this.setting.calcFun(d);
             }
         }
 
@@ -2233,8 +2234,8 @@ const createAncillaryGcl = function (setting) {
                 }
                 const masterKey = this.itemPre + item[this.setting.masterId];
                 if (resort.indexOf(masterKey) < 0) resort.push(masterKey);
+                if (this.setting.calcFun) this.setting.calcFun(d);
             }
-
         }
 
         /**
@@ -2330,6 +2331,16 @@ const createAncillaryGcl = function (setting) {
                 fun(prop.substring(3, prop.length), this.masterIndex[prop]);
             }
         }
+
+        /**
+         * 计算全部
+         */
+        calculateAll() {
+            if (!this.setting.calcFun)  return;
+            for (const d of this.datas) {
+                this.setting.calcFun(d);
+            }
+        }
     }
 
     return new AncillaryGcl(setting);

+ 117 - 0
app/public/js/shares/ai_trans.js

@@ -0,0 +1,117 @@
+const AITrans = function() {
+    const transResultObj = {
+        spread: null,
+        transSheetData: [],
+        initSpread: function() {
+            if (!this.spread) {
+                this.spread = SpreadJsObj.createNewSpread($('#ai-trans-grid-spread')[0]);
+                this.spread.options.tabStripVisible = true;
+            }
+        }
+    };
+    $('#ai-trans-grid').bind('shown.bs.modal', function() {
+        transResultObj.spread.refresh();
+    });
+    $('#ai-trans-grid-file').change(function() {
+        const files = this.files;
+        if (!files || files.length === 0) return;
+
+        const formData = new FormData();
+        formData.append('rela_type', transResultObj.rela_type);
+        formData.append('rela_id', transResultObj.rela_id);
+        for (const file of files) {
+            if (file === undefined) {
+                toastr.error('未选择上传文件!');
+                return false;
+            }
+            const filesize = file.size;
+            if (filesize > 50 * 1024 * 1024) {
+                toastr.error('存在上传文件大小过大!');
+                return false;
+            }
+            const fileext = '.' + file.name.toLowerCase().split('.').splice(-1)[0];
+            if (whiteList.indexOf(fileext) === -1) {
+                toastr.error('只能上传指定格式的附件!');
+                return false;
+            }
+            formData.append('size', filesize);
+            formData.append('file[]', file);
+        }
+        postDataWithFile('ai-trans', formData, function (data) {
+            transResultObj.transSheetData.length = 0;
+            const sheetParts = data.split('\n==++==\n');
+            for (const sp of sheetParts) {
+                const rows = sp.split('\n').filter(x => { return !!x; }).map(r => { return r.split('|')});
+                transResultObj.transSheetData.push(rows);
+            }
+            transResultObj.spread.setSheetCount(transResultObj.transSheetData.length);
+            for (const [i, sheetData] of transResultObj.transSheetData.entries()) {
+                SpreadJsObj.loadSimpleSheetData(transResultObj.spread.getSheet(i), sheetData);
+            }
+        });
+    });
+    const show = function() {
+        if (!transResultObj.rela_id) toastr.warning('请先选择清单、计量单元、附属工程量后再使用');
+
+        transResultObj.initSpread();
+        $('#ai-trans-grid').modal('show');
+    };
+
+    $('#ai-trans-ok').click(function() {
+        const files = $('#ai-trans-file')[0].files;
+        if (!files || files.length === 0) return;
+
+        const formData = new FormData();
+        formData.append('rela_type', transResultObj.rela_type);
+        formData.append('rela_id', transResultObj.rela_id);
+        if (transResultObj.rela_type === 'ancGcl') {
+            formData.append('rela_template', transResultObj.rela_node.calc_template);
+        } else {
+            formData.append('rela_template', transResultObj.rela_bills.calc_template);
+        }
+        formData.append('rela_bills_name', transResultObj.rela_bills.name);
+        formData.append('rela_keyword', $('#ai-trans-keyword').val());
+        formData.append('rela_billsType', transResultObj.billsType);
+        for (const file of files) {
+            if (file === undefined) {
+                toastr.error('未选择图片!');
+                return false;
+            }
+            const filesize = file.size;
+            if (filesize > 50 * 1024 * 1024) {
+                toastr.error('选择图片大小过大!');
+                return false;
+            }
+            const fileext = '.' + file.name.toLowerCase().split('.').splice(-1)[0];
+            if (whiteList.indexOf(fileext) === -1) {
+                toastr.error('只能上传指定格式的图片!');
+                return false;
+            }
+            formData.append('size', filesize);
+            formData.append('file[]', file);
+        }
+        postDataWithFile(window.location.pathname + '/aiComplie', formData, function (data) {
+            if (transResultObj.callback) transResultObj.callback(data);
+            $('#ai-trans').modal('hide');
+        });
+    });
+    const transDetail = function(callback) {
+        transResultObj.callback = callback;
+        $('#ai-trans').modal('show');
+    };
+    const billsIdentify = function (callback) {
+        if (!transResultObj.rela_id) toastr.warning('请先选择清单、计量单元、附属工程量后再使用');
+        if (!transResultObj.rela_bills) toastr.warning('未选择清单,或者选择的计量单元、附属工程量找不到所属清单');
+        postData('ai/billsIdentify', { name: transResultObj.rela_bills.name, unit: transResultObj.rela_bills.unit }, function (data) {
+            transResultObj.billsType = data;
+            callback(data);
+        });
+    };
+    const setRelaInfo = function(rela_type, rela_node, rela_bills) {
+        transResultObj.rela_type = rela_type;
+        transResultObj.rela_node = rela_node;
+        transResultObj.rela_id = rela_node ? rela_node.id : undefined;
+        transResultObj.rela_bills = rela_bills;
+    };
+    return { show, setRelaInfo, billsIdentify, transDetail, rela_type: function() {return transResultObj.rela_type} }
+};

+ 1 - 1
app/public/js/shares/common_audit.js

@@ -203,7 +203,7 @@ $(document).ready(function () {
 
         const btn = $(this);
         $.ajax({
-            url: '/profile/code',
+            url: '/profile/code?_csrf_j=' + csrf,
             type: 'post',
             data: { mobile: authMobile, type: 'shenpi' },
             dataTye: 'json',

+ 38 - 2
app/public/js/spreadjs_rela/spreadjs_zh.js

@@ -211,7 +211,7 @@ const SpreadJsObj = {
             return sheet.getRange(target.row, target.col, 1, 1);
         } else if (target.hitTestType === spreadNS.SheetArea.corner) {
             return sheet.getRange(-1, -1, sheet.getRowCount(), sheet.getColumnCount());
-        };
+        }
     },
     getCellInSelections: function (selections, row, col) {
         const count = selections.length;
@@ -1002,7 +1002,7 @@ const SpreadJsObj = {
                 // 单元格重新写入数据
                 this._loadRowData(sheet, data, row);
                 sheet.setRowVisible(row, data.visible);
-            };
+            }
             this.endMassOperation(sheet);
         } catch (err) {
             this.endMassOperation(sheet);
@@ -1146,6 +1146,42 @@ const SpreadJsObj = {
         this.protectedSheet(sheet);
         this.reLoadSheetData(sheet, force);
     },
+    loadSimpleSheetData: function(sheet, data, font) {
+        const sortData = data;
+        this.beginMassOperation(sheet);
+        try {
+            sheet.setRowCount(0, spreadNS.SheetArea.viewport);
+            sheet.clear(0, 0, sheet.getRowCount(), sheet.getColumnCount(), spreadNS.SheetArea.viewport, spreadNS.StorageType.data);
+            sheet.getRange(0, 0, sheet.getRowCount(), sheet.getColumnCount()).backColor(sheet.getDefaultStyle().backColor);
+            // 设置总行数
+            const totalRow = sortData.length;
+            sheet.setRowCount(totalRow, spreadNS.SheetArea.viewport);
+            const totalCol = sortData.map(x => { return x.length; }).reduce((x, y) => { return math.max(x, y)});
+            if (sortData && sortData.length > 0) sheet.setColumnCount(totalCol, spreadNS.SheetArea.viewport);
+
+            if (sortData) {
+                // 单元格写入数据
+                sortData.forEach(function (d, i) {
+                    // 单元格重新写入数据
+                    if (!d) return;
+
+                    d.forEach(function (value, j) {
+                        const cell = sheet.getCell(i, j);
+                        cell.value(value).vAlign(1).hAlign(1);
+                        font && cell.font(font);
+                        cell.setBorder(sheet.borderLine, {all: true});
+                    });
+                });
+                sortData[0].forEach(function(value, j) {
+                    sheet.autoFitColumn(j);
+                })
+            }
+            this.endMassOperation(sheet);
+        } catch (err) {
+            console.log(err);
+            this.endMassOperation(sheet);
+        }
+    },
     /**
      * 获取复制数据HTML格式(过滤不可见单元格)
      * @param {GC.Spread.Sheets.Worksheet} sheet

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

@@ -2763,9 +2763,9 @@ $(document).ready(() => {
             calcField: { qty: 'contract_qty', tp: 'contract_tp' }, calcType: TreeExprCalc.calcType.sortCache,
             exprFieldsIndex: {
                 tzsl: 'quantity', tzje: 'total_price', qysl: 'deal_qty', qyje: 'deal_tp',
-                bqhtje: 'contract_tp',
+                bqhtje: 'contract_tp', bqwcje: 'gather_tp',
             },
-            exprCacheKey: ['bqhtje'],
+            exprCacheKey: ['bqhtje', 'bqwcje'],
         });
         // 加载中间计量
         stageIm.init(tenderInfo, stage, imType, tenderInfo.decimal);

+ 110 - 72
app/public/report/js/rpt_main.js

@@ -821,6 +821,7 @@ let rptControlObj = {
     },
     downloadExcelReport: async function(pageDataArr, pageSize, rpt_names, signatureRelArr) {
         const MAX_BRK_PAGE = 300;
+        const MAX_SHEETS = 255; // 这个数据是excel能支持的sheet数量(测试时可以调低看逻辑),超过后需要拆分excel表 (正常极限为255,这里适当小一些)
         const __sleep = (n) => {
             var start = new Date().getTime();
             while(true)  if(new Date().getTime()-start > n) break;
@@ -842,6 +843,23 @@ let rptControlObj = {
             newPageData.items = [];
             return newPageData;
         };
+        const _splitPageDataOverMax = (dtlPgDatas, sheetNames, dtlRptName) => {
+            // 拆分sheets
+            const newDtlPgDatas = [];
+            const newSheetNames = [];
+            const newDtlRptNames = [];
+            const allAmt = dtlPgDatas.length;
+            for (let modIdx = 0; modIdx < allAmt; modIdx++) {
+                if (modIdx % MAX_SHEETS === 0) {
+                    newDtlPgDatas.push([]);
+                    newSheetNames.push([]);
+                    newDtlRptNames.push(`${dtlRptName}${newDtlPgDatas.length - 1}`);
+                }
+                newDtlPgDatas[newDtlPgDatas.length - 1].push(dtlPgDatas[modIdx]);
+                newSheetNames[newSheetNames.length - 1].push(sheetNames[modIdx]);
+            }
+            return { newDtlPgDatas, newSheetNames, newDtlRptNames };
+        };
         const _splitBothMasterDetailPageData = (pageData) => {
             const newPageDatas = [];
             // const fileNames = [];
@@ -910,7 +928,15 @@ let rptControlObj = {
                                 if (cntAmt > 0) subSignatureRelArr.push([]);
                                 cntAmt++;
                             }
-                            await rptControlObj.downloadExcelReportInOneBook(dtlPgDatas, pageSize, subSignatureRelArr, sheetNames, dtlRptName);
+                            if (dtlPgDatas.length > MAX_SHEETS) {
+                                // 拆分sheets
+                                const { newDtlPgDatas, newSheetNames, newDtlRptNames } = _splitPageDataOverMax(dtlPgDatas, sheetNames, dtlRptName);
+                                for (let curIdx = 0; curIdx < newDtlRptNames.length; curIdx++) {
+                                    await rptControlObj.downloadExcelReportInOneBook(newDtlPgDatas[curIdx], pageSize, subSignatureRelArr, newSheetNames[curIdx], newDtlRptNames[curIdx]);
+                                }
+                            } else {
+                                await rptControlObj.downloadExcelReportInOneBook(dtlPgDatas, pageSize, subSignatureRelArr, sheetNames, dtlRptName);
+                            }
                         }
                     } else {
                         // 只有单独的分表、分sheet
@@ -922,7 +948,15 @@ let rptControlObj = {
                             for (let ssrIdx = 1; ssrIdx < newPageDatas.length; ssrIdx++) {
                                 subSignatureRelArr.push([]);
                             }
-                            await rptControlObj.downloadExcelReportInOneBook(newPageDatas, pageSize, subSignatureRelArr, sheetNames, rptName);
+                            if (newPageDatas.length > MAX_SHEETS) {
+                                // 拆分sheets
+                                const { newDtlPgDatas, newSheetNames, newDtlRptNames } = _splitPageDataOverMax(newPageDatas, sheetNames, rptName);
+                                for (let curIdx = 0; curIdx < newDtlRptNames.length; curIdx++) {
+                                    await rptControlObj.downloadExcelReportInOneBook(newDtlPgDatas[curIdx], pageSize, subSignatureRelArr, newSheetNames[curIdx], newDtlRptNames[curIdx]);
+                                }
+                            } else {
+                                await rptControlObj.downloadExcelReportInOneBook(newPageDatas, pageSize, subSignatureRelArr, sheetNames, rptName);
+                            }
                         } 
                         if (pageDataArr[currentIndex].excelFileExportOptions) {
                             const singlePage = true;
@@ -1156,6 +1190,77 @@ let rptControlObj = {
     getPDFEx: async function (isOneFile = false) {
         this.getOutputCommon('PDF', isOneFile);
     },
+    getCommonRptNameInAllOneCase: function() {
+        let allRptName = '';
+        // 重新整理报表名称:有期就  {标段名称}-{模块}-{期数},无期就{标段名称}-{模块}
+        switch (current_stage_id) {
+            case -100:
+                allRptName = `${TENDER_NAME}-安全生产费`;
+                if (STAGE_ORDER > 0) {
+                    allRptName = `${allRptName}-${STAGE_ORDER}`;
+                }
+                break;
+            case -150:
+                allRptName = `${TENDER_NAME}-安全计量`;
+                if (STAGE_ORDER > 0) {
+                    allRptName = `${allRptName}-${STAGE_ORDER}`;
+                }
+                break;
+            case -200:
+                // 动态决算
+                allRptName = `${TENDER_NAME}-动态决算`;
+                break;
+            case -300:
+                // 变更令
+                allRptName = `${TENDER_NAME}-变更令-${BGL_OBJ?.currentBz?.code || ''}`;
+                break;
+            case -301:
+                // 变更方案
+                allRptName = `${TENDER_NAME}-变更方案-${BGL_OBJ?.currentBz?.code || ''}`;
+                break;
+            case -302:
+                // 变更立项
+                allRptName = `${TENDER_NAME}-变更立项-${BGL_OBJ?.currentBz?.code || ''}`;
+                break;
+            case -303:
+                // 变更申请
+                allRptName = `${TENDER_NAME}-变更申请-${BGL_OBJ?.currentBz?.code || ''}`;
+                break;
+            case -400:
+                // 预付款
+                allRptName = `${TENDER_NAME}-预付款`;
+                if (current_advance_order > 0) {
+                    allRptName = `${allRptName}-${current_advance_order}`;
+                }
+                break;
+            case -500:
+                // 材差
+                allRptName = `${TENDER_NAME}-材差`;
+                if (current_material_order > 0) {
+                    allRptName = `${allRptName}-${current_material_order}`;
+                }
+                break;
+            case -600:
+                // 合同
+                allRptName = `${TENDER_NAME}-合同`;
+                break;
+            case -700:
+                // 合同支付
+                allRptName = `${TENDER_NAME}-合同支付`;
+                if (STAGE_ORDER > 0) {
+                    allRptName = `${allRptName}-${STAGE_ORDER}`;
+                }
+                break;
+            default:
+                // 计量期
+                allRptName = `${TENDER_NAME}-计量期`;
+                if (current_stage_order > 0) {
+                    allRptName = `${allRptName}-${current_stage_order}`;
+                }
+                break;
+        }
+        return allRptName;
+    },
     getOutputCommon: async function(outputType, isWholeOne = false) {
         // 把导出Excel/PDF初步统一起来
         const pageShowPrp = (outputType === 'Excel') ? 'closeExportExcel' : 'closeExportPdf';
@@ -1250,81 +1355,14 @@ let rptControlObj = {
                         }
                         if (outputType === 'Excel') {
                             if (isWholeOne) {
-                                me.downloadExcelReportInOneBook(pageDataArr, pageSize, signatureRelArr, rpt_names);
+                                // 这里不考虑sheets数量上限了,因为用户选不了那么多表
+                                me.downloadExcelReportInOneBook(pageDataArr, pageSize, signatureRelArr, rpt_names, me.getCommonRptNameInAllOneCase());
                             } else {
                                 me.downloadExcelReport(pageDataArr, pageSize, rpt_names, signatureRelArr);
                             }
                         } else if (outputType === 'PDF') {
                             if (isWholeOne) {
-                                let allRptName = '';
-                                // 重新整理报表名称:有期就  {标段名称}-{模块}-{期数},无期就{标段名称}-{模块}
-                                switch (current_stage_id) {
-                                    case -100:
-                                        allRptName = `${TENDER_NAME}-安全生产费`;
-                                        if (STAGE_ORDER > 0) {
-                                            allRptName = `${allRptName}-${STAGE_ORDER}`;
-                                        }
-                                        break;
-                                    case -150:
-                                        allRptName = `${TENDER_NAME}-安全计量`;
-                                        if (STAGE_ORDER > 0) {
-                                            allRptName = `${allRptName}-${STAGE_ORDER}`;
-                                        }
-                                        break;
-                                    case -200:
-                                        // 动态决算
-                                        allRptName = `${TENDER_NAME}-动态决算`;
-                                        break;
-                                    case -300:
-                                        // 变更令
-                                        allRptName = `${TENDER_NAME}-变更令`;
-                                        break;
-                                    case -301:
-                                        // 变更方案
-                                        allRptName = `${TENDER_NAME}-变更方案`;
-                                        break;
-                                    case -302:
-                                        // 变更立项
-                                        allRptName = `${TENDER_NAME}-变更立项`;
-                                        break;
-                                    case -303:
-                                        // 变更申请
-                                        allRptName = `${TENDER_NAME}-变更申请`;
-                                        break;
-                                    case -400:
-                                        // 预付款
-                                        allRptName = `${TENDER_NAME}-预付款`;
-                                        if (current_advance_order > 0) {
-                                            allRptName = `${allRptName}-${current_advance_order}`;
-                                        }
-                                        break;
-                                    case -500:
-                                        // 材差
-                                        allRptName = `${TENDER_NAME}-材差`;
-                                        if (current_material_order > 0) {
-                                            allRptName = `${allRptName}-${current_material_order}`;
-                                        }
-                                        break;
-                                    case -600:
-                                        // 合同
-                                        allRptName = `${TENDER_NAME}-合同`;
-                                        break;
-                                    case -700:
-                                        // 计量管理
-                                        allRptName = `${TENDER_NAME}-计量管理`;
-                                        if (STAGE_ORDER > 0) {
-                                            allRptName = `${allRptName}-${STAGE_ORDER}`;
-                                        }
-                                        break;
-                                    default:
-                                        // 计量期
-                                        allRptName = `${TENDER_NAME}-计量期`;
-                                        if (current_stage_order > 0) {
-                                            allRptName = `${allRptName}-${current_stage_order}`;
-                                        }
-                                        break;
-                                }
-                                rpt_names[0] = allRptName;
+                                rpt_names[0] = me.getCommonRptNameInAllOneCase();
                             }
                             downloadPDFReport(pageDataArr, pageSize, rpt_names, signatureRelArr, result.signatureRelInfo, refRptTplIds, STAGE_AUDIT, isWholeOne);
                         }

+ 3 - 1
app/reports/rpt_component/jpc_flow_tab.js

@@ -1176,6 +1176,7 @@ JpcFlowTabSrv.prototype.createNew = function() {
         //     假如:第一列的前3个数据(1、2、3)是相同的,第二列中第2、3、4行数据相同,那么第二列只能合并2、3行的数据,不能合并到第四行
         //     同理如此类推第三列...n列
         // 另:要考虑紧密输出情况,需要重新拆分数据
+        const regSplit = new RegExp('\\|', 'g');
         const _splitValues = function(oCell) {
             // JpcFieldHelper.setValue(data_field, theRecIdx, value);
             const values = (typeof oCell.Value === 'string') ? oCell.Value.split('|') : [oCell.Value];
@@ -1313,13 +1314,14 @@ JpcFlowTabSrv.prototype.createNew = function() {
                             if (preCell.Value === '') {
                                 preCell = rstPageCells[cacheObj.hCache[hStr][cIdx]];
                             } else {
-                                if (preCell.Value === rstPageCells[cacheObj.hCache[hStr][cIdx]].Value) {
+                                if ((preCell.Value || ' ').replace(regSplit, '') === (rstPageCells[cacheObj.hCache[hStr][cIdx]].Value || '  ').replace(regSplit, '')) {
                                     if (preCell[JV.PROP_AREA][JV.PROP_RIGHT] < rstPageCells[cacheObj.hCache[hStr][cIdx]][JV.PROP_AREA][JV.PROP_RIGHT]) {
                                         preCell[JV.PROP_AREA][JV.PROP_RIGHT] = rstPageCells[cacheObj.hCache[hStr][cIdx]][JV.PROP_AREA][JV.PROP_RIGHT];
                                     } else {
                                         preCell[JV.PROP_AREA][JV.PROP_LEFT] = rstPageCells[cacheObj.hCache[hStr][cIdx]][JV.PROP_AREA][JV.PROP_LEFT];
                                     }
                                     removeCellIds.push(cacheObj.hCache[hStr][cIdx]);
+                                    preCell.Value = (preCell.Value || '').replace(regSplit, '');
                                 } else {
                                     preCell = rstPageCells[cacheObj.hCache[hStr][cIdx]];
                                 }

+ 28 - 1
app/router.js

@@ -342,6 +342,10 @@ module.exports = app => {
     app.post('/sp/:id/contract/tender/:tid/detail/:type/:cid/supplement/:csid/file/upload', sessionAuth, subProjectCheck, contractCheck, 'contractController.uploadFile');
     app.post('/sp/:id/contract/tender/:tid/detail/:type/:cid/supplement/:csid/file/delete', sessionAuth, subProjectCheck, contractCheck, 'contractController.deleteFile');
     app.get('/sp/:id/contract/tender/:tid/detail/:type/:cid/supplement/:csid/file/:fid/download', sessionAuth, subProjectCheck, contractCheck, 'contractController.downloadFile');
+    app.post('/sp/:id/contract/tender/:tid/detail/ai/upload', sessionAuth, subProjectCheck, contractCheck, 'contractController.uploadAndAi');
+    app.post('/sp/:id/contract/tender/:tid/detail/:type/ai/upload', sessionAuth, subProjectCheck, contractCheck, 'contractController.uploadAndAi');
+    app.post('/sp/:id/contract/tender/:tid/detail/ai/edit', sessionAuth, subProjectCheck, contractCheck, 'contractController.editAndAi');
+    app.post('/sp/:id/contract/tender/:tid/detail/:type/ai/edit', sessionAuth, subProjectCheck, contractCheck, 'contractController.editAndAi');
     // 项目合同管理
     app.get('/sp/:id/contract', sessionAuth, subProjectCheck, contractCheck, 'contractController.detail');
     app.post('/sp/:id/contract/audit/save', sessionAuth, subProjectCheck, contractCheck, 'contractController.auditSave');
@@ -369,6 +373,10 @@ module.exports = app => {
     app.post('/sp/:id/contract/detail/:type/:cid/supplement/:csid/file/upload', sessionAuth, subProjectCheck, contractCheck, 'contractController.uploadFile');
     app.post('/sp/:id/contract/detail/:type/:cid/supplement/:csid/file/delete', sessionAuth, subProjectCheck, contractCheck, 'contractController.deleteFile');
     app.get('/sp/:id/contract/detail/:type/:cid/supplement/:csid/file/:fid/download', sessionAuth, subProjectCheck, contractCheck, 'contractController.downloadFile');
+    app.post('/sp/:id/contract/detail/ai/upload', sessionAuth, subProjectCheck, contractCheck, 'contractController.uploadAndAi');
+    app.post('/sp/:id/contract/detail/:type/ai/upload', sessionAuth, subProjectCheck, contractCheck, 'contractController.uploadAndAi');
+    app.post('/sp/:id/contract/detail/ai/edit', sessionAuth, subProjectCheck, contractCheck, 'contractController.editAndAi');
+    app.post('/sp/:id/contract/detail/:type/ai/edit', sessionAuth, subProjectCheck, contractCheck, 'contractController.editAndAi');
 
 
     // 资料归集-列表
@@ -430,6 +438,7 @@ module.exports = app => {
     app.post('/sp/:id/budget/:btype/ybp', sessionAuth, subProjectCheck, budgetCheck, 'budgetController.uploadYbp');
     app.post('/sp/:id/budget/:btype/dsk', sessionAuth, subProjectCheck, budgetCheck, 'budgetController.importDsk');
     app.post('/sp/:id/budget/decimal', sessionAuth, subProjectCheck, budgetCheck, 'budgetController.decimal');
+    app.post('/sp/:id/budget/extra-info', sessionAuth, subProjectCheck, budgetCheck, 'budgetController.extraInfo');
 
     // 进度管理-标段进度
     app.get('/sp/:id/schedule/tender', sessionAuth, subProjectCheck, 'scheduleController.tender');
@@ -551,25 +560,31 @@ module.exports = app => {
     app.post('/sp/:id/cost/member', sessionAuth, subProjectCheck, projectManagerCheck, 'costController.member');
     app.post('/sp/:id/cost/memberSave', sessionAuth, subProjectCheck, projectManagerCheck, 'costController.memberSave');
     app.post('/sp/:id/cost/templateSet', sessionAuth, subProjectCheck, projectManagerCheck, 'subProjController.templateSet');
+    // app.get('/sp/:id/cost/tender/:tid/report', sessionAuth, subProjectCheck, tenderCheck, tenderPermissionCheck, 'reportController.indexForCostManager');
     app.get('/sp/:id/cost/tender/:tid/ledger', sessionAuth, subProjectCheck, tenderCheck, tenderPermissionCheck, 'costController.ledger');
     app.get('/sp/:id/cost/tender/:tid/book', sessionAuth, subProjectCheck, tenderCheck, tenderPermissionCheck, 'costController.book');
     app.get('/sp/:id/cost/tender/:tid/analysis', sessionAuth, subProjectCheck, tenderCheck, tenderPermissionCheck, 'costController.analysis');
     app.post('/sp/:id/cost/tender/:tid/addStage', sessionAuth, subProjectCheck, tenderCheck, tenderPermissionCheck, 'costController.addStage');
     app.post('/sp/:id/cost/tender/:tid/delStage', sessionAuth, subProjectCheck, tenderCheck, tenderPermissionCheck, 'costController.delStage');
     app.post('/sp/:id/cost/tender/:tid/:stype/auditors', sessionAuth, subProjectCheck, tenderCheck, tenderPermissionCheck, 'costController.loadAuditors');
+    app.post('/sp/:id/cost/tender/:tid/colSet', sessionAuth, subProjectCheck, tenderCheck, tenderPermissionCheck, 'costController.colSet');
     app.get('/sp/:id/cost/tender/:tid/:stype/:sorder/stage', sessionAuth, subProjectCheck, tenderCheck, tenderPermissionCheck, costStageCheck, 'costController.stage');
+    app.get('/sp/:id/cost/tender/:tid/:stype/:sorder/compare', sessionAuth, subProjectCheck, tenderCheck, tenderPermissionCheck, costStageCheck, 'costController.compare');
     app.post('/sp/:id/cost/tender/:tid/:stype/:sorder/load', sessionAuth, subProjectCheck, tenderCheck, tenderPermissionCheck, costStageCheck, 'costController.stageLoad');
     app.post('/sp/:id/cost/tender/:tid/:stype/:sorder/update', sessionAuth, subProjectCheck, tenderCheck, tenderPermissionCheck, costStageCheck, 'costController.stageUpdate');
+    app.post('/sp/:id/cost/tender/:tid/:stype/:sorder/decimal', sessionAuth, subProjectCheck, tenderCheck, tenderPermissionCheck, costStageCheck, 'costController.setDecimal');
     app.post('/sp/:id/cost/tender/:tid/:stype/:sorder/file/upload', sessionAuth, subProjectCheck, tenderCheck, tenderPermissionCheck, costStageCheck, 'costController.uploadStageFile');
     app.post('/sp/:id/cost/tender/:tid/:stype/:sorder/file/delete', sessionAuth, subProjectCheck, tenderCheck, tenderPermissionCheck, costStageCheck, 'costController.deleteStageFile');
     app.post('/sp/:id/cost/tender/:tid/:stype/:sorder/stage/tag', sessionAuth, subProjectCheck, tenderCheck, tenderPermissionCheck, costStageCheck, 'costController.tag');
-    app.get('/sp/:id/cost/tender/:tid/flow', sessionAuth, subProjectCheck, tenderCheck, projectManagerCheck, 'tenderController.shenpiSet');
+    app.get('/sp/:id/cost/tender/:tid/flow', sessionAuth, subProjectCheck, tenderCheck, tenderPermissionCheck, projectManagerCheck, 'tenderController.shenpiSet');
     app.post('/sp/:id/cost/tender/:tid/:stype/:sorder/audit/add', sessionAuth, subProjectCheck, tenderCheck, tenderPermissionCheck, costStageCheck, 'costController.addStageAudit');
     app.post('/sp/:id/cost/tender/:tid/:stype/:sorder/audit/delete', sessionAuth, subProjectCheck, tenderCheck, tenderPermissionCheck, costStageCheck, 'costController.deleteStageAudit');
     app.post('/sp/:id/cost/tender/:tid/:stype/:sorder/audit/start', sessionAuth, subProjectCheck, tenderCheck, tenderPermissionCheck, costStageCheck, 'costController.stageAuditStart');
     app.post('/sp/:id/cost/tender/:tid/:stype/:sorder/audit/check', sessionAuth, subProjectCheck, tenderCheck, tenderPermissionCheck, costStageCheck, 'costController.stageAuditCheck');
     app.post('/sp/:id/cost/tender/:tid/:stype/:sorder/audit/checkAgain', sessionAuth, subProjectCheck, tenderCheck, tenderPermissionCheck, costStageCheck, 'costController.stageAuditCheckAgain');
     app.post('/sp/:id/cost/tender/:tid/:stype/:sorder/audit/checkCancel', sessionAuth, subProjectCheck, tenderCheck, tenderPermissionCheck, costStageCheck, 'costController.stageAuditCheckCancel');
+    app.get('/sp/:id/cost/tender/:tid/gather', sessionAuth, subProjectCheck, tenderCheck, tenderPermissionCheck, 'costController.gather');
+    app.post('/sp/:id/cost/tender/:tid/gather/load', sessionAuth, subProjectCheck, tenderCheck, tenderPermissionCheck, 'costController.loadGatherData');
 
     // 安全管理
     // 安全计量
@@ -635,6 +650,8 @@ module.exports = app => {
     app.post('/tender/:id/saveRela', sessionAuth, tenderCheck, subProjectCheck, 'tenderController.saveRelaData');
     app.get('/tender/:id/cert', sessionAuth, tenderCheck, subProjectCheck, 'tenderController.certSet');
     app.post('/tender/:id/cert/save', sessionAuth, tenderCheck, subProjectCheck, 'tenderController.saveCert');
+    app.post('/tender/:id/ai/image2excel', sessionAuth, tenderCheck, subProjectCheck, 'tenderController.aiImage2Excel');
+    app.post('/tender/:id/ai/billsIdentify', sessionAuth, tenderCheck, subProjectCheck, 'tenderController.aiBillsIdentify');
 
     app.get('/tender/:id/ctrl-price', sessionAuth, tenderCheck, subProjectCheck, 'ctrlPriceController.index');
     app.post('/tender/:id/ctrl-price/load', sessionAuth, tenderCheck, subProjectCheck, 'ctrlPriceController.load');
@@ -694,7 +711,10 @@ module.exports = app => {
     app.post('/tender/:id/ledger/deal2sgfh', sessionAuth, tenderCheck, subProjectCheck, uncheckTenderCheck, 'ledgerController.deal2sgfh');
     app.post('/tender/:id/ledger/sgfh2deal', sessionAuth, tenderCheck, subProjectCheck, uncheckTenderCheck, 'ledgerController.sgfh2deal');
     app.post('/tender/:id/ledger/check', sessionAuth, tenderCheck, subProjectCheck, uncheckTenderCheck, 'ledgerController.check');
+    app.post('/tender/:id/ledger/aiComplie', sessionAuth, tenderCheck, subProjectCheck, uncheckTenderCheck, 'ledgerController.aiComplie');
     app.post('/tender/:id/measure/ledger/auditors', sessionAuth, tenderCheck, subProjectCheck, uncheckTenderCheck, 'ledgerController.ledgerAuditors');
+    app.get('/tender/:id/ledger/cpd', sessionAuth, tenderCheck, subProjectCheck, uncheckTenderCheck, 'ledgerController.exportLedgerData');
+    app.post('/tender/:id/ledger/cpd/load', sessionAuth, tenderCheck, subProjectCheck, uncheckTenderCheck, 'ledgerController.importLedgerData');
 
     // 台账附件
     app.post('/tender/:id/ledger/upload/file', sessionAuth, tenderCheck, subProjectCheck, uncheckTenderCheck, 'ledgerController.uploadFile');
@@ -847,11 +867,17 @@ module.exports = app => {
     app.post('/tender/:id/pay/:order/file/delete', sessionAuth, tenderCheck, subProjectCheck, phasePayCheck, 'payController.deleteFile');
     // 合同支付审批
     app.post('/tender/:id/pay/:order/audit/add', sessionAuth, tenderCheck, subProjectCheck, uncheckTenderCheck, phasePayCheck, 'payController.addAudit');
+    app.post('/tender/:id/pay/:order/report/audit/add', sessionAuth, tenderCheck, subProjectCheck, uncheckTenderCheck, phasePayCheck, 'payController.addAudit'); // 等效于上面接口
     app.post('/tender/:id/pay/:order/audit/delete', sessionAuth, tenderCheck, subProjectCheck, uncheckTenderCheck, phasePayCheck, 'payController.deleteAudit');
+    app.post('/tender/:id/pay/:order/report/audit/delete', sessionAuth, tenderCheck, subProjectCheck, uncheckTenderCheck, phasePayCheck, 'payController.deleteAudit'); // 等效于上面接口
     app.post('/tender/:id/pay/:order/audit/start', sessionAuth, tenderCheck, subProjectCheck, uncheckTenderCheck, phasePayCheck, 'payController.auditStart');
+    app.post('/tender/:id/pay/:order/report/audit/start', sessionAuth, tenderCheck, subProjectCheck, uncheckTenderCheck, phasePayCheck, 'payController.auditStart'); // 等效于上面接口
     app.post('/tender/:id/pay/:order/audit/check', sessionAuth, tenderCheck, subProjectCheck, uncheckTenderCheck, phasePayCheck, 'payController.auditCheck');
+    app.post('/tender/:id/pay/:order/report/audit/check', sessionAuth, tenderCheck, subProjectCheck, uncheckTenderCheck, phasePayCheck, 'payController.auditCheck'); // 等效于上面接口
     app.post('/tender/:id/pay/:order/audit/checkAgain', sessionAuth, tenderCheck, subProjectCheck, uncheckTenderCheck, phasePayCheck, 'payController.auditCheckAgain');
+    app.post('/tender/:id/pay/:order/report/audit/checkAgain', sessionAuth, tenderCheck, subProjectCheck, uncheckTenderCheck, phasePayCheck, 'payController.auditCheckAgain'); // 等效于上面接口
     app.post('/tender/:id/pay/:order/audit/checkCancel', sessionAuth, tenderCheck, subProjectCheck, uncheckTenderCheck, phasePayCheck, 'payController.auditCheckCancel');
+    app.post('/tender/:id/pay/:order/report/audit/checkCancel', sessionAuth, tenderCheck, subProjectCheck, uncheckTenderCheck, phasePayCheck, 'payController.auditCheckCancel'); // 等效于上面接口
 
     // 变更概况
     app.get('/tender/:id/measure/stage/:order/change', sessionAuth, tenderCheck, subProjectCheck, uncheckTenderCheck, stageCheck, 'stageController.change');
@@ -964,6 +990,7 @@ module.exports = app => {
     // app.get('/tender/:id/pay/:order/detail', sessionAuth, tenderCheck, subProjectCheck, phasePayCheck, 'payController.detail');
     // app.get('/tender/:id/pay/:order/report', sessionAuth, tenderCheck, subProjectCheck, phasePayCheck, 'reportController.indexForPhasePay'); // 计量管理
     app.get('/tender/:id/pay/:order/report', sessionAuth, tenderCheck, subProjectCheck, phasePayCheck, 'reportController.indexForPhasePay'); // 计量管理(无 phasePayCheck)
+    app.post('/tender/:id/pay/:order/report/audit/add', sessionAuth, tenderCheck, subProjectCheck, uncheckTenderCheck, phasePayCheck, 'payController.addAudit'); // 等效于 /tender/:id/pay/:order/audit/add
     app.get('/tender/:id/archiveReport', sessionAuth, tenderCheck, subProjectCheck, 'reportArchiveController.index');
     app.post('/tender/report_api/getReportArchive', sessionAuth, 'reportArchiveController.getReportArchive');
     app.post('/tender/report_api/getReportArchive4bz', sessionAuth, 'reportArchiveController.getReportArchive4bz');

+ 11 - 3
app/service/budget.js

@@ -103,6 +103,7 @@ module.exports = app => {
             await this.ctx.service.budgetYu.initByTemplate(transaction, operate.insertId, budgetStd.yu_template_id);
             await this.ctx.service.budgetZb.initByTemplate(transaction, operate.insertId, budgetStd.zb_template_id);
             await this.ctx.service.budgetCtrl.initByTemplate(transaction, operate.insertId, budgetStd.ctrl_template_id);
+            await this.ctx.service.budgetReal.initByTemplate(transaction, operate.insertId, budgetStd.real_template_id);
             return operate.insertId;
         }
 
@@ -263,11 +264,18 @@ module.exports = app => {
             }
         }
 
-        async doFinal(budget, final) {
+        async saveExtraInfo(data, page) {
+            const updateData = { id: this.ctx.budget.id };
+            updateData[page + '_profit'] = data.profit || 0;
+            updateData[page + '_optimize_rate'] = data.optimize_rate || 0;
+            await this.defaultUpdate(updateData);
+        }
+
+        async doFinal(budget, final, stage, contract) {
             const finalObj = new FinalObj(this.ctx);
             let finalData;
             try {
-                finalData = await finalObj.doFinal(budget, final);
+                finalData = await finalObj.doFinal(budget, final, stage, contract);
             } catch (err) {
                 this.db.update(this.ctx.service.budgetFinalList.tableName, { id: final.id, status: 4});
                 this.ctx.log(err);
@@ -278,7 +286,7 @@ module.exports = app => {
             try {
                 await conn.update(this.tableName, {id: budget.id, final_id: final.id, final_time: new Date() });
                 await conn.insert(this.ctx.service.budgetFinal.tableName, finalData);
-                await conn.update(this.ctx.service.budgetFinalList.tableName, { id: final.id, tender_info: JSON.stringify(final.tender_info), status: 3});
+                await conn.update(this.ctx.service.budgetFinalList.tableName, { id: final.id, tender_info: JSON.stringify(final.tender_info), contract: contract.join(','), status: 3});
                 await conn.commit();
                 return finalData;
             } catch (err) {

+ 18 - 0
app/service/budget_final_list.js

@@ -33,6 +33,7 @@ module.exports = app => {
         _analysisFinal(data) {
             data.tender = data.tender ? data.tender.split(',') : [];
             data.tender_info = data.tender_info ? JSON.parse(data.tender_info) : [];
+            data.contract = data.contract ? data.contract.split(',') : [];
         }
 
         async getFinal(id) {
@@ -52,6 +53,23 @@ module.exports = app => {
             const result = await this.db.insert(this.tableName, final);
             return await this.getFinal(result.insertId);
         }
+
+        async clearExpiredFinal(budget) {
+            const datas = await this.getAllDataByCondition({ where: { bid: budget.id }, orders: [['id', 'DESC']]});
+            if (datas.length > 3) {
+                datas.splice(0, 3);
+                const conn = await this.db.beginTransaction();
+                try {
+                    const id = datas.map(x => { return x.id; });
+                    await conn.delete(this.tableName, { id });
+                    await conn.delete(this.ctx.service.budgetFinal.tableName, { final_id: id });
+                    await conn.commit();
+                } catch (err) {
+                    await conn.rollback();
+                    this.ctx.log(err);
+                }
+            }
+        }
     }
 
     return BudgetFinalList;

+ 28 - 0
app/service/budget_real.js

@@ -0,0 +1,28 @@
+'use strict';
+
+/**
+ *
+ *
+ * @author Mai
+ * @date
+ * @version
+ */
+
+module.exports = app => {
+    class BudgetReal extends app.BaseBudgetService {
+
+        /**
+         * 构造函数
+         *
+         * @param {Object} ctx - egg全局变量
+         * @param {String} tableName - 表名
+         * @return {void}
+         */
+        constructor(ctx) {
+            super(ctx, { keyPre: 'budget_read_maxLid:' });
+            this.tableName = 'budget_real';
+        }
+    }
+
+    return BudgetReal;
+};

+ 36 - 12
app/service/contract.js

@@ -39,9 +39,10 @@ module.exports = app => {
 
         async add(options, node, data) {
             if (!options[this.setting.type] || !node || !data) throw '参数有误';
-            const insertId = this.uuid.v4();
+            const insertId = data.cid || this.uuid.v4();
             const transaction = await this.db.beginTransaction();
             try {
+                const add_key = ['c_code', 'name', 'tax', 'price', 'party_a', 'party_a_user', 'party_b', 'party_b_user', 'sign_date', 'address', 'entity', 'bank', 'bank_account'];
                 const maxId = await this.ctx.service.contractTree._getMaxLid(options);
                 const insertData = {
                     id: insertId,
@@ -53,17 +54,41 @@ module.exports = app => {
                     contract_pid: !node.c_code ? node.contract_id : node.contract_pid,
                     level: !node.c_code ? node.level + 1 : node.level,
                     is_leaf: 1,
-                    c_code: data.code,
-                    name: data.name,
-                    total_price: data.total_price ? parseFloat(data.total_price) : 0,
-                    party_a: data.party_a,
-                    party_b: data.party_b,
-                    sign_date: data.sign_date,
-                    remark: data.remark,
+                    // c_code: data.c_code,
+                    // name: data.name,
+                    // total_price: data.price ? parseFloat(data.price) : 0,
+                    // price: data.price ? parseFloat(data.price) : 0,
+                    // party_a: data.party_a,
+                    // party_b: data.party_b,
+                    // sign_date: data.sign_date,
+                    // remark: data.remark,
                     type: data.type,
                     create_time: new Date(),
                     need_shenpi: options.spid ? this.ctx.subProject.page_show.openContractSubProjectShenpi : this.ctx.subProject.page_show.openContractTenderShenpi,
                 };
+                for (const key of add_key) {
+                    if (key === 'price') {
+                        insertData[key] = data[key] ? parseFloat(data[key]) : 0;
+                        insertData.total_price = data[key] ? parseFloat(data[key]) : 0;
+                        continue;
+                    } else if (key === 'tax') {
+                        insertData[key] = data[key] ? parseFloat(data[key]) : null;
+                        continue;
+                    } else if (key === 'sign_date') {
+                        insertData[key] = data[key] ? new Date(data[key]) : null;
+                        continue;
+                    }
+                    insertData[key] = data[key] || '';
+                }
+                if (data.attribute_json) insertData.attribute_json = data.attribute_json;
+                // 删除attachments
+                if (!data.attach && data.fid) {
+                    const fileInfo = await this.ctx.service.contractAtt.getDataById(data.fid);
+                    if (fileInfo || Object.keys(fileInfo).length) {
+                        await this.ctx.app.fujianOss.delete(this.ctx.app.config.fujianOssFolder + fileInfo.filepath);
+                        await transaction.delete(this.ctx.service.contractAtt.tableName, { id: data.fid });
+                    }
+                }
                 insertData[this.setting.fullPath] = !node.c_code
                     ? node[this.setting.fullPath] + '-' + insertData[this.setting.kid]
                     : node[this.setting.fullPath].replace('-' + node[this.setting.kid], '-' + insertData[this.setting.kid]);
@@ -75,7 +100,7 @@ module.exports = app => {
                     await transaction.update(this.ctx.service.contractTree.tableName, { id: node.id, is_leaf: 0 });
                 }
                 this.ctx.service.contractTree._cacheMaxLid(options, maxId + 1);
-                await this.ctx.service.contractSpAudit.makeAudits(transaction, options, insertId, null, this.ctx.session.sessionUser.accountId);
+                await this.ctx.service.contractSpAudit.makeAudits(transaction, options, insertData, 'contract', this.ctx.session.sessionUser.accountId);
                 await transaction.commit();
             } catch (err) {
                 await transaction.rollback();
@@ -124,8 +149,7 @@ module.exports = app => {
                 await transaction.rollback();
                 throw err;
             }
-
-            return { update: await this.getDataById(ids) };
+            return { contract: await this.getDataById(ids), files: await this.ctx.service.contractAtt.getAtt(ids[0]) };
         }
 
         async getMaxOrder(options, pid, transaction) {
@@ -153,7 +177,7 @@ module.exports = app => {
                     // await this.ctx.service.contractSpAudit.loadAuditViewData(l);
                     // await this.ctx.service.contractSpAudit.checkShenpi(l);
                     if (l.status !== auditConst.status.checked || !l.final_auditor_str) {
-                        l.curAuditors2 = await this.ctx.service.contractSpAudit.getAuditorsByStatus(l.id, null, l.status, l.times);
+                        l.curAuditors2 = await this.ctx.service.contractSpAudit.getAuditorsByStatus(l, 'contract', l.status, l.times);
                         if (l.status === auditConst.status.checked && !l.final_auditor_str) {
                             const final_auditor_str = l.curAuditors2[0].audit_type === auditType.key.common
                                 ? l.curAuditors2[0].name + (l.curAuditors2[0].role ? '-' + l.curAuditors2[0].role : '')

+ 9 - 5
app/service/contract_pay.js

@@ -32,16 +32,17 @@ module.exports = app => {
             const pay_shenpi_status = this.ctx.contract_tender ? this.ctx.subProject.page_show.openContractPayTenderShenpi : this.ctx.subProject.page_show.openContractPaySubProjectShenpi;
             if (list.length > 0) {
                 const userList = await this.ctx.service.projectAccount.getAllDataByCondition({ where: { id: list.map(item => item.uid) } });
+                const type = 'pay';
                 for (const l of list) {
                     const userInfo = userList.find(item => item.id === l.uid);
                     l.username = userInfo ? userInfo.name : '';
                     l.files = await this.ctx.service.contractPayAtt.getAtt(l.id);
                     if (pay_shenpi_status) {
-                        await this.ctx.service.contractSpAudit.loadUser(l);
-                        await this.ctx.service.contractSpAudit.loadAuditViewData(l);
-                        await this.ctx.service.contractSpAudit.checkShenpi(l);
+                        await this.ctx.service.contractSpAudit.loadUser(l, type);
+                        await this.ctx.service.contractSpAudit.loadAuditViewData(l, type);
+                        await this.ctx.service.contractSpAudit.checkShenpi(l, type);
                         if (l.status !== auditConst.status.checked || !l.final_auditor_str) {
-                            l.curAuditors2 = await this.ctx.service.contractSpAudit.getAuditorsByStatus(l.cid, l.id, l.status, l.times);
+                            l.curAuditors2 = await this.ctx.service.contractSpAudit.getAuditorsByStatus(l, type, l.status, l.times);
                             if (l.status === auditConst.status.checked && !l.final_auditor_str) {
                                 const final_auditor_str = l.curAuditors2[0].audit_type === auditType.key.common
                                     ? l.curAuditors2[0].name + (l.curAuditors2[0].role ? '-' + l.curAuditors2[0].role : '')
@@ -82,7 +83,8 @@ module.exports = app => {
                 };
                 const result = await transaction.insert(this.tableName, insertData);
                 await this.calcContract(transaction, node);
-                await this.ctx.service.contractSpAudit.makeAudits(transaction, options, cid, result.insertId, this.ctx.session.sessionUser.accountId);
+                insertData.id = result.insertId;
+                await this.ctx.service.contractSpAudit.makeAudits(transaction, options, insertData, 'pay', this.ctx.session.sessionUser.accountId);
                 await transaction.commit();
             } catch (err) {
                 await transaction.rollback();
@@ -214,6 +216,7 @@ module.exports = app => {
                             tid: p.tid,
                             cid: p.cid,
                             cpid: p.id,
+                            csid: null,
                             aid: fp.uid,
                             order: 0,
                             times: 1,
@@ -234,6 +237,7 @@ module.exports = app => {
                                 tid: p.tid,
                                 cid: p.cid,
                                 cpid: p.id,
+                                csid: null,
                                 aid: a.aid,
                                 order: a.order,
                                 times: 1,

+ 270 - 170
app/service/contract_sp_audit.js

@@ -27,11 +27,16 @@ module.exports = app => {
             this.tableName = 'contract_sp_audit';
         }
 
-        async checkShenpi(contract) {
+        async checkShenpi(contract, type = 'contract') {
             const cid = contract.cid ? contract.cid : contract.id;
-            const cpid = contract.cid ? contract.id : null;
-            const change_type = contract.cid ? contractConst.typeMap[contract.contract_type] : 'contract';
-            const tableName = contract.cid ? this.ctx.service.contractPay : this.ctx.service.contract;
+            const cpid = type === 'pay' ? contract.id : null;
+            const csid = type === 'supplement' ? contract.id : null;
+            const change_type = type === 'contract' || type === 'supplement' ? 'contract' : contractConst.typeMap[contract.contract_type];
+            const tableName = {
+                contract: this.ctx.service.contract,
+                pay: this.ctx.service.contractPay,
+                supplement: this.ctx.service.contractSupplement,
+            }[type];
             const status = auditConst.status;
             let shenpi = '';
             if (this.ctx.contract_tender) {
@@ -43,7 +48,7 @@ module.exports = app => {
             const shenpi_status = shenpi.contract;
             if ((contract.status === status.uncheck || contract.status === status.checkNo) && shenpi_status !== shenpiConst.sp_status.sqspr) {
                 // 进一步比较审批流是否与审批流程设置的相同,不同则替换为固定审批流或固定的终审
-                const auditList = await this.getAllDataByCondition({ where: { cid, cpid, times: contract.times }, orders: [['order', 'asc']] });
+                const auditList = await this.getAllDataByCondition({ where: { cid, cpid, csid, times: contract.times }, orders: [['order', 'asc']] });
                 // auditList.shift();
                 if (shenpi_status === shenpiConst.sp_status.gdspl) {
                     // 判断并获取审批组
@@ -68,9 +73,9 @@ module.exports = app => {
                             }
                         }
                         if (!sameAudit) {
-                            await this.updateNewAuditList(contract, shenpiList);
-                            await this.loadUser(contract);
-                            await this.loadAuditViewData(contract);
+                            await this.updateNewAuditList(contract, type, shenpiList);
+                            await this.loadUser(contract, type);
+                            await this.loadAuditViewData(contract, type);
                         }
                     }
                 } else if (shenpi_status === shenpiConst.sp_status.gdzs) {
@@ -78,7 +83,7 @@ module.exports = app => {
                     // 判断最后一个id是否与固定终审id相同,不同则删除原审批流中如果存在的id和添加终审
                     const lastAuditors = auditList.filter(x => { return x.audit_order === auditList.length - 1; });
                     if (shenpiInfo && (lastAuditors.length === 0 || (lastAuditors.length > 1 || shenpiInfo.audit_id !== lastAuditors[0].aid))) {
-                        await this.updateLastAudit(contract, auditList, shenpiInfo.audit_id);
+                        await this.updateLastAudit(contract, type, auditList, shenpiInfo.audit_id);
                     } else if (!shenpiInfo) {
                         // 不存在终审人的状态下这里恢复为授权审批人
                         shenpi.contract = shenpiConst.sp_status.sqspr;
@@ -87,12 +92,12 @@ module.exports = app => {
             }
         }
 
-        async loadUser(contract) {
+        async loadUser(contract, type = 'contract') {
             const status = auditConst.status;
             const accountId = this.ctx.session.sessionUser.accountId;
 
             contract.user = await this.ctx.service.projectAccount.getAccountInfoById(contract.uid);
-            contract.auditors = await this.getAuditors(contract.cid ? contract.cid : contract.id, contract.cid ? contract.id : null, contract.times); // 全部参与的审批人
+            contract.auditors = await this.getAuditors(contract, type, contract.times); // 全部参与的审批人
             contract.auditorIds = this._.map(contract.auditors, 'aid');
             contract.curAuditors = contract.auditors.filter(x => { return x.status === status.checking; }); // 当前流程中审批中的审批人
             contract.curAuditorIds = this._.map(contract.curAuditors, 'aid');
@@ -105,21 +110,21 @@ module.exports = app => {
             contract.finalAuditorIds = this._.map(contract.userGroups[contract.userGroups.length - 1], 'aid');
         }
 
-        async loadAuditViewData(contract) {
+        async loadAuditViewData(contract, type = 'contract') {
             const times = contract.status === auditConst.status.checkNo ? contract.times - 1 : contract.times;
 
             if (!contract.user) contract.user = await this.ctx.service.projectAccount.getAccountInfoById(contract.uid);
-            contract.auditHistory = await this.getAuditorHistory(contract.cid ? contract.cid : contract.id, contract.cid ? contract.id : null, times);
+            contract.auditHistory = await this.getAuditorHistory(contract, type, times);
             // 获取审批流程中左边列表
             // if ((contract.status === auditConst.status.checkNo) && !(contract.uid === this.ctx.session.sessionUser.accountId || this.ctx.contract_audit_permission.permission_edit_contract || this.ctx.session.sessionUser.is_admin)) {
-            const auditors = await this.getAuditors(contract.cid ? contract.cid : contract.id, contract.cid ? contract.id : null, times); // 全部参与的审批人
+            const auditors = await this.getAuditors(contract, type, times); // 全部参与的审批人
             const auditorGroups = this.ctx.helper.groupAuditors(auditors);
             contract.auditors2 = this.ctx.helper.groupAuditorsUniq(auditorGroups);
             // } else {
             //     contract.auditors2 = contract.userGroups;
             // }
             if (contract.status === auditConst.status.uncheck || contract.status === auditConst.status.checkNo) {
-                contract.auditorList = await this.getAuditors(contract.cid ? contract.cid : contract.id, contract.cid ? contract.id : null, contract.times);
+                contract.auditorList = await this.getAuditors(contract, type, contract.times);
             }
         }
 
@@ -130,7 +135,7 @@ module.exports = app => {
          * @param {Number} times - 第几次审批
          * @return {Promise<*>}
          */
-        async getAuditors(cid, cpid = null, times = 1, order_sort = 'asc', noYB = false) {
+        async getAuditors(contract, type, 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`';
@@ -142,12 +147,20 @@ module.exports = app => {
             // for (const i in result) {
             //     result[i].max_sort = count.num;
             // }
+            const cid = contract.cid ? contract.cid : contract.id;
+            const cpid = type === 'pay' ? contract.id : null;
+            const csid = type === 'supplement' ? contract.id : null;
+            const cpidSql = type === 'pay'
+                ? ' AND la.cpid = ' + cpid
+                : ' AND la.cpid is null';
+            const csidSql = type === 'supplement'
+                ? ' AND la.csid = ' + csid
+                : ' AND la.csid is null';
             const ybSql = noYB ? ' AND la.audit_order != 0' : '';
-            const cpidSql = cpid ? ' AND la.cpid = ' + cpid : ' AND la.cpid is null';
             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.cid = ?' + cpidSql + ' AND la.times = ?' + ybSql +
+                '  WHERE la.cid = ?' + cpidSql + csidSql + ' AND la.times = ?' + ybSql +
                 '  ORDER BY la.order ' + order_sort;
             const sqlParam = [cid, times];
             const result = await this.db.query(sql, sqlParam);
@@ -165,21 +178,37 @@ module.exports = app => {
          * @param {Number} times - 第几次审批
          * @return {Promise<*>}
          */
-        async getCurAuditor(cid, cpid = null, times = 1) {
-            const cpidSql = cpid ? ' AND la.cpid = ' + cpid : ' AND la.cpid is null';
+        async getCurAuditor(contract, type, times = 1) {
+            const cid = contract.cid ? contract.cid : contract.id;
+            const cpid = type === 'pay' ? contract.id : null;
+            const csid = type === 'supplement' ? contract.id : null;
+            const cpidSql = type === 'pay'
+                ? ' AND la.cpid = ' + cpid
+                : ' AND la.cpid is null';
+            const csidSql = type === 'supplement'
+                ? ' AND la.csid = ' + csid
+                : ' AND la.csid is null';
             const sql = 'SELECT la.`aid`, pa.`name`, pa.`company`, pa.`role`, pa.`mobile`, pa.`telephone`, la.`times`, la.`order`, la.`status`, la.`opinion`, la.`begin_time`, la.`end_time` ' +
                 '  FROM ?? AS la Left Join ?? AS pa On la.`aid` = pa.`id` ' +
-                '  WHERE la.`cid` = ?' + cpidSql + ' and la.`status` = ? and la.`times` = ? and la.`audit_order` != 0';
+                '  WHERE la.`cid` = ?' + cpidSql + csidSql + ' and la.`status` = ? and la.`times` = ? and la.`audit_order` != 0';
             const sqlParam = [this.tableName, this.ctx.service.projectAccount.tableName, cid, auditConst.status.checking, times];
             return await this.db.queryOne(sql, sqlParam);
         }
 
-        async getCurAuditors(cid, cpid = null, times = 1) {
-            const cpidSql = cpid ? ' AND la.cpid = ' + cpid : ' AND la.cpid is null';
+        async getCurAuditors(contract, type, times = 1) {
+            const cid = contract.cid ? contract.cid : contract.id;
+            const cpid = type === 'pay' ? contract.id : null;
+            const csid = type === 'supplement' ? contract.id : null;
+            const cpidSql = type === 'pay'
+                ? ' AND la.cpid = ' + cpid
+                : ' AND la.cpid is null';
+            const csidSql = type === 'supplement'
+                ? ' AND la.csid = ' + csid
+                : ' AND la.csid is null';
             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.`cid` = ?' + cpidSql + ' and la.`status` = ? and la.`times` = ? and la.`audit_order` != 0';
+                '  WHERE la.`cid` = ?' + cpidSql + csidSql + ' and la.`status` = ? and la.`times` = ? and la.`audit_order` != 0';
             const sqlParam = [this.tableName, this.ctx.service.projectAccount.tableName, cid, auditConst.status.checking, times];
             return await this.db.query(sql, sqlParam);
         }
@@ -205,18 +234,21 @@ module.exports = app => {
          * @param auditorId
          * @return {Promise<*>}
          */
-        async getAuditGroupByList(cid, cpid = null, times, transaction = false) {
-            // const sql =
-            //     'SELECT la.`aid`, pa.`name`, pa.`company`, pa.`role`, la.`times`, la.`sid`, la.`aid`, la.`order`, la.`status`' +
-            //     '  FROM ?? AS la Left Join ?? AS pa On la.`aid` = pa.`id` ' +
-            //     '  WHERE la.`sid` = ? and la.`times` = ? and la.`is_old` = 0 GROUP BY la.`aid` ORDER BY la.`order`';
-            // const sqlParam = [this.tableName, this.ctx.service.projectAccount.tableName, stageId, times];
-            const cpidSql = cpid ? ' AND la.`cpid` = ' + cpid : ' AND la.`cpid` is null';
+        async getAuditGroupByList(contract, type, times, transaction = false) {
+            const cid = contract.cid ? contract.cid : contract.id;
+            const cpid = type === 'pay' ? contract.id : null;
+            const csid = type === 'supplement' ? contract.id : null;
+            const cpidSql = type === 'pay'
+                ? ' AND la.cpid = ' + cpid
+                : ' AND la.cpid is null';
+            const csidSql = type === 'supplement'
+                ? ' AND la.csid = ' + csid
+                : ' AND la.csid is null';
             const sql =
                 'SELECT la.`aid`, pa.`name`, pa.`company`, pa.`role`, la.`times`, la.`cid`, la.`cpid`, la.`aid`, la.`order`, la.`status`, la.audit_type, la.audit_order ' +
-                ' FROM (SELECT `aid`, max(`order`) `order` FROM ?? WHERE `cid` = ? and `cpid` ' + (cpid ? '= ' + cpid : 'is null') + ' and `times` = ? and `audit_order` != 0 GROUP BY aid) sa' +
+                ' FROM (SELECT `aid`, max(`order`) `order` FROM ?? WHERE `cid` = ? and `cpid` ' + (cpid ? '= ' + cpid : 'is null') + ' and `csid` ' + (csid ? '= ' + csid : 'is null') + ' and `times` = ? and `audit_order` != 0 GROUP BY aid) sa' +
                 ' LEFT JOIN ?? la ON sa.`aid` = la.`aid` AND sa.`order` = la.`order`' +
-                ' Left JOIN ?? AS pa On la.`aid` = pa.`id` WHERE la.`cid` = ?' + cpidSql + ' and la.`times` = ? and la.`audit_order` != 0 order BY la.`order`';
+                ' Left JOIN ?? AS pa On la.`aid` = pa.`id` WHERE la.`cid` = ?' + cpidSql + csidSql + ' and la.`times` = ? and la.`audit_order` != 0 order BY la.`order`';
             const sqlParam = [this.tableName, cid, times, this.tableName, this.ctx.service.projectAccount.tableName, cid, times];
             return transaction !== false ? await transaction.query(sql, sqlParam) : await this.db.query(sql, sqlParam);
         }
@@ -229,8 +261,16 @@ module.exports = app => {
          * @param {Number} status - 期次数
          * @return {Promise<boolean>}
          */
-        async getAuditorByStatus(cid, cpid = null, status, times = 1) {
-            const cpidSql = cpid ? ' AND la.cpid = ' + cpid : ' AND la.cpid is null';
+        async getAuditorByStatus(contract, type, status, times = 1) {
+            const cid = contract.cid ? contract.cid : contract.id;
+            const cpid = type === 'pay' ? contract.id : null;
+            const csid = type === 'supplement' ? contract.id : null;
+            const cpidSql = type === 'pay'
+                ? ' AND la.cpid = ' + cpid
+                : ' AND la.cpid is null';
+            const csidSql = type === 'supplement'
+                ? ' AND la.csid = ' + csid
+                : ' AND la.csid is null';
             let auditor = null;
             let sql = '';
             let sqlParam = '';
@@ -239,7 +279,7 @@ module.exports = app => {
                 case auditConst.status.checked :
                     sql = 'SELECT la.`aid`, pa.`name`, pa.`company`, pa.`role`, la.`times`, la.`cid`, la.`cpid`, la.`aid`, la.`order`, la.`status` ' +
                         '  FROM ?? AS la Left Join ?? AS pa On la.`aid` = pa.`id` ' +
-                        '  WHERE la.`cid` = ?' + cpidSql + ' and la.`status` = ? ' +
+                        '  WHERE la.`cid` = ?' + cpidSql + csidSql + ' and la.`status` = ? ' +
                         '  ORDER BY la.`times` desc, la.`order` desc';
                     sqlParam = [this.tableName, this.ctx.service.projectAccount.tableName, cid, status];
                     auditor = await this.db.queryOne(sql, sqlParam);
@@ -247,7 +287,7 @@ module.exports = app => {
                 case auditConst.status.checkNo :
                     sql = 'SELECT la.`aid`, pa.`name`, pa.`company`, pa.`role`, la.`times`, la.`cid`, la.`cpid`, la.`aid`, la.`order`, la.`status` ' +
                         '  FROM ?? AS la Left Join ?? AS pa On la.`aid` = pa.`id`' +
-                        '  WHERE la.`cid` = ?' + cpidSql + ' and la.`status` = ? and la.`times` = ?' +
+                        '  WHERE la.`cid` = ?' + cpidSql + csidSql + ' and la.`status` = ? and la.`times` = ?' +
                         '  ORDER BY la.`times` desc, la.`order` desc';
                     sqlParam = [this.tableName, this.ctx.service.projectAccount.tableName, cid, auditConst.status.checkNo, parseInt(times) - 1];
                     auditor = await this.db.queryOne(sql, sqlParam);
@@ -259,8 +299,16 @@ module.exports = app => {
             return auditor;
         }
 
-        async getAuditorsByStatus(cid, cpid = null, status, times = 1) {
-            const cpidSql = cpid ? ' AND la.cpid = ' + cpid : ' AND la.cpid is null';
+        async getAuditorsByStatus(contract, type, status, times = 1) {
+            const cid = contract.cid ? contract.cid : contract.id;
+            const cpid = type === 'pay' ? contract.id : null;
+            const csid = type === 'supplement' ? contract.id : null;
+            const cpidSql = type === 'pay'
+                ? ' AND la.cpid = ' + cpid
+                : ' AND la.cpid is null';
+            const csidSql = type === 'supplement'
+                ? ' AND la.csid = ' + csid
+                : ' AND la.csid is null';
             let auditor = [];
             let sql = '';
             let sqlParam = '';
@@ -269,22 +317,22 @@ module.exports = app => {
                 case auditConst.status.checking :
                 case auditConst.status.checked :
                 case auditConst.status.checkNoPre:
-                    cur = await this.db.queryOne('SELECT * From ?? where cid = ? AND cpid ' + (cpid ? '= ' + cpid : 'is null') + ' AND times = ? AND status = ? AND audit_order != 0 ORDER By times DESC, `order` DESC', [this.tableName, cid, times, status]);
+                    cur = await this.db.queryOne('SELECT * From ?? where cid = ? AND cpid ' + (cpid ? '= ' + cpid : 'is null') + ' AND csid ' + (csid ? '= ' + csid : 'is null') + ' AND times = ? AND status = ? AND audit_order != 0 ORDER By times DESC, `order` DESC', [this.tableName, cid, times, status]);
                     if (!cur) return [];
 
                     sql = 'SELECT la.`aid`, pa.`name`, pa.`company`, pa.`role`, la.`times`, la.`cid`, la.`cpid`, la.`order`, la.`status`, la.`audit_order`, la.`audit_type` ' +
                         '  FROM ?? AS la Left Join ?? AS pa On la.`aid` = pa.`id` ' +
-                        '  WHERE la.`cid` = ?' + cpidSql + ' and la.`order` = ? and times = ?';
+                        '  WHERE la.`cid` = ?' + cpidSql + csidSql + ' and la.`order` = ? and times = ?';
                     sqlParam = [this.tableName, this.ctx.service.projectAccount.tableName, cid, cur.order, times];
                     auditor = await this.db.query(sql, sqlParam);
                     break;
                 case auditConst.status.checkNo :
-                    cur = await this.db.queryOne('SELECT * From ?? where cid = ? AND cpid ' + (cpid ? '= ' + cpid : 'is null') + ' AND times = ? AND status = ? AND audit_order != 0 ORDER By times DESC, `order` DESC', [this.tableName, cid, parseInt(times) - 1, status]);
+                    cur = await this.db.queryOne('SELECT * From ?? where cid = ? AND cpid ' + (cpid ? '= ' + cpid : 'is null') + ' AND csid ' + (csid ? '= ' + csid : 'is null') + ' AND times = ? AND status = ? AND audit_order != 0 ORDER By times DESC, `order` DESC', [this.tableName, cid, parseInt(times) - 1, status]);
                     if (!cur) return [];
 
                     sql = 'SELECT la.`aid`, pa.`name`, pa.`company`, pa.`role`, la.`times`, la.`cid`, la.`cpid`, la.`order`, la.`status`, la.`audit_order`, la.`audit_type` ' +
                         '  FROM ?? AS la Left Join ?? AS pa On la.`aid` = pa.`id` ' +
-                        '  WHERE la.`cid` = ?' + cpidSql + ' and la.`order` = ? and la.`times` = ?';
+                        '  WHERE la.`cid` = ?' + cpidSql + csidSql + ' and la.`order` = ? and la.`times` = ?';
                     sqlParam = [this.tableName, this.ctx.service.projectAccount.tableName, cid, cur.order, parseInt(times) - 1];
                     auditor = await this.db.query(sql, sqlParam);
                     break;
@@ -295,9 +343,17 @@ module.exports = app => {
             return auditor;
         }
 
-        async getLastAudit(cid, cpid = null, times, transaction = null) {
-            const cpidSql = cpid ? ' AND cpid = ' + cpid : ' AND cpid is null';
-            const sql = 'SELECT * FROM ?? WHERE `cid` = ?' + cpidSql + ' AND `times` = ? ORDER BY `order` DESC';
+        async getLastAudit(contract, type, times, transaction = null) {
+            const cid = contract.cid ? contract.cid : contract.id;
+            const cpid = type === 'pay' ? contract.id : null;
+            const csid = type === 'supplement' ? contract.id : null;
+            const cpidSql = type === 'pay'
+                ? ' AND cpid = ' + cpid
+                : ' AND cpid is null';
+            const csidSql = type === 'supplement'
+                ? ' AND csid = ' + csid
+                : ' AND csid is null';
+            const sql = 'SELECT * FROM ?? WHERE `cid` = ?' + cpidSql + csidSql + ' AND `times` = ? ORDER BY `order` DESC';
             const sqlParam = [this.tableName, cid, times];
             return transaction ? await transaction.queryOne(sql, sqlParam) : await this.db.queryOne(sql, sqlParam);
         }
@@ -308,9 +364,9 @@ module.exports = app => {
          * @param {Number} times 审核次数
          * @return {Promise<Array>} 查询结果集(包括原报)
          */
-        async getAuditorsWithOwner(cid, cpid = null, times = 1) {
+        async getAuditorsWithOwner(contract, type, times = 1) {
             // const cpidSql = cpid ? ' AND la.cpid = ' + cpid : ' AND la.cpid is null';
-            const result = await this.getAuditGroupByList(cid, cpid, times);
+            const result = await this.getAuditGroupByList(contract, type, times);
             // const sql =
             //     'SELECT pa.`id` As aid, pa.`name`, pa.`company`, pa.`role`, ? As times, ? As cpid, 0 As `order`' +
             //     '  FROM ' +
@@ -335,20 +391,24 @@ module.exports = app => {
          * @param {Number} times - 第几次审批
          * @return {Promise<number>}
          */
-        async addAuditor(options, cid, cpid = null, auditorId, times = 1, is_gdzs = 0) {
+        async addAuditor(options, contract, type, auditorId, times = 1, is_gdzs = 0) {
+            const cid = contract.cid ? contract.cid : contract.id;
+            const cpid = type === 'pay' ? contract.id : null;
+            const csid = type === 'supplement' ? contract.id : null;
             const transaction = await this.db.beginTransaction();
             let flag = false;
             try {
-                let [newOrder, newAuditOrder] = await this.getNewOrder(cid, cpid, times);
+                let [newOrder, newAuditOrder] = await this.getNewOrder(contract, type, times);
                 // 判断是否存在固定终审,存在则newOrder - 1并使终审order+1
                 newOrder = is_gdzs === 1 ? newOrder - 1 : newOrder;
                 newAuditOrder = is_gdzs === 1 ? newAuditOrder - 1 : newAuditOrder;
-                if (is_gdzs) await this._syncOrderByDelete(transaction, cid, cpid, newOrder, times, '+');
+                if (is_gdzs) await this._syncOrderByDelete(transaction, contract, type, newOrder, times, '+');
                 const data = {
                     spid: options.spid || null,
                     tid: options.tid || null,
                     cid,
                     cpid,
+                    csid,
                     aid: auditorId,
                     times,
                     order: newOrder,
@@ -373,9 +433,17 @@ module.exports = app => {
          * @param {Number} times - 第几次审批
          * @return {Promise<number>}
          */
-        async getNewOrder(cid, cpid = null, times = 1) {
-            const cpidSql = cpid ? ' AND cpid = ' + cpid : ' AND cpid is null';
-            const sql = 'SELECT Max(`order`) As max_order, Max(audit_order) As max_audit_order FROM ?? Where `cid` = ?' + cpidSql + ' and `times` = ?';
+        async getNewOrder(contract, type, times = 1) {
+            const cid = contract.cid ? contract.cid : contract.id;
+            const cpid = type === 'pay' ? contract.id : null;
+            const csid = type === 'supplement' ? contract.id : null;
+            const cpidSql = type === 'pay'
+                ? ' AND cpid = ' + cpid
+                : ' AND cpid is null';
+            const csidSql = type === 'supplement'
+                ? ' AND csid = ' + csid
+                : ' AND csid is null';
+            const sql = 'SELECT Max(`order`) As max_order, Max(audit_order) As max_audit_order FROM ?? Where `cid` = ?' + cpidSql + csidSql + ' and `times` = ?';
             const sqlParam = [this.tableName, cid, times];
             const result = await this.db.queryOne(sql, sqlParam);
             return result && result.max_order ? [result.max_order + 1, result.max_audit_order + 1] : [1, 1];
@@ -389,11 +457,19 @@ module.exports = app => {
          * @param {Number} times - 第几次审批
          * @return {Promise<boolean>}
          */
-        async deleteAuditor(cid, cpid = null, auditorId, times = 1) {
+        async deleteAuditor(contract, type, auditorId, times = 1) {
             const transaction = await this.db.beginTransaction();
             try {
-                const cpidSql = cpid ? ' AND cpid = ' + cpid : ' AND cpid is null';
-                const sql = 'SELECT * FROM ?? WHERE `cid` = ?' + cpidSql + ' and `times` = ? and `aid` = ? and `audit_order` != 0 ORDER BY `order` DESC';
+                const cid = contract.cid ? contract.cid : contract.id;
+                const cpid = type === 'pay' ? contract.id : null;
+                const csid = type === 'supplement' ? contract.id : null;
+                const cpidSql = type === 'pay'
+                    ? ' AND cpid = ' + cpid
+                    : ' AND cpid is null';
+                const csidSql = type === 'supplement'
+                    ? ' AND csid = ' + csid
+                    : ' AND csid is null';
+                const sql = 'SELECT * FROM ?? WHERE `cid` = ?' + cpidSql + csidSql + ' and `times` = ? and `aid` = ? and `audit_order` != 0 ORDER BY `order` DESC';
                 const sqlParam = [this.tableName, cid, times, auditorId];
                 const auditor = await transaction.queryOne(sql, sqlParam);
                 // const condition = { cid, cpid, aid: auditorId, times };
@@ -401,8 +477,8 @@ module.exports = app => {
                 if (!auditor) {
                     throw '该审核人不存在';
                 }
-                await transaction.delete(this.tableName, { cid, cpid, order: auditor.order, times });
-                await this._syncOrderByDelete(transaction, cid, cpid, auditor.order, times);
+                await transaction.delete(this.tableName, { cid, cpid, csid, order: auditor.order, times });
+                await this._syncOrderByDelete(transaction, contract, type, auditor.order, times);
                 await transaction.commit();
             } catch (err) {
                 await transaction.rollback();
@@ -420,31 +496,11 @@ module.exports = app => {
          * @return {Promise<*>}
          * @private
          */
-        async _syncOrderByDelete(transaction, cid, cpid, order, times, selfOperate = '-') {
-            // this.initSqlBuilder();
-            // this.sqlBuilder.setAndWhere('cid', {
-            //     value: this.db.escape(cid),
-            //     operate: '=',
-            // });
-            // this.sqlBuilder.setAndWhere('order', {
-            //     value: order,
-            //     operate: '>=',
-            // });
-            // this.sqlBuilder.setAndWhere('times', {
-            //     value: times,
-            //     operate: '=',
-            // });
-            // this.sqlBuilder.setUpdateData('order', {
-            //     value: 1,
-            //     selfOperate,
-            // });
-            // this.sqlBuilder.setUpdateData('audit_order', {
-            //     value: 1,
-            //     selfOperate,
-            // });
-            // const [sql, sqlParam] = this.sqlBuilder.build(this.tableName, 'update');
-            // const data = await transaction.query(sql, sqlParam);
-            const sql = 'UPDATE ?? SET `order` = `order` ' + selfOperate + ' ?, `audit_order` = `audit_order` ' + selfOperate + ' ? WHERE `cid` = ? AND `cpid` ' + (cpid ? '= ' + cpid : 'is null') + ' AND `order` >= ? AND `times` = ?';
+        async _syncOrderByDelete(transaction, contract, type, order, times, selfOperate = '-') {
+            const cid = contract.cid ? contract.cid : contract.id;
+            const cpid = type === 'pay' ? contract.id : null;
+            const csid = type === 'supplement' ? contract.id : null;
+            const sql = 'UPDATE ?? SET `order` = `order` ' + selfOperate + ' ?, `audit_order` = `audit_order` ' + selfOperate + ' ? WHERE `cid` = ? AND `cpid` ' + (cpid ? '= ' + cpid : 'is null') + ' AND `csid` ' + (csid ? '= ' + csid : 'is null') + ' AND `order` >= ? AND `times` = ?';
             const sqlParam = [this.tableName, 1, 1, cid, order, times];
             const data = await transaction.query(sql, sqlParam);
             return data;
@@ -456,8 +512,11 @@ module.exports = app => {
          * @param {Number} times - 第几次审批
          * @return {Promise<boolean>}
          */
-        async start(options, shenpi, cid, cpid = null, times = 1) {
-            const audits = await this.getAllDataByCondition({ where: { cid, cpid, times, order: 1 } });
+        async start(options, shenpi, contract, type, times = 1) {
+            const cid = contract.cid ? contract.cid : contract.id;
+            const cpid = type === 'pay' ? contract.id : null;
+            const csid = type === 'supplement' ? contract.id : null;
+            const audits = await this.getAllDataByCondition({ where: { cid, cpid, csid, times, order: 1 } });
             if (audits.length === 0) {
                 if (shenpi.contract === shenpiConst.sp_status.gdspl) {
                     throw '请联系管理员添加审批人';
@@ -474,6 +533,7 @@ module.exports = app => {
                     tid: options.tid || null,
                     cid,
                     cpid,
+                    csid,
                     aid: this.ctx.session.sessionUser.accountId,
                     times,
                     order: 0,
@@ -489,6 +549,10 @@ module.exports = app => {
                     await transaction.update(this.ctx.service.contractPay.tableName, {
                         id: cpid, status: auditConst.status.checking,
                     });
+                } else if (csid) {
+                    await transaction.update(this.ctx.service.contractSupplement.tableName, {
+                        id: csid, status: auditConst.status.checking,
+                    });
                 } else {
                     await transaction.update(this.ctx.service.contract.tableName, {
                         id: cid, status: auditConst.status.checking,
@@ -589,30 +653,34 @@ module.exports = app => {
          * @param {Number} times - 第几次审批
          * @return {Promise<void>}
          */
-        async check(contract, checkData) {
+        async check(contract, type, checkData) {
             if (!this._.includes([auditConst.status.checked, auditConst.status.checkNo, auditConst.status.checkNoPre], checkData.checkType)) {
                 throw '提交数据错误';
             }
             const pid = this.ctx.session.sessionProject.id;
             switch (checkData.checkType) {
                 case auditConst.status.checked:
-                    await this._checked(pid, contract, checkData);
+                    await this._checked(pid, contract, type, checkData);
                     break;
                 case auditConst.status.checkNo:
-                    await this._checkNo(pid, contract, checkData);
+                    await this._checkNo(pid, contract, type, checkData);
                     break;
                 case auditConst.status.checkNoPre:
-                    await this._checkNoPre(pid, contract, checkData);
+                    await this._checkNoPre(pid, contract, type, checkData);
                     break;
                 default:
                     throw '无效审批操作';
             }
         }
 
-        async _checked(pid, contract, checkData) {
+        async _checked(pid, contract, type, checkData) {
             const accountId = this.ctx.session.sessionUser.accountId;
             const time = new Date();
-            const tableName = contract.cid !== undefined ? this.ctx.service.contractPay.tableName : this.ctx.service.contract.tableName;
+            const tableName = {
+                contract: this.ctx.service.contract.tableName,
+                pay: this.ctx.service.contractPay.tableName,
+                supplement: this.ctx.service.contractSupplement.tableName,
+            }[type];
 
             // 整理当前流程审核人状态更新
             if (contract.curAuditorIds.length === 0) throw '审核数据错误';
@@ -689,18 +757,25 @@ module.exports = app => {
             }
         }
 
-        async _checkNo(pid, contract, checkData) {
+        async _checkNo(pid, contract, type, checkData) {
             const accountId = this.ctx.session.sessionUser.accountId;
             const time = new Date();
-            const tableName = contract.cid !== undefined ? this.ctx.service.contractPay.tableName : this.ctx.service.contract.tableName;
+            const cid = contract.cid ? contract.cid : contract.id;
+            const cpid = type === 'pay' ? contract.id : null;
+            const csid = type === 'supplement' ? contract.id : null;
+            const tableName = {
+                contract: this.ctx.service.contract.tableName,
+                pay: this.ctx.service.contractPay.tableName,
+                supplement: this.ctx.service.contractSupplement.tableName,
+            }[type];
             const audits = contract.curAuditors;
             if (audits.length === 0) throw '审核数据错误';
             const selfAudit = audits.find(x => { return x.aid === accountId; });
             if (!selfAudit) throw '当前标段您无权审批';
-            const auditors = await this.getUniqAuditor(contract.cid || contract.id, contract.cid ? contract.id : null, contract.times, true); // 全部参与的审批人
+            const auditors = await this.getUniqAuditor(contract, type, contract.times, true); // 全部参与的审批人
             const newAuditors = auditors.map(x => {
                 return {
-                    aid: x.aid, spid: contract.spid || null, tid: contract.tid || null, cid: contract.cid ? contract.cid : contract.id, cpid: contract.cid ? contract.id : null,
+                    aid: x.aid, spid: contract.spid || null, tid: contract.tid || null, cid, cpid, csid,
                     times: contract.times + 1, order: x.audit_order, status: auditConst.status.uncheck,
                     audit_type: x.audit_type, audit_order: x.audit_order,
                 };
@@ -745,10 +820,17 @@ module.exports = app => {
          * @param {int} changeData - 变更令的数据
          * @return {void}
          */
-        async _checkNoPre(pid, contract, checkData) {
+        async _checkNoPre(pid, contract, type, checkData) {
             const accountId = this.ctx.session.sessionUser.accountId;
             const time = new Date();
-            const tableName = contract.cid !== undefined ? this.ctx.service.contractPay.tableName : this.ctx.service.contract.tableName;
+            const cid = contract.cid ? contract.cid : contract.id;
+            const cpid = type === 'pay' ? contract.id : null;
+            const csid = type === 'supplement' ? contract.id : null;
+            const tableName = {
+                contract: this.ctx.service.contract.tableName,
+                pay: this.ctx.service.contractPay.tableName,
+                supplement: this.ctx.service.contractSupplement.tableName,
+            }[type];
             // 整理当前流程审核人状态更新
             const audits = contract.curAuditors;
             if (audits.length === 0 || audits[0].order <= 1) throw '审核数据错误';
@@ -758,7 +840,7 @@ module.exports = app => {
 
             // 添加重新审批后,不能用order-1,取groupby值里的上一个才对
             // const preAuditor = await this.getDataByCondition({sid: stageId, times: times, order: audit.order - 1});
-            const auditors2 = await this.getAuditGroupByList(contract.cid || contract.id, contract.cid ? contract.id : null, contract.times);
+            const auditors2 = await this.getAuditGroupByList(contract, type, contract.times);
             const preAuditors = auditors2.filter(x => { return x.audit_order === selfAudit.audit_order - 1});
 
             const transaction = await this.db.beginTransaction();
@@ -802,13 +884,13 @@ module.exports = app => {
                 await transaction.updateRows(this.tableName, updateData);
                 // await this.ctx.service.noticeAgain.stopNoticeAgain(transaction, this.tableName, this._.map(updateData, 'id'));
                 // 顺移其后审核人流程顺序
-                const sql = 'UPDATE ' + this.tableName + ' SET `order` = `order` + 2 WHERE cid = ? AND cpid' + (contract.cid !== undefined ? ' = ' + contract.id : ' is null') + ' AND times = ? AND `order` > ?';
-                await transaction.query(sql, [contract.cid ? contract.cid : contract.id, selfAudit.times, selfAudit.order]);
+                const sql = 'UPDATE ' + this.tableName + ' SET `order` = `order` + 2 WHERE cid = ? AND cpid' + (cpid ? ' = ' + cpid : ' is null') + ' AND csid' + (csid ? ' = ' + csid : ' is null') + ' AND times = ? AND `order` > ?';
+                await transaction.query(sql, [cid, selfAudit.times, selfAudit.order]);
                 // 上一审批人,当前审批人 再次添加至流程
                 const newAuditors = [];
                 preAuditors.forEach(x => {
                     newAuditors.push({
-                        spid: contract.spid || null, tid: contract.tid || null, cid: contract.cid ? contract.cid : contract.id, cpid: contract.cid ? contract.id : null, aid: x.aid,
+                        spid: contract.spid || null, tid: contract.tid || null, cid, cpid, csid, aid: x.aid,
                         times: x.times, order: selfAudit.order + 1,
                         status: auditConst.status.checking, begin_time: time,
                         audit_type: x.audit_type, audit_order: x.audit_order,
@@ -823,7 +905,7 @@ module.exports = app => {
                 const newFlowAuditors = [];
                 flowAudits.forEach(x => {
                     newFlowAuditors.push({
-                        spid: contract.spid || null, tid: contract.tid || null, cid: contract.cid ? contract.cid : contract.id, cpid: contract.cid ? contract.id : null, aid: x.aid,
+                        spid: contract.spid || null, tid: contract.tid || null, cid, cpid, csid, aid: x.aid,
                         times: x.times, order: selfAudit.order + 2,
                         status: auditConst.status.uncheck,
                         audit_type: x.audit_type, audit_order: x.audit_order,
@@ -842,13 +924,13 @@ module.exports = app => {
             }
         }
 
-        async getAuditorGroup(cid, cpid, times) {
-            const auditors = await this.getAuditors(cid, cpid, times); // 全部参与的审批人
+        async getAuditorGroup(contract, type, times) {
+            const auditors = await this.getAuditors(contract, type, times); // 全部参与的审批人
             return this.ctx.helper.groupAuditors(auditors, 'order');
         }
 
-        async getUserGroup(cid, cpid, times) {
-            const group = await this.getAuditorGroup(cid, cpid, times);
+        async getUserGroup(contract, type, times) {
+            const group = await this.getAuditorGroup(contract, type, times);
             // const sql =
             //     'SELECT pa.`id` As aid, pa.`name`, pa.`company`, pa.`role`, ? As times, ? As cpid, 0 As `order`, 1 As audit_type, 0 As audit_order' +
             //     '  FROM ' + this.ctx.service.changePlan.tableName + ' As s' +
@@ -862,8 +944,8 @@ module.exports = app => {
             return group;
         }
 
-        async getUniqUserGroup(cid, cpid, times) {
-            const group = await this.getAuditorGroup(cid, cpid, times);
+        async getUniqUserGroup(contract, type, times) {
+            const group = await this.getAuditorGroup(contract, type, times);
             // const sql =
             //     'SELECT pa.`id` As aid, pa.`name`, pa.`company`, pa.`role`, ? As times, ? As cpid, 0 As `order`, 1 As audit_type, 0 As audit_order' +
             //     '  FROM ' + this.ctx.service.changePlan.tableName + ' As s' +
@@ -877,11 +959,11 @@ module.exports = app => {
             return this.ctx.helper.groupAuditorsUniq(group);
         }
 
-        async getAuditorHistory(cid, cpid, times, reverse = false) {
+        async getAuditorHistory(contract, type, times, reverse = false) {
             const history = [];
             if (times >= 1) {
                 for (let i = 1; i <= times; i++) {
-                    const auditors = await this.getAuditors(cid, cpid, i);
+                    const auditors = await this.getAuditors(contract, type, i);
                     const group = this.ctx.helper.groupAuditors(auditors);
                     const historyGroup = [];
                     // 找出group里audit_order最大值
@@ -936,8 +1018,8 @@ module.exports = app => {
             return history;
         }
 
-        async getUniqAuditor(cid, cpid, times, noYb = false) {
-            const auditors = await this.getAuditors(cid, cpid, times, 'asc', noYb); // 全部参与的审批人
+        async getUniqAuditor(contract, type, times, noYb = false) {
+            const auditors = await this.getAuditors(contract, type, times, 'asc', noYb); // 全部参与的审批人
             const result = [];
             auditors.forEach(x => {
                 if (result.findIndex(r => { return x.aid === r.aid && x.audit_order === r.audit_order; }) < 0) {
@@ -947,44 +1029,62 @@ module.exports = app => {
             return result;
         }
 
-        async makeAudits(transaction, options, cid, cpid = null, uid) {
+        async makeAudits(transaction, options, contract, type, uid) {
+            const cid = contract.cid ? contract.cid : contract.id;
+            const cpid = type === 'pay' ? contract.id : null;
+            const csid = type === 'supplement' ? contract.id : null;
             // 创建审批人列表
-            const lastContractInfo = await this.getHaveAuditLastInfo(options, cid, cpid, uid);
+            const lastContractInfo = await this.getHaveAuditLastInfo(options, contract, type, uid);
+            let auditList = [];
             if (lastContractInfo) {
                 // 再获取非原报审批人
-                const auditList = await this.getUniqAuditor(lastContractInfo.cid ? lastContractInfo.cid : lastContractInfo.id, lastContractInfo.cid ? lastContractInfo.id : null, lastContractInfo.times, true); // 全部参与的审批人
-                if (auditList.length > 0) {
-                    const spAudits = [];
-                    for (const x of auditList) {
-                        if (x.audit_order !== 0) {
-                            spAudits.push({
-                                spid: options.spid || null,
-                                tid: options.tid || null,
-                                cid, cpid, aid: x.aid,
-                                times: 1, order: x.audit_order, status: auditConst.status.uncheck,
-                                audit_type: x.audit_type, audit_order: x.audit_order,
-                            });
-                        }
+                auditList = await this.getUniqAuditor(lastContractInfo, type, lastContractInfo.times, true); // 全部参与的审批人
+            } else if ((type === 'supplement' || type === 'pay') && contract.cid) {
+                // 取该合同的审批人列表
+                const contractInfo = await this.ctx.service.contract.getDataById(contract.cid);
+                auditList = await this.getUniqAuditor(contractInfo, 'contract', contractInfo.times, true); // 全部参与的审批人
+            }
+            if (auditList.length > 0) {
+                const spAudits = [];
+                for (const x of auditList) {
+                    if (x.audit_order !== 0) {
+                        spAudits.push({
+                            spid: options.spid || null,
+                            tid: options.tid || null,
+                            cid, cpid, csid, aid: x.aid,
+                            times: 1, order: x.audit_order, status: auditConst.status.uncheck,
+                            audit_type: x.audit_type, audit_order: x.audit_order,
+                        });
                     }
-                    if (spAudits.length > 0) await transaction.insert(this.ctx.service.contractSpAudit.tableName, spAudits);
                 }
+                if (spAudits.length > 0) await transaction.insert(this.ctx.service.contractSpAudit.tableName, spAudits);
             }
         }
 
-        async getHaveAuditLastInfo(options, cid, cpid, uid) {
+        async getHaveAuditLastInfo(options, contract, type, uid) {
             const optionsSql = options.spid ? 'c.spid = "' + options.spid + '"' : 'c.tid = ' + options.tid;
-            const tableName = cpid ? this.ctx.service.contractPay.tableName : this.ctx.service.contract.tableName;
-            const joinSql = cpid ? 'ca.cpid' : 'ca.cid';
-            const cpidSql = cpid ? 'AND ca.`cpid` is not null' : 'AND ca.`cpid` is null';
-            const sql = 'SELECT c.* FROM ?? as c LEFT JOIN ?? as ca ON c.`id` = ' + joinSql + ' WHERE ' + optionsSql + ' AND c.`contract_type` = ? AND c.`uid` = ? ' + cpidSql + ' AND ca.`audit_order` > 0 ORDER BY c.`create_time` DESC';
+            const cpid = type === 'pay' ? contract.id : null;
+            const csid = type === 'supplement' ? contract.id : null;
+            const tableName = {
+                contract: this.ctx.service.contract.tableName,
+                pay: this.ctx.service.contractPay.tableName,
+                supplement: this.ctx.service.contractSupplement.tableName,
+            }[type];
+            const joinSql = cpid ? 'ca.`cpid`' : (csid ? 'ca.`csid`' : 'ca.`cid`');
+            const cpidSql = cpid ? ' AND ca.`cpid` is not null' : ' AND ca.`cpid` is null';
+            const csidSql = cpid ? ' AND ca.`csid` is not null' : ' AND ca.`csid` is null';
+            const sql = 'SELECT c.* FROM ?? as c LEFT JOIN ?? as ca ON c.`id` = ' + joinSql + ' WHERE ' + optionsSql + ' AND c.`contract_type` = ? AND c.`uid` = ?' + cpidSql + csidSql + ' AND ca.`audit_order` > 0 ORDER BY c.`create_time` DESC';
             const sqlParam = [tableName, this.tableName, options.contract_type, uid];
             return await this.db.queryOne(sql, sqlParam);
         }
 
-        async saveAudit(contract, times, sp_group, data) {
+        async saveAudit(contract, type, times, sp_group, data) {
             const transaction = await this.db.beginTransaction();
             try {
-                const auditors = await this.getAuditGroupByList(contract.cid || contract.id, contract.cid ? contract.id : null, times);
+                const cid = contract.cid ? contract.cid : contract.id;
+                const cpid = type === 'pay' ? contract.id : null;
+                const csid = type === 'supplement' ? contract.id : null;
+                const auditors = await this.getAuditGroupByList(contract, type, times);
                 const now_audit = this._.find(auditors, { aid: data.old_aid });
                 if (data.operate !== 'del') {
                     // const exist = await this.getDataByCondition({ cpid: cpId, times, aid: data.new_aid });
@@ -998,13 +1098,13 @@ module.exports = app => {
                         throw '当前人下无法操作新增';
                     }
                     const newAudit = {
-                        spid: contract.spid || null, tid: contract.tid || null, cid: contract.cid || contract.id, cpid: contract.cid ? contract.id : null, aid: data.new_aid,
+                        spid: contract.spid || null, tid: contract.tid || null, cid, cpid, csid, aid: data.new_aid,
                         order: now_audit.order + 1,
                         audit_order: now_audit.audit_order + 1, audit_type: auditType.key.common,
                         times, status: auditConst.status.uncheck,
                     };
                     // order+1
-                    await this._syncOrderByDelete(transaction, contract.cid || contract.id, contract.cid ? contract.id : null, now_audit.order + 1, times, '+');
+                    await this._syncOrderByDelete(transaction, contract, type, now_audit.order + 1, times, '+');
                     await transaction.insert(this.tableName, newAudit);
                     // 更新审批流程页数据,如果存在
                 } else if (data.operate === 'add-sibling') {
@@ -1012,7 +1112,7 @@ module.exports = app => {
                         throw '当前人下无法操作新增';
                     }
                     const newAudit = {
-                        spid: contract.spid || null, tid: contract.tid || null, cid: contract.cid || contract.id, cpid: contract.cid ? contract.id : null, aid: data.new_aid,
+                        spid: contract.spid || null, tid: contract.tid || null, cid, cpid, csid, aid: data.new_aid,
                         order: now_audit.order,
                         audit_order: now_audit.audit_order, audit_type: now_audit.audit_type,
                         times, status: auditConst.status.uncheck,
@@ -1023,8 +1123,8 @@ module.exports = app => {
                         throw '当前人无法操作删除';
                     }
                     const flowAuditors = auditors.filter(x => { return x.audit_order === now_audit.audit_order; });
-                    await transaction.delete(this.tableName, { cid: contract.cid || contract.id, cpid: contract.cid ? contract.id : null, times, aid: now_audit.aid, order: now_audit.order });
-                    if (flowAuditors.length === 1) await this._syncOrderByDelete(transaction, contract.cid || contract.id, contract.cid ? contract.id : null, now_audit.order, times);
+                    await transaction.delete(this.tableName, { cid, cpid, csid, times, aid: now_audit.aid, order: now_audit.order });
+                    if (flowAuditors.length === 1) await this._syncOrderByDelete(transaction, contract, type, now_audit.order, times);
                     // 旧的更新为is_old为1
                     // await transaction.update(this.tableName, { is_old: 1 }, {
                     //     where: {
@@ -1034,7 +1134,7 @@ module.exports = app => {
                     //     }
                     // });
                 } else if (data.operate === 'change') {
-                    const nowAudit = await this.getDataByCondition({ cid: contract.cid || contract.id, cpid: contract.cid ? contract.id : null, times, aid: now_audit.aid, order: now_audit.order });
+                    const nowAudit = await this.getDataByCondition({ cid, cpid, csid, times, aid: now_audit.aid, order: now_audit.order });
                     if (now_audit.status !== auditConst.status.uncheck || !nowAudit) {
                         throw '当前人无法操作替换';
                     }
@@ -1052,24 +1152,24 @@ module.exports = app => {
                 if (this.ctx.contract_tender) {
                     const tenderInfo = await this.ctx.service.tenderInfo.getTenderInfo(this.ctx.contract.id);
                     if (tenderInfo.shenpi.contract === shenpiConst.sp_status.gdspl) {
-                        const newAuditors = await transaction.select(this.tableName, { where: { cid: contract.cid || contract.id, cpid: contract.cid ? contract.id : null, times }, orders: [['order', 'asc']] });
+                        const newAuditors = await transaction.select(this.tableName, { where: { cid, cpid, csid, times }, orders: [['order', 'asc']] });
                         newAuditors.shift();
                         const newAuditorGroup = this.ctx.helper.groupAuditors(newAuditors, 'order');
                         const uniqNewAuditorGroup = this.ctx.helper.groupAuditorsUniq(newAuditorGroup);
                         await this.ctx.service.shenpiAudit.updateAuditListWithAuditType(transaction, this.ctx.contract.id, tenderInfo.shenpi.contract, shenpiConst.sp_type.contract, uniqNewAuditorGroup, sp_group);
                     } else if (tenderInfo.shenpi.contract === shenpiConst.sp_status.gdzs) {
-                        const newAuditors = await this.getAuditGroupByList(contract.cid || contract.id, contract.cid ? contract.id : null, times, transaction);
+                        const newAuditors = await this.getAuditGroupByList(contract, type, times, transaction);
                         await this.ctx.service.shenpiAudit.updateAuditList(transaction, this.ctx.contract.id, tenderInfo.shenpi.contract, shenpiConst.sp_type.contract, this._.map(newAuditors, 'aid'));
                     }
                 } else {
                     if (this.ctx.subProject.shenpi.contract === shenpiConst.sp_status.gdspl) {
-                        const newAuditors = await transaction.select(this.tableName, { where: { cid: contract.cid || contract.id, cpid: contract.cid ? contract.id : null, times }, orders: [['order', 'asc']] });
+                        const newAuditors = await transaction.select(this.tableName, { where: { cid, cpid, csid, times }, orders: [['order', 'asc']] });
                         newAuditors.shift();
                         const newAuditorGroup = this.ctx.helper.groupAuditors(newAuditors, 'order');
                         const uniqNewAuditorGroup = this.ctx.helper.groupAuditorsUniq(newAuditorGroup);
                         await this.ctx.service.shenpiAudit.updateAuditListWithAuditType(transaction, this.ctx.subProject.id, this.ctx.subProject.shenpi.contract, shenpiConst.sp_type.contract, uniqNewAuditorGroup, sp_group);
                     } else if (this.ctx.subProject.shenpi.contract === shenpiConst.sp_status.gdzs) {
-                        const newAuditors = await this.getAuditGroupByList(contract.cid || contract.id, contract.cid ? contract.id : null, times, transaction);
+                        const newAuditors = await this.getAuditGroupByList(contract, type, times, transaction);
                         await this.ctx.service.shenpiAudit.updateAuditList(transaction, this.ctx.subProject.id, this.ctx.subProject.shenpi.contract, shenpiConst.sp_type.contract, this._.map(newAuditors, 'aid'));
                     }
                 }
@@ -1081,7 +1181,7 @@ module.exports = app => {
             }
         }
 
-        async changeSpGroup(contract, sp_group) {
+        async changeSpGroup(contract, type, sp_group) {
             const transaction = await this.db.beginTransaction();
             try {
                 const group = await this.ctx.service.shenpiGroup.getDataById(sp_group);
@@ -1096,11 +1196,13 @@ module.exports = app => {
                 }
                 const shenpiList = await this.ctx.service.shenpiAudit.getAllDataByCondition({ where: condition });
                 // await this.ctx.service.shenpiAudit.noYbShenpiList(change.uid, shenpiList);
-                await this.updateNewAuditors(contract, shenpiList, transaction);
-                if (contract.cid === undefined) {
+                await this.updateNewAuditors(contract, type, shenpiList, transaction);
+                if (type === 'contract') {
                     await transaction.update(this.ctx.service.contract.tableName, { id: contract.id, sp_group: group.id });
-                } else {
+                } else if (type === 'pay') {
                     await transaction.update(this.ctx.service.contractPay.tableName, { id: contract.id, sp_group: group.id });
+                } else if (type === 'supplement') {
+                    await transaction.update(this.ctx.service.contractSupplement.tableName, { id: contract.id, sp_group: group.id });
                 }
                 await transaction.commit();
             } catch (err) {
@@ -1110,10 +1212,10 @@ module.exports = app => {
             return true;
         }
 
-        async updateNewAuditList(contract, newList) {
+        async updateNewAuditList(contract, type, newList) {
             const transaction = await this.db.beginTransaction();
             try {
-                await this.updateNewAuditors(contract, newList, transaction);
+                await this.updateNewAuditors(contract, type, newList, transaction);
                 await transaction.commit();
             } catch (err) {
                 await transaction.rollback();
@@ -1121,22 +1223,21 @@ module.exports = app => {
             }
         }
 
-        async updateNewAuditors(contract, newList, transaction) {
+        async updateNewAuditors(contract, type, newList, transaction) {
             const condition = { times: contract.times };
-            if (contract.cid !== undefined) {
-                condition.cid = contract.cid;
-                condition.cpid = contract.id;
-            } else {
-                condition.cid = contract.id;
-                condition.cpid = null;
-            }
+            const cid = contract.cid ? contract.cid : contract.id;
+            const cpid = type === 'pay' ? contract.id : null;
+            const csid = type === 'supplement' ? contract.id : null;
+            condition.cid = cid;
+            condition.cpid = cpid;
+            condition.csid = csid;
             // 先删除旧的审批流,再添加新的
             await transaction.delete(this.tableName, condition);
 
             const newAuditors = [];
             for (const auditor of newList) {
                 newAuditors.push({
-                    spid: contract.spid || null, tid: contract.tid || null, cid: contract.cid || contract.id, cpid: contract.cid ? contract.id : null, aid: auditor.audit_id,
+                    spid: contract.spid || null, tid: contract.tid || null, cid, cpid, csid, aid: auditor.audit_id,
                     times: contract.times, order: auditor.audit_order, status: auditConst.status.uncheck,
                     audit_type: auditor.audit_type, audit_order: auditor.audit_order,
                 });
@@ -1156,21 +1257,20 @@ module.exports = app => {
             }
         }
 
-        async updateLastAudit(contract, auditList, lastId) {
+        async updateLastAudit(contract, type, auditList, lastId) {
             const transaction = await this.db.beginTransaction();
             try {
                 // 先判断auditList里的aid是否与lastId相同,相同则删除并重新更新order
                 const existAudit = auditList.find(x => { return x.aid === lastId; });
+                const cid = contract.cid ? contract.cid : contract.id;
+                const cpid = type === 'pay' ? contract.id : null;
+                const csid = type === 'supplement' ? contract.id : null;
                 let order = auditList.length > 0 ? auditList.reduce((rst, a) => { return Math.max(rst, a.order); }, 0) + 1 : 1; // 最大值 + 1
                 if (existAudit) {
                     const condition = { times: contract.times, aid: lastId };
-                    if (contract.cid !== undefined) {
-                        condition.cid = contract.cid;
-                        condition.cpid = contract.id;
-                    } else {
-                        condition.cid = contract.id;
-                        condition.cpid = null;
-                    }
+                    condition.cid = cid;
+                    condition.cpid = cpid;
+                    condition.csid = csid;
                     await transaction.delete(this.tableName, condition);
                     const sameOrder = auditList.filter(x => { return x.order === existAudit.order; });
                     if (sameOrder.length === 1) {
@@ -1187,7 +1287,7 @@ module.exports = app => {
                 }
                 // 添加终审
                 const newAuditor = {
-                    spid: contract.spid || null, tid: contract.tid || null, cid: contract.cid || contract.id, cpid: contract.cid ? contract.id : null, aid: lastId,
+                    spid: contract.spid || null, tid: contract.tid || null, cid, cpid, csid, aid: lastId,
                     times: contract.times, order, status: auditConst.status.uncheck,
                     audit_type: auditType.key.common, audit_order: order,
                 };

+ 10 - 7
app/service/contract_supplement.js

@@ -32,16 +32,17 @@ module.exports = app => {
             const shenpi_status = this.ctx.contract_tender ? this.ctx.subProject.page_show.openContractTenderShenpi : this.ctx.subProject.page_show.openContractSubProjectShenpi;
             if (list.length > 0) {
                 const userList = await this.ctx.service.projectAccount.getAllDataByCondition({ where: { id: list.map(item => item.uid) } });
+                const type = 'supplement';
                 for (const l of list) {
                     const userInfo = userList.find(item => item.id === l.uid);
                     l.username = userInfo ? userInfo.name : '';
                     l.files = await this.ctx.service.contractSupplementAtt.getAtt(l.id);
                     if (shenpi_status) {
-                        await this.ctx.service.contractSpAudit.loadUser(l);
-                        await this.ctx.service.contractSpAudit.loadAuditViewData(l);
-                        await this.ctx.service.contractSpAudit.checkShenpi(l);
+                        await this.ctx.service.contractSpAudit.loadUser(l, type);
+                        await this.ctx.service.contractSpAudit.loadAuditViewData(l, type);
+                        await this.ctx.service.contractSpAudit.checkShenpi(l, type);
                         if (l.status !== auditConst.status.checked || !l.final_auditor_str) {
-                            l.curAuditors2 = await this.ctx.service.contractSpAudit.getAuditorsByStatus(l.cid, l.id, l.status, l.times);
+                            l.curAuditors2 = await this.ctx.service.contractSpAudit.getAuditorsByStatus(l, type, l.status, l.times);
                             if (l.status === auditConst.status.checked && !l.final_auditor_str) {
                                 const final_auditor_str = l.curAuditors2[0].audit_type === auditType.key.common
                                     ? l.curAuditors2[0].name + (l.curAuditors2[0].role ? '-' + l.curAuditors2[0].role : '')
@@ -72,8 +73,8 @@ module.exports = app => {
                     code: data.code,
                     name: data.name,
                     price: data.price || 0,
-                    party_a: data.party_a || 0,
-                    party_b: data.party_b || 0,
+                    party_a: data.party_a,
+                    party_b: data.party_b,
                     sign_time: data.sign_time || null,
                     address: data.address,
                     content: data.content,
@@ -82,7 +83,8 @@ module.exports = app => {
                 };
                 const result = await transaction.insert(this.tableName, insertData);
                 await this.calcContract(transaction, node);
-                await this.ctx.service.contractSpAudit.makeAudits(transaction, options, cid, result.insertId, this.ctx.session.sessionUser.accountId);
+                insertData.id = result.insertId;
+                await this.ctx.service.contractSpAudit.makeAudits(transaction, options, insertData, 'supplement', this.ctx.session.sessionUser.accountId);
                 await transaction.commit();
             } catch (err) {
                 await transaction.rollback();
@@ -151,6 +153,7 @@ module.exports = app => {
                 supplement_price = this.ctx.helper.add(supplement_price, l.price);
             }
             node.supplement_price = supplement_price;
+            node.total_price = this.ctx.helper.add(node.price, supplement_price);
             node.exist_supplement = supplementsList.length === 0 ? 0 : 1;
             await transaction.update(this.ctx.service.contract.tableName, node);
         }

+ 12 - 4
app/service/contract_tree.js

@@ -381,11 +381,13 @@ module.exports = app => {
                     if (select.uid !== this.ctx.session.sessionUser.accountId && !this.ctx.session.sessionUser.is_admin) throw '当前合同无权删除';
                     const contractPays = await this.ctx.service.contractPay.getDataByCondition({ cid: select.id });
                     if (contractPays) throw '还存在合同支付项,无法删除';
+                    const contractSupplements = await this.ctx.service.contractSupplement.getDataByCondition({ cid: select.id });
+                    if (contractSupplements) throw '还存在补充合同,无法删除';
                     await transaction.delete(this.ctx.service.contract.tableName, { id: select.id });
                     const attList = await this.ctx.service.contractAtt.getAllDataByCondition({ where: { cid: select.id } });
                     await this.ctx.helper.delFiles(attList);
                     await transaction.delete(this.ctx.service.contractAtt.tableName, { cid: select.id });
-                    await transaction.delete(this.ctx.service.contractSpAudit.tableName, { cid: select.id, cpid: null });
+                    await transaction.delete(this.ctx.service.contractSpAudit.tableName, { cid: select.id, cpid: null, csid: null });
                 } else {
                     await transaction.delete(this.tableName, { id: select.id });
                     const delOptions = this._.cloneDeep(options);
@@ -399,10 +401,12 @@ module.exports = app => {
                         if (contractUids.length > 1 || !(contractUids[0] === this.ctx.session.sessionUser.accountId || this.ctx.session.sessionUser.is_admin)) throw '存在合同你无权删除';
                         const contractPays = await transaction.select(this.ctx.service.contractPay.tableName, { where: { cid: this.ctx.helper._.map(contracts, 'id') } });
                         if (contractPays.length > 0) throw '还存在合同支付项,无法删除';
+                        const contractSupplements = await this.ctx.service.contractSupplement.getDataByCondition({ cid: this.ctx.helper._.map(contracts, 'id') });
+                        if (contractSupplements) throw '还存在补充合同,无法删除';
                         const attList = await this.ctx.service.contractAtt.getAllDataByCondition({ where: { cid: this.ctx.helper._.map(contracts, 'id') } });
                         await this.ctx.helper.delFiles(attList);
                         await transaction.delete(this.ctx.service.contractAtt.tableName, { cid: this.ctx.helper._.map(contracts, 'id') });
-                        await transaction.delete(this.ctx.service.contractSpAudit.tableName, { cid: this.ctx.helper._.map(contracts, 'id'), cpid: null });
+                        await transaction.delete(this.ctx.service.contractSpAudit.tableName, { cid: this.ctx.helper._.map(contracts, 'id'), cpid: null, csid: null });
                     }
                     const operate = await this._deletePosterity(options, select, transaction);
                 }
@@ -462,11 +466,13 @@ module.exports = app => {
                         if (s.uid !== this.ctx.session.sessionUser.accountId && !this.ctx.session.sessionUser.is_admin) throw '存在合同你无权删除';
                         const contractPays = await this.ctx.service.contractPay.getDataByCondition({ cid: s.id });
                         if (contractPays) throw '部分合同还存在合同支付项,无法删除';
+                        const contractSupplements = await this.ctx.service.contractSupplement.getDataByCondition({ cid: s.id });
+                        if (contractSupplements) throw '部分合同还存在补充合同,无法删除';
                         await transaction.delete(this.ctx.service.contract.tableName, { id: s.id });
                         const attList = await this.ctx.service.contractAtt.getAllDataByCondition({ where: { cid: s.id } });
                         await this.ctx.helper.delFiles(attList);
                         await transaction.delete(this.ctx.service.contractAtt.tableName, { cid: s.id });
-                        await transaction.delete(this.ctx.service.contractSpAudit.tableName, { cid: s.id, cpid: null });
+                        await transaction.delete(this.ctx.service.contractSpAudit.tableName, { cid: s.id, cpid: null, csid: null });
                     } else {
                         await transaction.delete(this.tableName, { id: s.id });
                         const contracts = _.filter(deleteData, function (item) {
@@ -477,10 +483,12 @@ module.exports = app => {
                             if (contractUids.length > 1 || !(contractUids[0] === this.ctx.session.sessionUser.accountId || this.ctx.session.sessionUser.is_admin)) throw '存在合同你无权删除';
                             const contractPays = await transaction.select(this.ctx.service.contractPay.tableName, { where: { cid: _.map(contracts, 'id') } });
                             if (contractPays.length > 0) throw '还存在合同支付项,无法删除';
+                            const contractSupplements = await this.ctx.service.contractSupplement.getDataByCondition({ cid: _.map(contracts, 'id') });
+                            if (contractSupplements) throw '部分合同还存在补充合同,无法删除';
                             const attList = await this.ctx.service.contractAtt.getAllDataByCondition({ where: { cid: _.map(contracts, 'id') } });
                             await this.ctx.helper.delFiles(attList);
                             await transaction.delete(this.ctx.service.contractAtt.tableName, { cid: _.map(contracts, 'id') });
-                            await transaction.delete(this.ctx.service.contractSpAudit.tableName, { cid: _.map(contracts, 'id'), cpid: null });
+                            await transaction.delete(this.ctx.service.contractSpAudit.tableName, { cid: _.map(contracts, 'id'), cpid: null, csid: null });
                         }
                     }
                     const operate = await this._deletePosterity(options, s, transaction);

+ 41 - 10
app/service/cost_stage.js

@@ -25,13 +25,14 @@ module.exports = app => {
             super(ctx);
             this.tableName = 'cost_stage';
             this.stageType = {
-                ledger: { key: 'ledger', name: '成本报审', decimal: { tp: 6, tax: 2 }, shenpi_status: 'cost_stage_ledger', push_type: 'costStageLedger', dataService: 'costStageLedger', detailService: 'costStageDetail' },
-                book: { key: 'book', name: '财务账面', decimal: { tp: 6 }, shenpi_status: 'cost_stage_book', push_type: 'costStageBook', dataService: 'costStageBook', detailService: 'costStageBookDetail' },
-                analysis: { key: 'analysis', name: '成本分析', decimal: { tp: 6 }, shenpi_status: 'cost_stage_analysis', push_type: 'costStageAnalysis', dataService: 'costStageAnalysis', detailService: 'costStageAnalysisDetail' },
+                duty: { key: 'duty', name: '责任成本', decimal: { tp: 6, tax: 2, excl_tax_tp: 6 }, shenpi_status: 'cost_stage_duty', push_type: 'costStageDuty', dataService: 'costStageDuty' },
+                ledger: { key: 'ledger', name: '成本报审', decimal: { tp: 6, tax: 2, excl_tax_tp: 6 }, shenpi_status: 'cost_stage_ledger', push_type: 'costStageLedger', dataService: 'costStageLedger', detailService: 'costStageDetail' },
+                book: { key: 'book', name: '财务账面', decimal: { tp: 6, tax: 2, excl_tax_tp: 6 }, shenpi_status: 'cost_stage_book', push_type: 'costStageBook', dataService: 'costStageBook', detailService: 'costStageBookDetail' },
+                analysis: { key: 'analysis', name: '成本分析', decimal: { tp: 6, tax: 2, excl_tax_tp: 6 }, shenpi_status: 'cost_stage_analysis', push_type: 'costStageAnalysis', dataService: 'costStageAnalysis', detailService: 'costStageAnalysisDetail' },
             };
         }
 
-        _analysisstage(stage) {
+        _analysisStage(stage) {
             if (!stage) return;
             const stages = stage instanceof Array ? stage : [stage];
             if (stage.length === 0) return;
@@ -51,9 +52,10 @@ module.exports = app => {
                 }
                 s.typeInfo = this.stageType[s.stage_type];
                 s.rela_stage = s.rela_stage ? JSON.parse(s.rela_stage) : null;
+                s.extra_info = s.extra_info ? JSON.parse(s.extra_info) : {};
             });
         }
-        async calcRealtime(stage) {
+        async calcRuntime(stage) {
             const typeInfo = this.stageType[stage.stage_type];
             if (!typeInfo) return;
 
@@ -71,24 +73,24 @@ module.exports = app => {
             const sql = `SELECT cls.*, pa.name AS user_name FROM ${this.tableName} cls LEFT JOIN ${this.ctx.service.projectAccount.tableName} pa ON cls.create_user_id = pa.id` +
                         `  WHERE cls.tid = ? and cls.stage_type = ? ORDER BY cls.stage_order ${sort}`;
             const result = await this.db.query(sql, [tid, stage_type]);
-            this._analysisstage(result);
+            this._analysisStage(result);
             return result;
         }
         async getAllCheckedStages(tid, stage_type, sort = 'ASC') {
             const sql = `SELECT cls.*, pa.name AS user_name FROM ${this.tableName} cls LEFT JOIN ${this.ctx.service.projectAccount.tableName} pa ON cls.create_user_id = pa.id` +
                 `  WHERE cls.tid = ? AND cls.stage_type = ? AND audit_status = ? ORDER BY cls.stage_order ${sort}`;
             const result = await this.db.query(sql, [tid, stage_type, audit.status.checked]);
-            this._analysisstage(result);
+            this._analysisStage(result);
             return result;
         }
         async getStage(id) {
             const result = await this.getDataById(id);
-            this._analysisstage(result);
+            this._analysisStage(result);
             return result;
         }
         async getStageByOrder(tid, stage_type, stage_order) {
             const result = await this.getDataByCondition({ tid, stage_type, stage_order });
-            this._analysisstage(result);
+            this._analysisStage(result);
             return result;
         }
 
@@ -117,6 +119,7 @@ module.exports = app => {
             const preStage = maxOrder > 0 ? await this.getStageByOrder(tid, stage_type, maxOrder) : null;
             if (preStage) {
                 data.stage_pre_tp = JSON.stringify(preStage.stage_end_tp);
+                data.decimal = JSON.stringify(preStage.decimal);
             }
             const transaction = await this.db.beginTransaction();
             try {
@@ -294,6 +297,7 @@ module.exports = app => {
             const info = this.ctx.tender.info;
             const typeInfo = this.stageType[stage.stage_type];
             const shenpi_status = info.shenpi[typeInfo.shenpi_status];
+            console.log(typeInfo.shenpi_status, info.shenpi);
             if ((stage.audit_status === status.uncheck || stage.audit_status === status.checkNo) && shenpi_status !== shenpiConst.sp_status.sqspr) {
                 // 进一步比较审批流是否与审批流程设置的相同,不同则替换为固定审批流或固定的终审
                 const auditList = await this.ctx.service.costStageAudit.getAllDataByCondition({ where: { stage_id: stage.id, audit_times: stage.audit_times }, orders: [['audit_order', 'asc']] });
@@ -329,7 +333,34 @@ module.exports = app => {
                 }
             }
         }
+
+        async getLastCheckedStage(tid, stage_type) {
+            const lastCheckedStage = await this.ctx.service.costStage.getAllDataByCondition({
+                where: { tid, stage_type, audit_status: audit.status.checked },
+                orders: [['stage_order', 'desc']],
+            });
+            if (lastCheckedStage && lastCheckedStage.length > 0) {
+                this._analysisStage(lastCheckedStage[0]);
+                return lastCheckedStage[0];
+            }
+            return null;
+        }
+
+        async getNumByChecked(tenderId, stage_type) {
+            const num = await this.db.queryOne(`SELECT COUNT(*) as num From ${this.tableName} WHERE tid = ? and stage_type = ? and audit_status = ?`, [tenderId, stage_type, audit.status.checked]);
+            return num ? num.num : 0;
+        }
+
+        async saveExtraInfo(stage, data) {
+            const extra_info = this.ctx.helper._.assign(stage.extra_info, data);
+            try {
+                await this.defaultUpdate({ id: stage.id, extra_info: JSON.stringify(extra_info) });
+                return extra_info;
+            } catch (err) {
+                throw '保存附加信息错误';
+            }
+        }
     }
 
     return CostStage;
-};
+};

+ 229 - 173
app/service/cost_stage_analysis.js

@@ -17,13 +17,13 @@ const costFields = {
     hisFields: ['calc_his'],
     taxFields: ['tax'],
     treeFields: ['tree_id', 'tree_pid', 'tree_level', 'tree_order', 'tree_full_path', 'tree_is_leaf'],
-    baseFields: ['id', 'cost_id', 'tender_id', 'stage_id', 'node_type', 'calc_type'],
+    baseFields: ['id', 'cost_id', 'tender_id', 'stage_id', 'node_type', 'calc_type', 'has_detail', 'import_cost_id'],
 };
 costFields.sumFields = [...costFields.calcFields, ...costFields.selfCalcFields];
 costFields.preCopyFields = [...costFields.treeFields, ...costFields.textFields, ...costFields.selfTextFields, ...costFields.calcFields, ...costFields.selfCalcFields, ...costFields.taxFields];
 costFields.editQueryFields = [...costFields.baseFields, ...costFields.treeFields, ...costFields.textFields, ...costFields.selfTextFields, ...costFields.calcFields, ...costFields.selfCalcFields, ...costFields.taxFields];
 costFields.readQueryFields = [...costFields.baseFields, ...costFields.treeFields, ...costFields.textFields, ...costFields.selfTextFields, ...costFields.readFields, ...costFields.taxFields];
-costFields.compareQueryFields = [...costFields.baseFields, ...costFields.treeFields, ...costFields.textFields, ...costFields.selfTextFields, ...costFields.taxFields, ...costFields.hisFields];
+costFields.compareQueryFields = [...costFields.baseFields, ...costFields.treeFields, ...costFields.textFields, ...costFields.selfTextFields, ...costFields.sumFields, ...costFields.taxFields, ...costFields.hisFields];
 const calcType = { other: 0, in: 1, out: 2 };
 
 const auditConst = require('../const/audit').costStage;
@@ -52,6 +52,7 @@ module.exports = app => {
             });
             // this.depart = ctx.app.config.table_depart.light;
             this.tableName = 'cost_stage_analysis';
+            this.calcFields = costFields.sumFields;
         }
 
         // 继承方法
@@ -60,6 +61,16 @@ module.exports = app => {
                 data[f] = 0;
             }
         }
+        /**
+         * 删除相关数据 继承
+         * @param mid
+         * @param deleteData
+         * @return {Promise<void>}
+         * @private
+         */
+        async _deleteRelaData(mid, deleteData) {
+            await this.transaction.delete(this.ctx.service.costStageFile.tableName, { rela_id: this._.map(deleteData, 'id') });
+        }
 
         _getDefaultData(data, stage) {
             data.id = this.uuid.v4();
@@ -129,14 +140,13 @@ module.exports = app => {
         async init(stage, transaction) {
             if (!this.ctx.subProject.cost_analysis_template) throw '未设置成本分析模板,请联系管理员设置';
             if (!stage || !transaction) throw '成本分析数据错误';
-            const userId = this.ctx.session.sessionUser.accountId;
 
             const templateData = await this.ctx.service.tenderNodeTemplate.getData(this.ctx.subProject.cost_analysis_template);
             if (templateData.length === 0) throw '台账模板无数据,连请联系管理员修改';
             const calcTemplate = await this.ctx.service.calcTmpl.getTemplate(stage.calc_template);
             if (!calcTemplate) throw '计算模板未定义,请联系管理员设置';
 
-            const insertTemplateData = [], insertLedgerData = [], insertDetailData = [];
+            const insertTemplateData = [];
             let maxId = 0;
             for (const b of templateData) {
                 const bills = {
@@ -149,68 +159,12 @@ module.exports = app => {
                 this._getDefaultData(bills, stage);
                 insertTemplateData.push(bills);
             }
-            const relaStage = await this.service.costStage.getStage(JSON.parse(stage.rela_stage).sid);
-            const costStageLedger = await this.getCostStageData(relaStage);
-            const costNode = insertTemplateData.find(x => { return x.node_type === 2 && x.tree_level === 1; });
-            if (costStageLedger.nodes.length > 0) costNode.tree_is_leaf = 0;
-            for (const [i, node] of costStageLedger.nodes.entries()) {
-                const parent = costStageLedger.getParent(node);
-                const newParent = parent ? insertLedgerData.find(x => { return x.tree_id === parent.new_tree_id; }) : costNode;
-                const ln = { calc_type: 2 };
-                this._getDefaultData(ln, stage);
-                ln.node_type = 0;
-                ln.cost_id = node.cost_id;
-                ln.tree_id = maxId + i + 1;
-                node.new_tree_id = ln.tree_id;
-                ln.tree_pid = newParent.tree_id;
-                ln.tree_order = node.tree_order;
-                ln.tree_level = newParent.tree_level + 1;
-                ln.tree_full_path = newParent.tree_full_path + '-' + ln.tree_id;
-                ln.tree_is_leaf = node.tree_is_leaf;
-                ln.code = node.code;
-                ln.name = node.name;
-                ln.unit = node.unit;
-                ln.tax = 0;
-                for (const prop of costFields.sumFields) {
-                    ln[prop] = 0;
-                }
-                insertLedgerData.push(ln);
-                if (node.detail && node.detail.length > 0) {
-                    for (const [i, d] of node.detail.entries()) {
-                        const idd = {
-                            id: this.uuid.v4(), tender_id: stage.tid, stage_id: stage.id,
-                            add_user_id: userId, update_user_id: userId,
-                            ledger_id: ln.id, cost_id: ln.cost_id, source_id: d.source_cid,
-                            d_order: i+1,
-                            code: d.code, name: d.name, party_b: d.party_b || '', tax: d.tax || 0,
-                            yf_excl_tax_tp: d.yf_excl_tax_tp || 0, in_excl_tax_tp: d.in_excl_tax_tp || 0, sf_excl_tax_tp: d.sf_excl_tax_tp || 0,
-                            is_deal: d.is_deal,
-                        };
-                        this.ctx.service.calcTmpl.calcByTemplate(idd, idd, idd, calcTemplate.calc_expr);
-                        idd.sf_percent = idd.yf_excl_tax_tp ? this.ctx.helper.mul(this.ctx.helper.div(idd.sf_excl_tax_tp, idd.yf_excl_tax_tp), 100, 2) : 0;
-                        insertDetailData.push(idd);
-                        for (const prop of costFields.sumFields) {
-                            ln[prop] = this.ctx.helper.add(ln[prop], idd[prop]);
-                        }
-                    }
-                } else {
-                    ln.tax = node.tax;
-                    ln.yf_excl_tax_tp = node.end_yf_excl_tax_tp || 0;
-                    ln.in_excl_tax_tp = node.end_in_excl_tax_tp || 0;
-                    ln.sf_excl_tax_tp = node.end_sf_excl_tax_tp || 0;
-                    this.ctx.service.calcTmpl.calcByTemplate(ln, ln, ln, calcTemplate.calc_expr);
-                }
-                ln.sf_percent = ln.yf_excl_tax_tp ? this.ctx.helper.mul(this.ctx.helper.div(ln.sf_excl_tax_tp, ln.yf_excl_tax_tp), 100, 2) : 0;
-            }
 
             await transaction.insert(this.tableName, insertTemplateData);
-            await transaction.insert(this.tableName, insertLedgerData);
-            if (insertDetailData.length > 0) await transaction.insert(this.ctx.service.costStageAnalysisDetail.tableName, insertDetailData);
         }
         async initByPre(stage, preStage, transaction) {
             if (!stage || !preStage || !transaction) throw '成本分析数据错误';
-            const userId = this.ctx.session.sessionUser.accountId;
-            const insertData = [], insertDetailData = [];
+            const insertData = [];
             let maxId = 0;
             if (!stage.calcTemplate) stage.calcTemplate = await this.service.calcTmpl.getTemplate(stage.calc_template);
 
@@ -224,94 +178,107 @@ module.exports = app => {
             const preDetails = await this.ctx.service.costStageAnalysisDetail.getReadData(preStage);
             for (const pl of preLedger.nodes) {
                 if (pl.tree_is_leaf) pl.detail = preDetails.filter(x => { return x.ledger_id === pl.id; });
-                if (pl.tree_level > 1) {
-                    const parent = preLedger.getTopParent(pl);
-                    if (parent.node_type === 2) continue;
-                }
                 const idata = {};
                 this._getDefaultData(idata, stage);
                 idata.cost_id = pl.cost_id;
                 idata.node_type = pl.node_type;
                 idata.calc_type = pl.calc_type;
-                for (const prop of costFields.preCopyFields) {
-                    idata[prop] = pl[prop];
+                idata.has_detail = pl.has_detail && pl.detail.length > 0;
+                idata.import_cost_id = pl.import_cost_id;
+                if (!idata.has_detail) {
+                    for (const prop of costFields.preCopyFields) {
+                        idata[prop] = pl[prop];
+                    }
                 }
                 maxId = Math.max(maxId, idata.tree_id);
                 insertData.push(idata);
             }
 
-            const relaStage = await this.service.costStage.getStage(JSON.parse(stage.rela_stage).sid);
-            const costStageLedger = await this.getCostStageData(relaStage);
-            const costNode = insertData.find(x => { return x.node_type === 2 && x.tree_level === 1; });
-            if (costStageLedger.nodes.length > 0) costNode.tree_is_leaf = 0;
-            for (const [i, node] of costStageLedger.nodes.entries()) {
-                const parent = costStageLedger.getParent(node);
-                const newParent = parent ? insertData.find(x => { return x.tree_id === parent.new_tree_id; }) : costNode;
-                const ln = { calc_type: 2 };
-                this._getDefaultData(ln, stage);
-                ln.node_type = 0;
-                ln.cost_id = node.cost_id;
-                ln.tree_id = maxId + i + 1;
-                node.new_tree_id = ln.tree_id;
-                ln.tree_pid = newParent.tree_id;
-                ln.tree_order = node.tree_order;
-                ln.tree_level = newParent.tree_level + 1;
-                ln.tree_full_path = newParent.tree_full_path + '-' + ln.tree_id;
-                ln.tree_is_leaf = node.tree_is_leaf;
-                ln.code = node.code;
-                ln.name = node.name;
-                ln.unit = node.unit;
-                ln.tax = node.tax;
-                ln.yf_excl_tax_tp = node.end_yf_excl_tax_tp || 0;
-                ln.in_excl_tax_tp = node.end_in_excl_tax_tp || 0;
-                ln.sf_excl_tax_tp = node.end_sf_excl_tax_tp || 0;
-                ln.postil = '';
-                ln.sf_percent = 0;
-                insertData.push(ln);
-                const preSource = preLedger.nodes.find(x => { return x.cost_id === node.cost_id; });
-                for (const prop of costFields.selfTextFields) {
-                    ln[prop] = preSource ? preSource[prop] : '';
-                }
-                if (!node.detail || node.detail.length === 0) {
-                    for (const prop of costFields.selfCalcFields) {
-                        ln[prop] = preSource ? preSource[prop] : 0;
-                    }
-                } else {
-                    for (const prop of costFields.sumFields) {
-                        ln[prop] = 0;
-                    }
-                    for (const [i, d] of node.detail.entries()) {
-                        const ld = {
-                            id: this.uuid.v4(), tender_id: stage.tid, stage_id: stage.id,
-                            add_user_id: userId, update_user_id: userId,
-                            ledger_id: ln.id, cost_id: ln.cost_id, source_id: d.source_cid,
-                            d_order: i+1,
-                            code: d.code, name: d.name, party_b: d.party_b || '', tax: ln.tax || 0,
-                            yf_excl_tax_tp: d.yf_excl_tax_tp || 0, in_excl_tax_tp: d.in_excl_tax_tp || 0, sf_excl_tax_tp: d.sf_excl_tax_tp || 0,
-                            is_deal: d.is_deal,
-                        };
-                        const preDetailSource = preSource ? preSource.detail.find(x => { return x.source_id === d.source_cid; }) : undefined;
-                        for (const prop of costFields.selfTextFields) {
-                            ld[prop] = preDetailSource ? preDetailSource[prop] : '';
-                        }
-                        for (const prop of costFields.selfCalcFields) {
-                            ld[prop] = preDetailSource ? preDetailSource[prop] || 0 : 0;
-                        }
-                        this.ctx.service.calcTmpl.calcByTemplate(ld, ld, ld, stage.calcTemplate.calc_expr);
-                        ld.sf_percent = ld.yf_excl_tax_tp ? this.ctx.helper.mul(this.ctx.helper.div(ld.sf_excl_tax_tp, ld.yf_excl_tax_tp), 100, 2) : 0;
-                        for (const prop of costFields.sumFields) {
-                            ln[prop] = this.ctx.helper.add(ln[prop], ld[prop]);
-                        }
-                        insertDetailData.push(ld);
-                    }
-                }
-                this.ctx.service.calcTmpl.calcByTemplate(ln, ln, ln, stage.calcTemplate.calc_expr);
-                ln.sf_percent = ln.yf_excl_tax_tp ? this.ctx.helper.mul(this.ctx.helper.div(ln.sf_excl_tax_tp, ln.yf_excl_tax_tp), 100, 2) : 0;
-            }
-
             await transaction.insert(this.tableName, insertData);
-            if (insertDetailData.length > 0) await transaction.insert(this.ctx.service.costStageAnalysisDetail.tableName, insertDetailData);
         }
+        // 以下为新增时,根据上一期导入的数据,自动导入明细等数据
+        // todo 暂未进行任何测试 -- 未考虑利润与利润率计算问题(待核实)
+        // async initByPre(stage, preStage, transaction) {
+        //     if (!stage || !preStage || !transaction) throw '成本分析数据错误';
+        //     const insertData = [], insertDetailData = [];
+        //     if (!stage.calcTemplate) stage.calcTemplate = await this.service.calcTmpl.getTemplate(stage.calc_template);
+        //
+        //     const costStageLedger = await this.getCostStageData(stage.relaStage);
+        //
+        //     const preBills = await this.getReadData(preStage);
+        //     const preLedger = new Ledger.baseTree(this.ctx, {
+        //         id: 'tree_id', pid: 'tree_pid', order: 'tree_order',
+        //         level: 'tree_level', isLeaf: 'tree_is_leaf', fullPath: 'tree_full_path',
+        //         rootId: -1, calcField: [],
+        //     });
+        //     preLedger.loadDatas(preBills);
+        //     const preDetails = await this.ctx.service.costStageAnalysisDetail.getReadData(preStage);
+        //     for (const pl of preLedger.nodes) {
+        //         if (pl.tree_is_leaf) pl.detail = preDetails.filter(x => { return x.ledger_id === pl.id; });
+        //         const idata = {};
+        //         this._getDefaultData(idata, stage);
+        //         idata.cost_id = pl.cost_id;
+        //         idata.node_type = pl.node_type;
+        //         idata.calc_type = pl.calc_type;
+        //         idata.has_detail = pl.has_detail && pl.detail.length > 0;
+        //         idata.import_cost_id = pl.import_cost_id;
+        //         if (!idata.has_detail) {
+        //             for (const prop of costFields.preCopyFields) {
+        //                 idata[prop] = pl[prop];
+        //             }
+        //         }
+        //         insertData.push(idata);
+        //         if (idata.import_cost_id) {
+        //             const cost_id = idata.import_cost_id.split(',');
+        //             const nodes = costStageLedger.nodes.filter(x => { return cost_id.indexOf(x.cost_id) >= 0; });
+        //             if (nodes.length > 0) {
+        //                 if (nodes[0].detail.length > 0) {
+        //                     const insertDetails = [];
+        //                     const preDetail = await this.ctx.service.costStageAnalysisDetail.getAllDataByCondition({ where: { stage_id: preStage.id, cost_id: select.cost_id } });
+        //                     for (const node of nodes) {
+        //                         for (const d of node.detail) {
+        //                             let id = insertDetails.find(x => { return x.source_id === d.source_cid && x.is_deal === d.is_deal; });
+        //                             if (!id) {
+        //                                 id = {
+        //                                     id: this.uuid.v4(), tender_id: stage.tid, stage_id: stage.id,
+        //                                     add_user_id: userId, update_user_id: userId,
+        //                                     ledger_id: select.id, cost_id: select.cost_id, source_id: d.source_cid, is_deal: d.is_deal,
+        //                                     d_order: insertDetails.length + 1,
+        //                                     code: d.code, name: d.name, party_b: d.party_b || '', tax: d.tax || 0,
+        //                                 };
+        //                                 insertDetails.push(id);
+        //                             }
+        //                             id.yf_excl_tax_tp = this.ctx.helper.add(id.yf_excl_tax_tp, d.yf_excl_tax_tp || 0);
+        //                             id.sf_excl_tax_tp = this.ctx.helper.add(id.sf_excl_tax_tp, d.sf_excl_tax_tp || 0);
+        //                             id.in_excl_tax_tp = this.ctx.helper.add(id.in_excl_tax_tp, d.in_excl_tax_tp || 0);
+        //                         }
+        //                     }
+        //                     for (const id of insertDetails) {
+        //                         const sourceDetail = preDetail.find(x => { return x.source_id === id.source_id && x.is_deal === id.is_deal; });
+        //                         for (const prop of costFields.selfCalcFields) {
+        //                             id[prop] = sourceDetail ? sourceDetail[prop] || 0 : 0;
+        //                         }
+        //                         this.ctx.service.calcTmpl.calcByTemplate(id, id, id, stage.calcTemplate.calc_expr);
+        //                         for (const prop of costFields.sumFields) {
+        //                             idata[prop] = this.ctx.helper.add(idata[prop], id[prop]);
+        //                         }
+        //                     }
+        //                     insertDetailData.push(...insertDetails);
+        //                 } else {
+        //                     for (const node of nodes) {
+        //                         idata.yf_excl_tax_tp = this.ctx.helper.add(idata.yf_excl_tax_tp, node.end_yf_excl_tax_tp || 0);
+        //                         idata.sf_excl_tax_tp = this.ctx.helper.add(idata.sf_excl_tax_tp, node.end_sf_excl_tax_tp || 0);
+        //                         idata.in_excl_tax_tp = this.ctx.helper.add(idata.in_excl_tax_tp, node.end_in_excl_tax_tp || 0);
+        //                     }
+        //                 }
+        //             }
+        //             this.ctx.service.calcTmpl.calcByTemplate(idata, idata, select, stage.calcTemplate.calc_expr);
+        //         }
+        //     }
+        //
+        //     await transaction.insert(this.tableName, insertData);
+        //     if (insertDetailData.length > 0) await transaction.insert(this.tableName, insertDetailData);
+        // }
         async initStageData(transaction, stage, preStage) {
             try {
                 if (preStage) {
@@ -429,7 +396,7 @@ module.exports = app => {
             const ids = id instanceof Array ? id : [id];
             if (ids.length === 0) return [];
 
-            const data = await this.db.getAllDataByCondition({ where: { id: ids } });
+            const data = await this.getAllDataByCondition({ where: { id: ids } });
             return data;
         }
 
@@ -478,7 +445,7 @@ module.exports = app => {
                         ? select[this.setting.fullPath].replace('-' + select[this.setting.kid], '-' + newData[this.setting.kid])
                         : newData[this.setting.kid] + '';
                     newData[this.setting.isLeaf] = true;
-                    newData.calc_type = calcType.in;
+                    newData.calc_type = select ? select.calc_type : calcType.in;
                     this._getDefaultData(newData, stage);
                     newDatas.push(newData);
                 }
@@ -504,6 +471,54 @@ module.exports = app => {
             }
         }
 
+        async addAnalysisChild(stage, targetId, count) {
+            if (!stage) return null;
+
+            const select = targetId ? await this.getDataByKid(stage.id, targetId) : null;
+            if (targetId && !select) throw '新增节点数据错误';
+            const children = await this.getChildrenByParentId(stage.id, select[this.setting.kid]);
+            const lastChild = children.length > 0 ? children[children.length - 1] : null;
+
+            this.transaction = await this.db.beginTransaction();
+            const newDatas = [];
+            try {
+                const maxId = await this._getMaxLid(stage.id);
+                for (let i = 1; i < count + 1; i++) {
+                    const newData = {};
+                    newData[this.setting.kid] = maxId + i;
+                    newData[this.setting.pid] = select[this.setting.kid];
+                    newData[this.setting.mid] = stage.id;
+                    newData[this.setting.level] = select[this.setting.level] + 1;
+                    newData[this.setting.order] = lastChild ? lastChild[this.setting.order] + i : i;
+                    newData[this.setting.fullPath] = select[this.setting.fullPath] + '-' + newData[this.setting.kid];
+                    newData[this.setting.isLeaf] = true;
+                    newData.calc_type = select.calc_type;
+                    this._getDefaultData(newData, stage);
+                    newDatas.push(newData);
+                }
+                const insertResult = await this.transaction.insert(this.tableName, newDatas);
+                this._cacheMaxLid(stage.id, maxId + count);
+
+                if (insertResult.affectedRows !== count) throw '新增节点数据错误';
+                if (!lastChild) {
+                    const updateParent = { id: select.id };
+                    updateParent[this.setting.isLeaf] = false;
+                    this.clearParentingData(updateParent);
+                    await this.transaction.update(this.tableName, updateParent);
+                }
+                await this.transaction.commit();
+                this.transaction = null;
+            } catch (err) {
+                await this.transaction.rollback();
+                this.transaction = null;
+                throw err;
+            }
+
+            const createData = await this.getDataByIds(newDatas.map(x => { return x.id; }));
+            const updateData = lastChild ? await this.getDataById(select.id) : [];
+            return {create: createData, update: updateData};
+        }
+
         async getProfitUpdateData(updateData) {
             const leafNode = await this.getAllDataByCondition({ where: { stage_id: this.ctx.costStage.id, tree_is_leaf: 1 } });
             const profit = leafNode.find(x => { return x.node_type === 3; });
@@ -612,6 +627,80 @@ module.exports = app => {
             }
             return { update: [payUpdate, ...profitUpdate] };
         }
+        async importCost(stage, select_id, cost_id) {
+            const userId = this.ctx.session.sessionUser.accountId;
+            const select = await this.getDataById(select_id);
+            if (!select) throw '选择的节点不存在,请刷新后重试';
+
+            const costStageLedger = await this.getCostStageData(stage.relaStage);
+            const nodes = costStageLedger.nodes.filter(x => { return cost_id.indexOf(x.cost_id) >= 0; });
+            if(nodes.length === 0 || nodes.length !== cost_id.length) throw '选择的成本数据不存在,请刷新后重试';
+
+            const orgDetailDatas = await this.ctx.service.costStageAnalysisDetail.getAllDataByCondition({ where: { ledger_id: select.id, stage_id: stage.id } });
+            const updateData = {
+                id: select.id, tree_id: select.tree_id, update_user_id: userId,
+                import_cost_id: cost_id.join(',')
+            };
+            const insertDetails = [];
+            if (nodes[0].detail.length > 0) {
+                const preStage = stage.stage_order > 1 ? await this.ctx.service.costStage.getStageByOrder(stage.tid, stage.stage_type, stage.stage_order -1) : null;
+                const preDetail = preStage ? await this.ctx.service.costStageAnalysisDetail.getAllDataByCondition({ where: { stage_id: preStage.id, cost_id: select.cost_id } }) : [];
+                for (const node of nodes) {
+                    for (const d of node.detail) {
+                        let id = insertDetails.find(x => { return x.source_id === d.source_cid && x.is_deal === d.is_deal; });
+                        if (!id) {
+                            id = {
+                                id: this.uuid.v4(), tender_id: stage.tid, stage_id: stage.id,
+                                add_user_id: userId, update_user_id: userId,
+                                ledger_id: select.id, cost_id: select.cost_id, source_id: d.source_cid, is_deal: d.is_deal,
+                                d_order: insertDetails.length + 1,
+                                code: d.code, name: d.name, party_b: d.party_b || '', tax: d.tax || 0,
+                            };
+                            insertDetails.push(id);
+                        }
+                        id.yf_excl_tax_tp = this.ctx.helper.add(id.yf_excl_tax_tp, d.yf_excl_tax_tp || 0);
+                        id.sf_excl_tax_tp = this.ctx.helper.add(id.sf_excl_tax_tp, d.sf_excl_tax_tp || 0);
+                        id.in_excl_tax_tp = this.ctx.helper.add(id.in_excl_tax_tp, d.in_excl_tax_tp || 0);
+                    }
+                }
+                for (const id of insertDetails) {
+                    let sourceDetail = orgDetailDatas.find(x => { return x.source_id === id.source_id && x.is_deal === id.is_deal; });
+                    if (!sourceDetail) sourceDetail = preDetail.find(x => { return x.source_id === id.source_id && x.is_deal === id.is_deal; });
+                    for (const prop of costFields.selfCalcFields) {
+                        id[prop] = sourceDetail ? sourceDetail[prop] || 0 : 0;
+                    }
+                    this.ctx.service.calcTmpl.calcByTemplate(id, id, id, stage.calcTemplate.calc_expr);
+                    for (const prop of costFields.sumFields) {
+                        updateData[prop] = this.ctx.helper.add(updateData[prop], id[prop]);
+                    }
+                }
+            } else {
+                for (const node of nodes) {
+                    updateData.yf_excl_tax_tp = this.ctx.helper.add(updateData.yf_excl_tax_tp, node.end_yf_excl_tax_tp || 0);
+                    updateData.sf_excl_tax_tp = this.ctx.helper.add(updateData.sf_excl_tax_tp, node.end_sf_excl_tax_tp || 0);
+                    updateData.in_excl_tax_tp = this.ctx.helper.add(updateData.in_excl_tax_tp, node.end_in_excl_tax_tp || 0);
+                }
+            }
+            this.ctx.service.calcTmpl.calcByTemplate(updateData, updateData, select, stage.calcTemplate.calc_expr);
+            const profitUpdate = await this.getProfitUpdateData([updateData]);
+
+            const conn = await this.db.beginTransaction();
+            try {
+                await conn.update(this.tableName, updateData);
+                await conn.updateRows(this.tableName, profitUpdate);
+                await conn.delete(this.ctx.service.costStageAnalysisDetail.tableName, { stage_id: stage.id, ledger_id: select.id });
+                if (insertDetails.length > 0) await conn.insert(this.ctx.service.costStageAnalysisDetail.tableName, insertDetails);
+                await conn.commit();
+            } catch(err) {
+                this.ctx.log(err);
+                await conn.rollback();
+                throw '导入成本数据错误';
+            }
+            return {
+                update: [updateData, ...profitUpdate],
+                detail: { add: insertDetails, del: orgDetailDatas.map(x => { return x.id; }) }
+            };
+        }
         async reCalcProfit(stageId) {
             if (!this.ctx.costStage) {
                 const costStage = await this.service.costStage.getStage(stageId);
@@ -643,7 +732,7 @@ module.exports = app => {
                     if (fi >= 0) his.splice(fi, 1);
                     const newHis = { ...auditInfo };
                     for (const f of costFields.sumFields) {
-                        newHis[f] = data[f];
+                        newHis[f] = l[f];
                     }
                     his.push(newHis);
                     data.calc_his = JSON.stringify(his);
@@ -653,39 +742,6 @@ module.exports = app => {
             if (updateData.length > 0) await transaction.updateRows(this.tableName, updateData);
         }
 
-        async setDecimal(decimal) {
-            if (!this.ctx.costStage) throw '读取数据错误';
-            if (this.ctx.costStage.stage_order !== this.ctx.costStage.latestOrder) throw '往期不可修改小数位数';
-            if (this.ctx.costStage.audit_status !== auditConst.status.uncheck && this.ctx.costStage.audit_status !== auditConst.status.checkNo) throw '仅原报可修改小数位数';
-            const orgDecimal = this.ctx.costStage.decimal;
-
-            const calcTp = decimal.tp < orgDecimal.tp;
-            this.ctx.costStage.decimal = { tp: decimal.tp, tax: decimal.tax || this.ctx.costStage.decimal.tax };
-
-            const updateData = [];
-            if (calcTp) {
-                const calcData = await this.getAllDataByCondition({ where: {stage_id: this.ctx.costStage.id, tree_is_leaf: 1 } });
-                for (const cd of calcData) {
-                    const nd = { id: cd.id, tree_id: cd.tree_id };
-                    for (const prop of costFields.calcFields) {
-                        nd[prop] = this.ctx.helper.round(nd[prop], decimal.tp);
-                    }
-                    updateData.push(nd);
-                }
-            }
-            const conn = await this.db.beginTransaction();
-            try {
-                await conn.update(this.ctx.service.costStage.tableName, { id: this.ctx.costStage.id, decimal: JSON.stringify(this.ctx.costStage.decimal)});
-                if (updateData.length > 0) await conn.updateRows(this.tableName, updateData);
-                await conn.commit();
-                return { calc: calcTp, update: updateData };
-            } catch(err) {
-                await conn.rollback();
-                return { calc: false, update: [] };
-            }
-
-        }
-
         async getSum(stage) {
             const sumCol = stage.calcTemplate.col_set.find(x => { return x.sum; });
             const field = sumCol.field;

+ 3 - 3
app/service/cost_stage_analysis_detail.js

@@ -20,9 +20,9 @@ const costFields = {
 };
 costFields.sumFields = [...costFields.calcFields, ...costFields.selfCalcFields];
 costFields.preCopyFields = [...costFields.selfTextFields, ...costFields.calcFields, ...costFields.selfCalcFields, ...costFields.taxFields];
-costFields.editQueryFields = [...costFields.baseFields, ...costFields.textFields, ...costFields.selfTextFields, ...costFields.calcFields, ...costFields.selfCalcFields, ...costFields.taxFields];
+costFields.editQueryFields = [...costFields.baseFields, ...costFields.textFields, ...costFields.selfTextFields, ...costFields.sumFields, ...costFields.taxFields];
 costFields.readQueryFields = [...costFields.baseFields, ...costFields.textFields, ...costFields.selfTextFields, ...costFields.readFields, ...costFields.taxFields];
-costFields.compareQueryFields = [...costFields.baseFields, ...costFields.textFields, ...costFields.selfTextFields, ...costFields.taxFields, ...costFields.hisFields];
+costFields.compareQueryFields = [...costFields.baseFields, ...costFields.textFields, ...costFields.selfTextFields, ...costFields.sumFields, ...costFields.taxFields, ...costFields.hisFields];
 
 module.exports = app => {
     class CostStageDetail extends app.BaseService {
@@ -147,7 +147,7 @@ module.exports = app => {
                     if (fi >= 0) his.splice(fi, 1);
                     const newHis = { ...auditInfo };
                     for (const f of costFields.sumFields) {
-                        newHis[f] = data[f];
+                        newHis[f] = l[f];
                     }
                     his.push(newHis);
                     data.calc_his = JSON.stringify(his);

+ 6 - 1
app/service/cost_stage_audit.js

@@ -213,7 +213,7 @@ module.exports = app => {
                     result.push(x);
                 }
             });
-            return result;
+            return latestGroup;
         }
         // ***** 查询审批人相关
 
@@ -471,6 +471,11 @@ module.exports = app => {
             await this.ctx.service[stage.typeInfo.dataService].auditCachePre(transaction, stage.id, {
                 user_id: audit.audit_id, audit_times: audit.audit_times, audit_order: audit.audit_order, active_order: audit.active_order,
             });
+            if (stage.typeInfo.detailService) {
+                await this.ctx.service[stage.typeInfo.detailService].auditCachePre(transaction, stage.id, {
+                    user_id: audit.audit_id, audit_times: audit.audit_times, audit_order: audit.audit_order, active_order: audit.active_order,
+                });
+            }
         }
         async start(stage) {
             const audits = await this.getAllDataByCondition({ where: { stage_id: stage.id, audit_times: stage.audit_times, audit_order: 1 } });

+ 73 - 2
app/service/cost_stage_book.js

@@ -19,6 +19,7 @@ costFields.calcFields = [...costFields.curFields];
 costFields.editQueryFields = [...costFields.baseFields, ...costFields.textFields, ...costFields.preFields, ...costFields.curFields];
 costFields.readQueryFields = [...costFields.baseFields, ...costFields.textFields, ...costFields.preFields, ...costFields.readFields];
 costFields.compareQueryFields = [...costFields.baseFields, ...costFields.textFields, ...costFields.preFields, ...costFields.curFields, 'calc_his'];
+const auditConst = require('../const/audit').costStage;
 
 module.exports = app => {
     class CostStageBook extends app.BaseService {
@@ -26,6 +27,7 @@ module.exports = app => {
         constructor(ctx) {
             super(ctx);
             this.tableName = 'cost_stage_book';
+            this.calcFields = costFields.calcFields;
         }
 
         async getEditData(stage) {
@@ -128,8 +130,9 @@ module.exports = app => {
                 if (d.in_tp !== undefined ) {
                     nd.in_tp = d.in_tp !== undefined ? this.ctx.helper.round(d.in_tp || 0, this.ctx.costStage.decimal.tp) : (od.in_tp ? od.in_tp : 0);
                     const divNum = this.ctx.helper.add(1, this.ctx.helper.div(cl.tax, 100));
-                    nd.in_excl_tax_tp = this.ctx.helper.div(nd.in_tp, divNum, this.ctx.costStage.decimal.tp);
+                    nd.in_excl_tax_tp = this.ctx.helper.div(nd.in_tp, divNum, this.ctx.costStage.decimal.excl_tax_tp);
                 }
+                if (d.in_excl_tax_tp !== undefined) nd.in_excl_tax_tp = this.ctx.helper.round(d.in_excl_tax_tp, this.ctx.costStage.decimal.excl_tax_tp);
                 if (od) {
                     updateDetail.push(nd);
                 } else {
@@ -181,7 +184,7 @@ module.exports = app => {
                     if (fi >= 0) his.splice(fi, 1);
                     const newHis = { ...auditInfo };
                     for (const f of costFields.curFields) {
-                        newHis[f] = data[f];
+                        newHis[f] = l[f];
                     }
                     his.push(newHis);
                     data.calc_his = JSON.stringify(his);
@@ -196,6 +199,74 @@ module.exports = app => {
             const result = await this.db.queryOne(`SELECT ${sumFields.join(', ')} FROM ${this.tableName} where stage_id = ?`, [stage.id]);
             return result;
         }
+
+        async setDecimal(decimal) {
+            if (!this.ctx.costStage) throw '读取数据错误';
+            if (this.ctx.costStage.stage_order !== this.ctx.costStage.latestOrder) throw '往期不可修改小数位数';
+            if (this.ctx.costStage.audit_status !== auditConst.status.uncheck && this.ctx.costStage.audit_status !== auditConst.status.checkNo) throw '仅原报可修改小数位数';
+            const orgDecimal = this.ctx.costStage.decimal;
+
+            const calcTp = decimal.tp < orgDecimal.tp || decimal.excl_tax_tp !== orgDecimal.excl_tax_tp;
+            this.ctx.costStage.decimal = this.ctx.helper._.assignIn(this.ctx.costStage.typeInfo.decimal, decimal);
+
+            const updateData = [], detailUpdateData = [];
+            let orgLedger, orgDetail;
+            if (calcTp) {
+                const detailCalcData = await this.ctx.service.costStageBookDetail.getAllDataByCondition({ where: { stage_id: this.ctx.costStage.id }});
+                orgDetail = await this.ctx.service.costStageDetail.getAllDataByCondition({ where: { stage_id: this.ctx.costStage.relaStage.id }});
+                for (const cd of detailCalcData) {
+                    const nd = { id: cd.id, ledger_id: cd.ledger_id, detail_id: cd.detail_id };
+                    const od = orgDetail.find(x => { return x.id === cd.detail_id });
+                    nd.in_tp = this.ctx.helper.round(cd.in_tp, decimal.tp);
+                    const divNum = od.tax ? this.ctx.helper.add(1, this.ctx.helper.div(od.tax, 100)) : 1;
+                    nd.in_excl_tax_tp = this.ctx.helper.div(nd.in_tp !== undefined ? nd.in_tp : nd.in_tp, divNum, decimal.excl_tax_tp);
+                    detailUpdateData.push(nd);
+                }
+                const calcData = await this.getAllDataByCondition({ where: {stage_id: this.ctx.costStage.id } });
+                orgLedger = await this.ctx.service.costStageLedger.getAllDataByCondition({ where: { stage_id: this.ctx.costStage.relaStage.id }});
+                for (const cd of calcData) {
+                    const nd = { id: cd.id, ledger_id: cd.ledger_id };
+                    const relaDetail = detailUpdateData.filter(x => { return x.ledger_id === cd.ledger_id; });
+                    if (relaDetail && relaDetail.length > 0) {
+                        for (const rd of relaDetail) {
+                            for (const prop of costFields.calcFields) {
+                                nd[prop] = this.ctx.helper.add(nd[prop], rd[prop]);
+                            }
+                        }
+                    } else {
+                        const cl = orgLedger.find(x => { return x.id === cd.ledger_id });
+                        nd.in_tp = this.ctx.helper.round(cd.in_tp, decimal.tp);
+                        const divNum = cl.tax ? this.ctx.helper.add(1, this.ctx.helper.div(cl.tax, 100)) : 1;
+                        nd.in_excl_tax_tp = this.ctx.helper.div(nd.in_tp !== undefined ? nd.in_tp : nd.in_tp, divNum, decimal.excl_tax_tp);
+                    }
+                    updateData.push(nd);
+                }
+            }
+
+            const conn = await this.db.beginTransaction();
+            try {
+                await conn.update(this.ctx.service.costStage.tableName, { id: this.ctx.costStage.id, decimal: JSON.stringify(this.ctx.costStage.decimal)});
+                if (updateData.length > 0) await conn.updateRows(this.tableName, updateData);
+                if (detailUpdateData.length > 0) await conn.updateRows(this.ctx.service.costStageBookDetail.tableName, detailUpdateData);
+                await conn.commit();
+            } catch(err) {
+                await conn.rollback();
+                return { calc: false, bills: [], detail: [] };
+            }
+            updateData.forEach(i => {
+                i.book_id = i.id;
+                delete i.id;
+                const cl = orgLedger.find(x => { return x.id === i.ledger_id });
+                i.tree_id = cl.tree_id;
+            });
+            detailUpdateData.forEach(u => {
+                u.book_id = u.id;
+                delete u.id;
+                const cl = orgDetail.find(x => { return x.id === u.detail_id; });
+                u.id = cl.id;
+            });
+            return { calc: calcTp, bills: updateData, detail: detailUpdateData };
+        }
     }
 
     return CostStageBook;

+ 9 - 3
app/service/cost_stage_book_detail.js

@@ -120,7 +120,12 @@ module.exports = app => {
                     calc = true;
                     nd.in_tp = d.in_tp !== undefined ? this.ctx.helper.round(d.in_tp || 0, this.ctx.costStage.decimal.tp) : (od.in_tp ? od.in_tp : 0);
                     const divNum = this.ctx.helper.add(1, this.ctx.helper.div(cd.tax, 100));
-                    nd.in_excl_tax_tp = this.ctx.helper.div(nd.in_tp, divNum, this.ctx.costStage.decimal.tp);
+                    nd.in_excl_tax_tp = this.ctx.helper.div(nd.in_tp, divNum, this.ctx.costStage.decimal.excl_tax_tp);
+                }
+                if (d.in_excl_tax_tp !== undefined) {
+                    if (d.in_tp === undefined) nd.in_tp = od ? od.in_tp || 0 : 0;
+                    calc = true;
+                    nd.in_excl_tax_tp = this.ctx.helper.round(d.in_excl_tax_tp, this.ctx.costStage.decimal.excl_tax_tp);
                 }
                 if (od) {
                     updateDetail.push(nd);
@@ -129,7 +134,8 @@ module.exports = app => {
                 }
             }
             const bookUpdate = calc ? await this._getBookUpdateData([...insertDetail, ...updateDetail], orgDetail[0].ledger_id) : null;
-            const orgBook = await this.ctx.service.costStageBook.getDataByCondition({ ledger_id: orgDetail[0].ledger_id });
+            console.log(bookUpdate);
+            const orgBook = await this.ctx.service.costStageBook.getDataByCondition({ stage_id: this.ctx.costStage.id, ledger_id: orgDetail[0].ledger_id });
             const orgLedger = await this.ctx.service.costStageLedger.getDataById(orgDetail[0].ledger_id);
             if (bookUpdate) {
                 if (!orgBook) {
@@ -199,7 +205,7 @@ module.exports = app => {
                     if (fi >= 0) his.splice(fi, 1);
                     const newHis = { ...auditInfo };
                     for (const f of costFields.curFields) {
-                        newHis[f] = data[f];
+                        newHis[f] = l[f];
                     }
                     his.push(newHis);
                     data.calc_his = JSON.stringify(his);

+ 13 - 9
app/service/cost_stage_detail.js

@@ -160,8 +160,8 @@ module.exports = app => {
                 if (d.tax !== undefined || nd.sf_tp !== undefined || nd.yf_tp !== undefined) {
                     nd.tax = d.tax !== undefined ? this.ctx.helper.round(d.tax || 0, this.ctx.costStage.decimal.tax) : 0;
                     const divNum = nd.tax ? this.ctx.helper.add(1, this.ctx.helper.div(nd.tax, 100)) : 1;
-                    nd.yf_excl_tax_tp = this.ctx.helper.div(nd.yf_tp !== undefined ? nd.yf_tp : 0, divNum, this.ctx.costStage.decimal.tp);
-                    nd.sf_excl_tax_tp = this.ctx.helper.div(nd.sf_tp !== undefined ? nd.sf_tp : 0, divNum, this.ctx.costStage.decimal.tp);
+                    nd.yf_excl_tax_tp = this.ctx.helper.div(nd.yf_tp !== undefined ? nd.yf_tp : 0, divNum, this.ctx.costStage.decimal.excl_tax_tp);
+                    nd.sf_excl_tax_tp = this.ctx.helper.div(nd.sf_tp !== undefined ? nd.sf_tp : 0, divNum, this.ctx.costStage.decimal.excl_tax_tp);
                 }
                 insertData.push(nd);
             }
@@ -207,7 +207,7 @@ module.exports = app => {
                 await conn.delete(this.tableName, { id: data });
                 if (updateData.length > 0) await conn.updateRows(this.tableName, updateData);
                 await conn.delete(this.ctx.service.costStageTag.tableName, { rela_sub_id: data });
-                await conn.delete(this.ctx.service.costStageFile.tableName, { rela_sub_id: data });
+                await conn.update(this.ctx.service.costStageFile.tableName, { is_deleted: 1 }, { where: { rela_sub_id: data } });
                 await conn.update(this.ctx.service.costStageLedger.tableName, billsUpdate);
                 await conn.commit();
                 return [data, updateData, billsUpdate];
@@ -250,8 +250,8 @@ module.exports = app => {
                 if (d.tax !== undefined || nd.sf_tp !== undefined || nd.yf_tp !== undefined) {
                     nd.tax = d.tax !== undefined ? this.ctx.helper.round(d.tax || 0, this.ctx.costStage.decimal.tax) : od.tax;
                     const divNum = nd.tax ? this.ctx.helper.add(1, this.ctx.helper.div(nd.tax, 100)) : 1;
-                    nd.yf_excl_tax_tp = this.ctx.helper.div(nd.yf_tp !== undefined ? nd.yf_tp : od.yf_tp, divNum, this.ctx.costStage.decimal.tp);
-                    nd.sf_excl_tax_tp = this.ctx.helper.div(nd.sf_tp !== undefined ? nd.sf_tp : od.sf_tp, divNum, this.ctx.costStage.decimal.tp);
+                    nd.yf_excl_tax_tp = this.ctx.helper.div(nd.yf_tp !== undefined ? nd.yf_tp : od.yf_tp, divNum, this.ctx.costStage.decimal.excl_tax_tp);
+                    nd.sf_excl_tax_tp = this.ctx.helper.div(nd.sf_tp !== undefined ? nd.sf_tp : od.sf_tp, divNum, this.ctx.costStage.decimal.excl_tax_tp);
                 }
                 uDatas.push(nd);
             }
@@ -327,8 +327,8 @@ module.exports = app => {
                 idetail.yf_tp = this.ctx.helper.add(idetail.yf_tp, pay.yf_price);
                 idetail.sf_tp = this.ctx.helper.add(idetail.sf_tp, pay.sf_price);
                 const divNum = idetail.tax ? this.ctx.helper.add(1, this.ctx.helper.div(idetail.tax, 100)) : 1;
-                idetail.yf_excl_tax_tp = this.ctx.helper.add(idetail.yf_excl_tax_tp, this.ctx.helper.div(pay.yf_price, divNum, this.ctx.costStage.decimal.tp));
-                idetail.sf_excl_tax_tp = this.ctx.helper.add(idetail.sf_excl_tax_tp, this.ctx.helper.div(pay.sf_price, divNum, this.ctx.costStage.decimal.tp));
+                idetail.yf_excl_tax_tp = this.ctx.helper.add(idetail.yf_excl_tax_tp, this.ctx.helper.div(pay.yf_price, divNum, this.ctx.costStage.decimal.excl_tax_tp));
+                idetail.sf_excl_tax_tp = this.ctx.helper.add(idetail.sf_excl_tax_tp, this.ctx.helper.div(pay.sf_price, divNum, this.ctx.costStage.decimal.excl_tax_tp));
             }
             const detailDatas = await this.getAllDataByCondition({ columns: ['id'], where: { ledger_id: ledgerId, stage_id: this.ctx.costStage.id } });
             const billsUpdate = await this._getLedgerUpdateData(insertDetails, ledgerId, []);
@@ -337,6 +337,10 @@ module.exports = app => {
             const conn = await this.db.beginTransaction();
             try {
                 await conn.delete(this.tableName, { stage_id: this.ctx.costStage.id, ledger_id: ledgerId });
+                if (detailDatas.length > 0) {
+                    await conn.update(this.ctx.service.costStageFile.tableName, { is_deleted: 1 }, { where: { rela_sub_id: detailDatas.map(x => { return x.id; }) } });
+                    await conn.delete(this.ctx.service.costStageTag.tableName, { rela_sub_id: detailDatas.map(x => { return x.id; }) });
+                }
                 if (insertDetails.length > 0) await conn.insert(this.tableName, insertDetails);
                 await conn.update(this.ctx.service.costStageLedger.tableName, billsUpdate);
                 await conn.commit();
@@ -370,7 +374,7 @@ module.exports = app => {
                 const diff = costFields.curFields.find(x => {
                     return l[x] !== l['read_' + x];
                 });
-                if (diff) {
+                if (diff || !l.calc_his) {
                     const data = { id: l.id };
                     // cache read
                     for (const f of costFields.curFields) {
@@ -382,7 +386,7 @@ module.exports = app => {
                     if (fi >= 0) his.splice(fi, 1);
                     const newHis = { ...auditInfo };
                     for (const f of costFields.curFields) {
-                        newHis[f] = data[f];
+                        newHis[f] = l[f];
                     }
                     his.push(newHis);
                     data.calc_his = JSON.stringify(his);

+ 403 - 0
app/service/cost_stage_duty.js

@@ -0,0 +1,403 @@
+'use strict';
+
+/**
+ *
+ * 支付审批-安全生产
+ * @author Mai
+ * @date
+ * @version
+ */
+const billsUtils = require('../lib/bills_utils');
+const costFields = {
+    textFields: ['code', 'b_code', 'name', 'unit', 'memo', 'drawing_code'],
+    curFields: ['quantity', 'unit_price', 'total_price'],
+    readFields: ['read_quantity', 'read_unit_price', 'read_total_price'],
+    treeFields: ['tree_id', 'tree_pid', 'tree_level', 'tree_order', 'tree_full_path', 'tree_is_leaf'],
+    baseFields: ['id', 'cost_id', 'tender_id', 'stage_id',],
+};
+costFields.calcFields = [...costFields.curFields];
+costFields.editQueryFields = [...costFields.baseFields, ...costFields.treeFields, ...costFields.textFields, ...costFields.curFields];
+costFields.readQueryFields = [...costFields.baseFields, ...costFields.treeFields, ...costFields.textFields, ...costFields.readFields];
+costFields.compareQueryFields = [...costFields.baseFields, ...costFields.treeFields, ...costFields.textFields, ...costFields.curFields, 'calc_his'];
+
+const auditConst = require('../const/audit').costStage;
+
+module.exports = app => {
+
+    class CostStageLedger extends app.BaseTreeService {
+
+        /**
+         * 构造函数
+         *
+         * @param {Object} ctx - egg全局变量
+         * @return {void}
+         */
+        constructor(ctx) {
+            super(ctx, {
+                mid: 'stage_id',
+                kid: 'tree_id',
+                pid: 'tree_pid',
+                order: 'tree_order',
+                level: 'tree_level',
+                isLeaf: 'tree_is_leaf',
+                fullPath: 'tree_full_path',
+                keyPre: 'cost_duty_maxTreeId:',
+                uuid: true,
+            });
+            this.tableName = 'cost_stage_duty';
+            this.calcFields = costFields.calcFields;
+        }
+
+        // 继承方法
+        clearParentingData(data) {
+            for (const f of costFields.calcFields) {
+                data[f] = 0;
+            }
+        }
+
+        _getDefaultData(data, stage) {
+            data.id = this.uuid.v4();
+            data.cost_id = data.id;
+            data.tender_id = stage.tid;
+            data.stage_id = stage.id;
+            data.add_user_id = this.ctx.session.sessionUser.accountId;
+            data.update_user_id = data.add_user_id;
+        }
+
+        async getEditData(stage) {
+            const result = await this.getAllDataByCondition({
+                where: { stage_id: stage.id },
+                columns: costFields.editQueryFields
+            });
+            return result;
+        }
+        async getReadData(stage) {
+            const result = await this.getAllDataByCondition({
+                where: { stage_id: stage.id },
+                columns: costFields.readQueryFields
+            });
+            result.forEach(x => {
+                for(const prop of costFields.curFields) {
+                    x[prop] = x['read_' + prop];
+                }
+            });
+            return result;
+        }
+        async getCompareData(stage) {
+            const result = await this.getAllDataByCondition({
+                where: { stage_id: stage.id },
+                columns: costFields.compareQueryFields
+            });
+            result.forEach(x => {
+                x.calc_his = x.calc_his ? JSON.parse(x.calc_his) : [];
+            });
+            return result;
+        }
+
+        async init(stage, transaction) {
+            if (!stage || !transaction) throw '成本报审数据错误';
+
+            if (!this.ctx.tender.data.measure_type) throw '未设置台账模板,请联系管理员设置';
+            const templateId = await this.ctx.service.valuation.getValuationTemplate(
+                this.ctx.tender.data.valuation, this.ctx.tender.data.measure_type);
+            const tempData = await this.ctx.service.tenderNodeTemplate.getData(templateId, true);
+            if (tempData.length === 0) throw '台账模板无数据,连请联系管理员修改';
+
+            const insertData = [];
+            for (const b of tempData) {
+                const bills = {
+                    tree_id: b.template_id, tree_pid: b.pid, tree_order: b.order, tree_level: b.level, tree_full_path: b.full_path, tree_is_leaf: b.is_leaf,
+                    code: b.code || '', b_code: '', name: b.name || '', unit: b.unit || '',
+                };
+                this._getDefaultData(bills, stage);
+                insertData.push(bills);
+            }
+
+            const operate = await transaction.insert(this.tableName, insertData);
+            return operate.affectedRows === insertData.length;
+        }
+        async initStageData(transaction, stage, preStage) {
+            await this.init(stage, transaction);
+        }
+
+        /**
+         * 新增数据(供内部或其他service类调用, controller不可直接使用)
+         * @param {Array|Object} data - 新增数据
+         * @param {Number} stageId - 期id
+         * @param {Object} transaction - 新增事务
+         * @return {Promise<boolean>} - {Promise<是否正确新增成功>}
+         */
+        async innerAdd(data, stageId, transaction) {
+            const datas = data instanceof Array ? data : [data];
+            if (!stageId) {
+                throw '期id错误';
+            }
+            if (datas.length <= 0) {
+                throw '插入数据为空';
+            }
+            if (!transaction) {
+                throw '内部错误';
+            }
+            // 整理数据
+            const insertData = [];
+            for (const tmp of datas) {
+                tmp[this.setting.id] = tmp.template_id;
+                tmp[this.setting.pid] = tmp.pid;
+                tmp[this.setting.mid] = stageId;
+                delete tmp.template_id;
+                delete tmp.pid;
+                tmp.id = this.uuid.v4();
+                insertData.push(tmp);
+            }
+            const operate = await transaction.insert(this.tableName, insertData);
+            return operate.affectedRows === datas.length;
+        }
+        /**
+         * 新增数据
+         *
+         * @param {Object} data - 新增的数据(可批量)
+         * @param {Number} stageId - 安全计量期id
+         * @return {Boolean} - 返回新增的结果
+         */
+        async add(data, stageId) {
+            this.transaction = await this.db.beginTransaction();
+            let result = false;
+            try {
+                result = await this.innerAdd(data, stageId, this.transaction);
+                if (!result) {
+                    throw '新增数据错误';
+                }
+                await this.transaction.commit();
+            } catch (error) {
+                await this.transaction.rollback();
+                result = false;
+            }
+
+            return result;
+        }
+
+        /**
+         * 根据节点Id获取数据
+         *
+         * @param {Number} stageId - 安全计量期id
+         * @param {Number} nodeId - 项目节/工程量清单节点id
+         * @return {Object} - 返回查询到的节点数据
+         */
+        async getDataByNodeId(stageId, nodeId) {
+            if ((nodeId <= 0) || (stageId <= 0)) {
+                return undefined;
+            }
+            const where = {};
+            where[this.setting.mid] = stageId;
+            where[this.setting.id] = nodeId;
+            const data = await this.db.getDataByCondition(where);
+
+            return data;
+        }
+        /**
+         * 根据节点Id获取数据
+         * @param {Number} stageId - 期Id
+         * @param {Array} nodesIds - 节点Id
+         * @return {Array}
+         */
+        async getDataByNodeIds(stageId, nodesIds) {
+            if (stageId <= 0) {
+                return [];
+            }
+
+            const where = {};
+            where[this.setting.mid] = stageId;
+            where[this.setting.id] = nodesIds;
+            const data = await this.db.getAllDataByCondition({ where });
+
+            return this._.sortBy(data, function(d) {
+                return nodesIds.indexOf(d[this.setting.id]);
+            });
+        }
+        /**
+         * 根据主键id获取数据
+         * @param {Array|Number} id - 主键id
+         * @return {Promise<*>}
+         */
+        async getDataByIds(id) {
+            if (!id) return [];
+            const ids = id instanceof Array ? id : [id];
+            if (ids.length === 0) return [];
+
+            const data = await this.db.getAllDataByCondition({ where: { id: ids } });
+            return data;
+        }
+
+        /**
+         * 根据 父节点id 获取子节点
+         * @param stageId
+         * @param nodeId
+         * @return {Promise<*>}
+         */
+        async getChildrenByParentId(stageId, nodeId) {
+            if (stageId <= 0 || !nodeId) {
+                return undefined;
+            }
+            const nodeIds = nodeId instanceof Array ? nodeId : [nodeId];
+            if (nodeIds.length === 0) {
+                return [];
+            }
+
+            const where = {};
+            where[this.setting.mid] = stageId;
+            where[this.setting.pid] = nodeIds;
+            const data = await this.getAllDataByCondition({ where, orders: [[this.setting.order, 'ASC']] });
+
+            return data;
+        }
+
+        async addDutyNode(stage, targetId, count) {
+            if (!stage) return null;
+
+            const select = targetId ? await this.getDataByKid(stage.id, targetId) : null;
+            if (targetId && !select) throw '新增节点数据错误';
+
+            this.transaction = await this.db.beginTransaction();
+            try {
+                if (select) await this._updateChildrenOrder(stage.id, select[this.setting.pid], select[this.setting.order] + 1, count);
+                const newDatas = [];
+                const maxId = await this._getMaxLid(stage.id);
+                for (let i = 1; i < count + 1; i++) {
+                    const newData = {};
+                    newData[this.setting.kid] = maxId + i;
+                    newData[this.setting.pid] = select ? select[this.setting.pid] : this.rootId;
+                    newData[this.setting.mid] = stage.id;
+                    newData[this.setting.level] = select ? select[this.setting.level] : 1;
+                    newData[this.setting.order] = select ? select[this.setting.order] + i : i;
+                    newData[this.setting.fullPath] = newData[this.setting.level] > 1
+                        ? select[this.setting.fullPath].replace('-' + select[this.setting.kid], '-' + newData[this.setting.kid])
+                        : newData[this.setting.kid] + '';
+                    newData[this.setting.isLeaf] = true;
+                    this._getDefaultData(newData, stage);
+                    newDatas.push(newData);
+                }
+                const insertResult = await this.transaction.insert(this.tableName, newDatas);
+                this._cacheMaxLid(stage.id, maxId + count);
+
+                if (insertResult.affectedRows !== count) throw '新增节点数据错误';
+                await this.transaction.commit();
+                this.transaction = null;
+            } catch (err) {
+                await this.transaction.rollback();
+                this.transaction = null;
+                throw err;
+            }
+
+            if (select) {
+                const createData = await this.getChildBetween(stage.id, select[this.setting.pid], select[this.setting.order], select[this.setting.order] + count + 1);
+                const updateData = await this.getNextsData(stage.id, select[this.setting.pid], select[this.setting.order] + count);
+                return {create: createData, update: updateData};
+            } else {
+                const createData = await this.getChildBetween(stage.id, -1, 0, count + 1);
+                return {create: createData};
+            }
+        }
+
+        async updateCalc(stage, data) {
+            const helper = this.ctx.helper;
+            // 简单验证数据
+            if (!stage) throw '成本报审不存在';
+            const decimal = stage.decimal;
+            if (!data) throw '提交数据错误';
+            const datas = data instanceof Array ? data : [data];
+            const ids = datas.map(x => { return x.id; });
+            const orgData = await this.getAllDataByCondition({ where: { id: ids }});
+            const updateData = [];
+            for (const row of datas) {
+                const oData = orgData.find(x => { return x.id === row.id });
+                if (!oData || oData.stage_id !== stage.id || oData.tree_id !== row.tree_id) throw '提交数据错误';
+
+                let nData = { id: oData.id, tree_id: oData.tree_id, update_user_id: this.ctx.session.sessionUser.accountId };
+
+                // calc
+                if (row.quantity !== undefined || row.unit_price !== undefined) {
+                    nData.quantity = row.quantity !== undefined ? helper.round(row.quantity || 0, decimal.qty) : oData.quantity || 0;
+                    nData.unit_price = row.unit_price !== undefined ? helper.round(row.unit_price || 0, decimal.up) : oData.unit_price || 0;
+                    nData.total_price = helper.mul(nData.quantity, nData.unit_price, decimal.tp);
+                }
+                for (const field of costFields.textFields) {
+                    if (row[field] !== undefined) nData[field] = row[field] || '';
+                }
+                updateData.push(nData);
+            }
+
+            await this.db.updateRows(this.tableName, updateData);
+            return { update: updateData };
+        }
+
+        async auditCache(transaction, stageId, auditInfo) {
+            const leaf = await this.getAllDataByCondition({ where: { stage_id: stageId, tree_is_leaf: true} });
+            const updateData = [];
+            for (const l of leaf) {
+                const diff = costFields.curFields.find(x => {
+                    return l[x] !== l['read_' + x];
+                });
+                if (diff || !l.calc_his) {
+                    const data = { id: l.id };
+                    // cache read
+                    for (const f of costFields.curFields) {
+                        data['read_' + f] = l[f];
+                    }
+                    // cache his
+                    const his = l.calc_his ? JSON.parse(l.calc_his) : [];
+                    const fi = his.find(x => { return x.audit_times === auditInfo.audit_times && x.active_order === auditInfo.active_order; });
+                    if (fi >= 0) his.splice(fi, 1);
+                    const newHis = { ...auditInfo };
+                    for (const f of costFields.curFields) {
+                        newHis[f] = l[f];
+                    }
+                    his.push(newHis);
+                    data.calc_his = JSON.stringify(his);
+                    updateData.push(data);
+                }
+            }
+            if (updateData.length > 0) await transaction.updateRows(this.tableName, updateData);
+        }
+
+        async setDecimal(decimal) {
+            if (!this.ctx.costStage) throw '读取数据错误';
+            if (this.ctx.costStage.stage_order !== this.ctx.costStage.latestOrder) throw '往期不可修改小数位数';
+            if (this.ctx.costStage.audit_status !== auditConst.status.uncheck && this.ctx.costStage.audit_status !== auditConst.status.checkNo) throw '仅原报可修改小数位数';
+            const orgDecimal = this.ctx.costStage.decimal;
+
+            const calc = decimal.tp < orgDecimal.tp || decimal.qty !== orgDecimal.qty || decimal.up !== orgDecimal.up;
+            this.ctx.costStage.decimal = this.ctx.helper._.assignIn(this.ctx.costStage.typeInfo.decimal, decimal);
+
+            const updateData = [];
+            if (calc) {
+                const calcData = await this.getAllDataByCondition({ where: {stage_id: this.ctx.costStage.id, tree_is_leaf: 1 } });
+                for (const cd of calcData) {
+                    const nd = { id: cd.id, tree_id: cd.tree_id };
+                    nd.quantity = this.ctx.helper.round(cd.quantity, decimal.qty);
+                    nd.unit_price = this.ctx.helper.round(cd.unit_price, decimal.up);
+                    nd.total_price = this.ctx.helper.mul(cd.quantity, cd.unit_price, decimal.tp);
+                    updateData.push(nd);
+                }
+            }
+
+            const conn = await this.db.beginTransaction();
+            try {
+                await conn.update(this.ctx.service.costStage.tableName, { id: this.ctx.costStage.id, decimal: JSON.stringify(this.ctx.costStage.decimal)});
+                if (updateData.length > 0) await conn.updateRows(this.tableName, updateData);
+                await conn.commit();
+                return { calc, bills: updateData };
+            } catch(err) {
+                await conn.rollback();
+                return { calc: false, bills: [] };
+            }
+        }
+
+        async getSum(stage) {
+            const sumFields = costFields.curFields.map(f => { return `SUM(${f}) AS ${f}`});
+            const result = await this.db.queryOne(`SELECT ${sumFields.join(', ')} FROM ${this.tableName} where stage_id = ?`, [stage.id]);
+            return result;
+        }
+    }
+
+    return CostStageLedger;
+};

+ 59 - 13
app/service/cost_stage_ledger.js

@@ -48,6 +48,7 @@ module.exports = app => {
             });
             // this.depart = ctx.app.config.table_depart.light;
             this.tableName = 'cost_stage_ledger';
+            this.calcFields = costFields.calcFields;
         }
 
         // 继承方法
@@ -161,6 +162,18 @@ module.exports = app => {
         }
 
         /**
+         * 删除相关数据 继承
+         * @param mid
+         * @param deleteData
+         * @return {Promise<void>}
+         * @private
+         */
+        async _deleteRelaData(mid, deleteData) {
+            await this.ctx.service.costStageDetail.deletePartData(this.transaction, mid, this._.map(deleteData, 'id'));
+            await this.transaction.update(this.ctx.service.costStageFile.tableName, { is_deleted: 1 }, { where: { rela_id: this._.map(deleteData, 'id') } });
+            await this.transaction.delete(this.ctx.service.costStageTag.tableName, { rela_id: this._.map(deleteData, 'id') });
+        }
+        /**
          * 新增数据(供内部或其他service类调用, controller不可直接使用)
          * @param {Array|Object} data - 新增数据
          * @param {Number} stageId - 期id
@@ -336,7 +349,11 @@ module.exports = app => {
                         c[setting.pid] = newBills[setting.id];
                     }
                     for (const prop of costFields.curFields) {
-                        newBills[prop] = this.ctx.helper.round(d[prop] || 0, tpDecimal.tp);
+                        if (prop.indexOf('_excl_tax_') > 0) {
+                            newBills[prop] = this.ctx.helper.round(d[prop] || 0, tpDecimal.excl_tax_tp);
+                        } else {
+                            newBills[prop] = this.ctx.helper.round(d[prop] || 0, tpDecimal.tp);
+                        }
                         newBills['end_' + prop] = newBills[prop];
                     }
                     pbd.push(newBills);
@@ -448,8 +465,8 @@ module.exports = app => {
                 if (row.tax !== undefined || nData.sf_tp !== undefined || nData.yf_tp !== undefined) {
                     nData.tax = row.tax !== undefined ? helper.round(row.tax || 0, decimal.tax) : oData.tax;
                     const divNum = nData.tax ? helper.add(1, helper.div(nData.tax, 100)) : 1;
-                    nData.yf_excl_tax_tp = helper.div(nData.yf_tp !== undefined ? nData.yf_tp : oData.yf_tp, divNum, decimal.tp);
-                    nData.sf_excl_tax_tp = helper.div(nData.sf_tp !== undefined ? nData.sf_tp : oData.sf_tp, divNum, decimal.tp);
+                    nData.yf_excl_tax_tp = helper.div(nData.yf_tp !== undefined ? nData.yf_tp : oData.yf_tp, divNum, decimal.excl_tax_tp);
+                    nData.sf_excl_tax_tp = helper.div(nData.sf_tp !== undefined ? nData.sf_tp : oData.sf_tp, divNum, decimal.excl_tax_tp);
                 }
                 for (const field of costFields.textFields) {
                     if (row[field] !== undefined) nData[field] = row[field] || '';
@@ -468,7 +485,7 @@ module.exports = app => {
                 const diff = costFields.curFields.find(x => {
                     return l[x] !== l['read_' + x];
                 });
-                if (diff) {
+                if (diff || !l.calc_his) {
                     const data = { id: l.id };
                     // cache read
                     for (const f of costFields.curFields) {
@@ -480,7 +497,7 @@ module.exports = app => {
                     if (fi >= 0) his.splice(fi, 1);
                     const newHis = { ...auditInfo };
                     for (const f of costFields.curFields) {
-                        newHis[f] = data[f];
+                        newHis[f] = l[f];
                     }
                     his.push(newHis);
                     data.calc_his = JSON.stringify(his);
@@ -496,31 +513,60 @@ module.exports = app => {
             if (this.ctx.costStage.audit_status !== auditConst.status.uncheck && this.ctx.costStage.audit_status !== auditConst.status.checkNo) throw '仅原报可修改小数位数';
             const orgDecimal = this.ctx.costStage.decimal;
 
-            const calcTp = decimal.tp < orgDecimal.tp;
-            this.ctx.costStage.decimal = { up: decimal.up, tp: decimal.tp, qty: decimal.qty };
+            const calcTp = decimal.tp < orgDecimal.tp || decimal.excl_tax_tp !== orgDecimal.excl_tax_tp;
+            this.ctx.costStage.decimal = this.ctx.helper._.assignIn(this.ctx.costStage.typeInfo.decimal, decimal);
 
-            const updateData = [];
+            const updateData = [], detailUpdateData = [];
             if (calcTp) {
+                const detailCalcData = await this.ctx.service.costStageDetail.getAllDataByCondition({ where: { stage_id: this.ctx.costStage.id }});
+                for (const cd of detailCalcData) {
+                    const nd = { id: cd.id, ledger_id: cd.ledger_id };
+                    nd.tax = this.ctx.helper.round(cd.tax, decimal.tax);
+                    nd.pay_tp = this.ctx.helper.round(cd.pay_tp, decimal.tp);
+                    nd.cut_tp = this.ctx.helper.round(cd.cut_tp, decimal.tp);
+                    nd.yf_tp = this.ctx.helper.round(cd.yf_tp, decimal.tp);
+                    nd.sf_tp = this.ctx.helper.round(cd.sf_tp, decimal.tp);
+                    const divNum = nd.tax ? this.ctx.helper.add(1, this.ctx.helper.div(nd.tax, 100)) : 1;
+                    nd.yf_excl_tax_tp = this.ctx.helper.div(nd.yf_tp !== undefined ? nd.yf_tp : nd.yf_tp, divNum, decimal.excl_tax_tp);
+                    nd.sf_excl_tax_tp = this.ctx.helper.div(nd.sf_tp !== undefined ? nd.sf_tp : nd.sf_tp, divNum, decimal.excl_tax_tp);
+                    detailUpdateData.push(nd);
+                }
                 const calcData = await this.getAllDataByCondition({ where: {stage_id: this.ctx.costStage.id, tree_is_leaf: 1 } });
                 for (const cd of calcData) {
                     const nd = { id: cd.id, tree_id: cd.tree_id };
-                    for (const prop of costFields.curFields) {
-                        nd[prop] = this.ctx.helper.round(nd[prop]);
+                    const relaDetail = detailUpdateData.filter(x => { return x.ledger_id === cd.id; });
+                    if (relaDetail && relaDetail.length > 0) {
+                        for (const rd of relaDetail) {
+                            for (const prop of costFields.calcFields) {
+                                if (prop === 'tax') continue;
+                                nd[prop] = this.ctx.helper.add(nd[prop], rd[prop]);
+                            }
+                        }
+                    } else {
+                        nd.tax = this.ctx.helper.round(cd.tax, decimal.tax);
+                        nd.pay_tp = this.ctx.helper.round(cd.pay_tp, decimal.tp);
+                        nd.cut_tp = this.ctx.helper.round(cd.cut_tp, decimal.tp);
+                        nd.yf_tp = this.ctx.helper.round(cd.yf_tp, decimal.tp);
+                        nd.sf_tp = this.ctx.helper.round(cd.sf_tp, decimal.tp);
+                        const divNum = nd.tax ? this.ctx.helper.add(1, this.ctx.helper.div(nd.tax, 100)) : 1;
+                        nd.yf_excl_tax_tp = this.ctx.helper.div(nd.yf_tp !== undefined ? nd.yf_tp : nd.yf_tp, divNum, decimal.excl_tax_tp);
+                        nd.sf_excl_tax_tp = this.ctx.helper.div(nd.sf_tp !== undefined ? nd.sf_tp : nd.sf_tp, divNum, decimal.excl_tax_tp);
                     }
                     updateData.push(nd);
                 }
             }
+
             const conn = await this.db.beginTransaction();
             try {
                 await conn.update(this.ctx.service.costStage.tableName, { id: this.ctx.costStage.id, decimal: JSON.stringify(this.ctx.costStage.decimal)});
                 if (updateData.length > 0) await conn.updateRows(this.tableName, updateData);
+                if (detailUpdateData.length > 0) await conn.updateRows(this.ctx.service.costStageDetail.tableName, detailUpdateData);
                 await conn.commit();
-                return { calc: calcTp, update: updateData };
+                return { calc: calcTp, bills: updateData, detail: detailUpdateData };
             } catch(err) {
                 await conn.rollback();
-                return { calc: false, update: [] };
+                return { calc: false, bills: [], detail: [] };
             }
-
         }
 
         async getSum(stage) {

+ 28 - 0
app/service/ledger.js

@@ -863,6 +863,34 @@ module.exports = app => {
             }
         }
 
+        async importCpdData(ledger, pos) {
+            const now = new Date();
+            const conn = await this.db.beginTransaction();
+            try {
+                ledger.forEach(x => {
+                    x.tender_id = this.ctx.tender.id;
+                });
+                pos.forEach(x => {
+                    x.tid = this.ctx.tender.id;
+                    x.in_time = now;
+                    x.add_user = this.ctx.session.sessionUser.accountId;
+                    x.add_stage = 0;
+                    x.add_stage_order = 0;
+                    x.add_times = 0;
+                });
+                await conn.delete(this.tableName, { tender_id: this.ctx.tender.id });
+                await conn.insert(this.ctx.service.ledger.tableName, ledger);
+                await conn.delete(this.ctx.service.pos.tableName, { tid: this.ctx.tender.id });
+                await conn.insert(this.ctx.service.pos.tableName, pos);
+                this._removeCacheMaxLid(this.ctx.tender.id);
+                await conn.commit();
+            } catch (err) {
+                this.ctx.log(err);
+                await conn.rollback();
+                throw (err.stack ? '导入台账数据错误' : err);
+            }
+        }
+
         async getMemoData(tid, columns) {
             if (!columns || columns.length === 0) return [];
 

+ 117 - 8
app/service/pos.js

@@ -258,26 +258,27 @@ module.exports = app => {
             let needUpdateBills;
             for (const d of data) {
                 if (d.sgfh_qty !== undefined || d.sjcl_qty !== undefined || d.qtcl_qty !== undefined || d.ex_qty1 !== undefined) {
+                    const od = billsPos.find(x => { return x.id === d.id; });
                     if (d.sgfh_qty !== undefined) {
                         d.sgfh_qty = this.round(d.sgfh_qty, precision.value);
-                    } else if (op) {
-                        d.sgfh_qty = op.sgfh_qty;
+                    } else if (od) {
+                        d.sgfh_qty = od.sgfh_qty;
                     }
                     if (d.sjcl_qty !== undefined) {
                         d.sjcl_qty = this.round(d.sjcl_qty, precision.value);
-                    } else if (op) {
-                        d.sjcl_qty = op.sjcl_qty;
+                    } else if (od) {
+                        d.sjcl_qty = od.sjcl_qty;
                     }
                     if (d.qtcl_qty !== undefined) {
                         d.qtcl_qty = this.round(d.qtcl_qty, precision.value);
-                    } else if (op) {
-                        d.qtcl_qty = op.qtcl_qty;
+                    } else if (od) {
+                        d.qtcl_qty = od.qtcl_qty;
                     }
                     d.quantity = this.ctx.helper.sum([d.sgfh_qty, d.qtcl_qty, d.sjcl_qty]);
                     if (d.ex_qty1 !== undefined) {
                         d.ex_qty1 = this.round(d.ex_qty1, precision.value);
-                    } else if (op) {
-                        d.ex_qty1 = op.ex_qty1;
+                    } else if (od) {
+                        d.ex_qty1 = od.ex_qty1;
                     }
                     needUpdateBills = true;
                 }
@@ -577,6 +578,112 @@ module.exports = app => {
             result.ancGclDetail = { add: insertAncGclDetail };
             return result;
         }
+        async _addPosDataWithDetail(tid, data) {
+            if (!(data.pos instanceof Array)) throw '提交数据错误';
+
+            const result = { ledger: {}, pos: null };
+            const user_id = this.ctx.session.sessionUser.accountId;
+
+            const bills = await this.ctx.service.ledger.getDataById(data.lid);
+            const le = await this.ctx.service.ledgerExtra.getDataById(data.lid);
+            const calcTemplate = le && le.calc_template ? await this.ctx.service.calcTmpl.getTemplate(le.calc_template) : null;
+            const precision = this.ctx.helper.findPrecision(this.ctx.tender.info.precision, bills.unit);
+            const existPos = await this.ctx.service.pos.getAllDataByCondition({ where : { lid: bills.id }, orders: [['porder', 'DESC']]});
+            const maxOrder = existPos.length > 0 ? existPos[0].porder : 0;
+            const updateBills = existPos.length > 0
+                ? { id: bills.id, sgfh_qty: bills.sgfh_qty, sjcl_qty: bills.sjcl_qty, qtcl_qty: bills.qtcl_qty, quantity: bills.quantity, ex_qty1: bills.ex_qty1 }
+                : { id: bills.id, sgfh_qty: 0, sjcl_qty: 0, qtcl_qty: 0, quantity: 0, ex_qty1: 0 };
+
+            const insertPos = [], insertDetail = [], insertAncGcl = [];
+            for (const [i, b] of data.pos.entries()) {
+                const nip = {};
+                insertPos.push(nip);
+                this._completeInsertPosData(tid, nip);
+                nip.lid = b.lid;
+                nip.name = b.name || '';
+                nip.porder = maxOrder + i + 1;
+
+                let sumQty = 0;
+                for (const [icd, cd] of b.calcDetail.entries()) {
+                    const newDetail = {
+                        id: this.uuid.v4(), tid: tid, lid: bills.id, pid: nip.id,
+                        create_user_id: user_id, update_user_id: user_id,
+                        pcd_order: icd+1,
+                    };
+                    if (data.calc_template === le.calc_template) {
+                        newDetail.str1 = cd.str1 || '';
+                        newDetail.str2 = cd.str2 || '';
+                        newDetail.str3 = cd.str3 || '';
+                        newDetail.str4 = cd.str4 || '';
+                        newDetail.num_a = cd.num_a || 0;
+                        newDetail.num_b = cd.num_b || 0;
+                        newDetail.num_c = cd.num_c || 0;
+                        newDetail.num_d = cd.num_d || 0;
+                        newDetail.num_e = cd.num_e || 0;
+                        newDetail.num_f = cd.num_f || 0;
+                        newDetail.num_g = cd.num_g || 0;
+                        newDetail.num_h = cd.num_h || 0;
+                        newDetail.num_i = cd.num_i || 0;
+                        newDetail.num_j = cd.num_j || 0;
+                        newDetail.num_k = cd.num_k || 0;
+                        newDetail.num_l = cd.num_l || 0;
+                        newDetail.num_m = cd.num_m || 0;
+                        newDetail.num_n = cd.num_n || 0;
+                        newDetail.num_o = cd.num_o || 0;
+                        newDetail.num_p = cd.num_p || 0;
+                        newDetail.num_q = cd.num_q || 0;
+                        newDetail.num_r = cd.num_r || 0;
+                        newDetail.num_s = cd.num_s || 0;
+                        newDetail.num_t = cd.num_t || 0;
+                        newDetail.num_u = cd.num_u || 0;
+                        newDetail.qty = cd.qty || 0;
+                        newDetail.expr = cd.expr || '';
+                        newDetail.spec = cd.spec || '';
+                    } else {
+                        this.ctx.service.posCalcDetail._loadDataAndCalc(newDetail, cd, {}, calcTemplate);
+                    }
+                    sumQty = this.ctx.helper.add(sumQty, newDetail.qty);
+                    insertDetail.push(newDetail);
+                }
+                this._calcExpr(nip, 'sgfh_qty', '', sumQty, precision);
+
+                for (const [iag, ag] of b.ancGcl.entries()) {
+                    const nig = {
+                        id: this.uuid.v4(), tid: this.ctx.tender.id,
+                        add_user_id: user_id, update_user_id: user_id,
+                        lid: nip.lid, pid: nip.id, g_order: iag + 1,
+                        is_aux: 0, name: ag.name || '', unit: ag.unit || '', quantity: ag.quantity || 0
+                    };
+                    insertAncGcl.push(nig);
+                }
+
+                nip.quantity = this.ctx.helper.add(nip.sgfh_qty, this.ctx.helper.add(nip.sjcl_qty, nip.qtcl_qty));
+                updateBills.sgfh_qty = this.ctx.helper.add(updateBills.sgfh_qty, nip.sgfh_qty);
+            }
+
+            const info = this.ctx.tender.info;
+            updateBills.quantity = this.ctx.helper.add(updateBills.sgfh_qty, this.ctx.helper.add(updateBills.sjcl_qty, updateBills.qtcl_qty));
+            updateBills.sgfh_tp = this.ctx.helper.mul(updateBills.sgfh_qty, bills.unit_price, info.decimal.tp);
+            updateBills.total_price = this.ctx.helper.mul(updateBills.quantity, bills.unit_price, info.decimal.tp);
+
+            const transaction = await this.db.beginTransaction();
+            try {
+                await transaction.insert(this.tableName, insertPos);
+                if (insertDetail.length > 0) await transaction.insert(this.ctx.service.posCalcDetail.tableName, insertDetail);
+                if (insertAncGcl.length > 0) await transaction.insert(this.ctx.service.ancillaryGcl.tableName, insertAncGcl);
+                await transaction.update(this.ctx.service.ledger.tableName, updateBills);
+                await transaction.commit();
+            } catch (err) {
+                await transaction.rollback();
+                throw err;
+            }
+            result.pos = insertPos;
+            updateBills.ledger_id = bills.ledger_id;
+            result.ledger.update = [updateBills];
+            result.posCalcDetail = { add: insertDetail };
+            result.ancGcl = { add: insertAncGcl };
+            return result;
+        }
 
         /**
          * 保存部位明细数据
@@ -600,6 +707,8 @@ module.exports = app => {
                     return await this._insertPosData(tid, data.updateData);
                 case 'paste-block':
                     return await this.pasteBlockData(tid, data.updateData);
+                case 'add-with-detail':
+                    return await this._addPosDataWithDetail(tid, data.updateData);
             }
         }
 

+ 1 - 1
app/service/pos_calc_detail.js

@@ -154,7 +154,7 @@ module.exports = app => {
             const insertData = [], updateData = [];
             let isCalc = false;
             for (const [i, d] of datas.entries()) {
-                if (!d.lid || !d.pid || !d.pcd_order) throw '新增设计量明细数据,提交的数据错误';
+                if (!d.lid || !d.pid) throw '新增设计量明细数据,提交的数据错误';
                 const nd = {
                     id: this.uuid.v4(), tid: this.ctx.tender.id,
                     create_user_id: user_id, update_user_id: user_id,

+ 19 - 0
app/service/report.js

@@ -402,6 +402,10 @@ module.exports = app => {
                             runnableRst.push(params.budget_id > 0 ? budgetSource.budgetCtrl(params.budget_id) : budgetSource.tenderCtrl(params.tender_id));
                             runnableKey.push(filter);
                             break;
+                        case 'mem_budget_real':
+                            runnableRst.push(params.budget_id > 0 ? budgetSource.budgetReal(params.budget_id) : budgetSource.tenderReal(params.tender_id));
+                            runnableKey.push(filter);
+                            break;
                         case 'mem_budget_final':
                             runnableRst.push(params.budget_id > 0 ? budgetSource.budgetFinal(params.budget_id) : budgetSource.tenderFinal(params.tender_id));
                             runnableKey.push(filter);
@@ -426,6 +430,10 @@ module.exports = app => {
                             runnableRst.push(params.budget_id > 0 ? budgetSource.budgetCtrl(params.budget_id, true) : budgetSource.tenderCtrl(params.tender_id, true));
                             runnableKey.push(filter);
                             break;
+                        case 'mem_budget_real_filter':
+                            runnableRst.push(params.budget_id > 0 ? budgetSource.budgetReal(params.budget_id, true) : budgetSource.tenderReal(params.tender_id, true));
+                            runnableKey.push(filter);
+                            break;
                         case 'mem_budget_final_filter':
                             runnableRst.push(params.budget_id > 0 ? budgetSource.budgetFinal(params.budget_id, true) : budgetSource.tenderFinal(params.tender_id, true));
                             runnableKey.push(filter);
@@ -551,6 +559,14 @@ module.exports = app => {
                             runnableRst.push(service.posCalcDetail.getAllDataByCondition({ where: { tid: params.tender_id } }));
                             runnableKey.push(filter);
                             break;
+                        case 'mem_anc_gcl':
+                            runnableRst.push(service.ancillaryGcl.getAllDataByCondition({ where: { tid: params.tender_id } }));
+                            runnableKey.push(filter);
+                            break;
+                        case 'mem_anc_gcl_detail':
+                            runnableRst.push(service.ancillaryGclDetail.getAllDataByCondition({ where: { tid: params.tender_id } }));
+                            runnableKey.push(filter);
+                            break;
                         default:
                             break;
                     }
@@ -716,6 +732,9 @@ module.exports = app => {
                     case 'mem_spss_compare_stage_pos':
                         rst[filter] = await spssSource.getCompareStagePos();
                         break;
+                    case 'mem_huajing_ledger_check':
+                        const huajingHelper = new rptCustomData.huajingHelper(this.ctx);
+                        rst[filter] = await huajingHelper.getLedgerCheckData(params.tender_id);
                     default:
                         break;
                 }

+ 3 - 1
app/service/shenpi_audit.js

@@ -413,7 +413,9 @@ module.exports = app => {
                     return;
                 }
                 // 更新固定审批流
-                await transaction.delete(this.tableName, { tid: tenderId, sp_type, sp_status });
+                const delCondition = { sp_type, sp_status };
+                this._.assign(delCondition, tidCondition);
+                await transaction.delete(this.tableName, delCondition);
                 const insertDatas = [];
                 for (const [i, id] of ids.entries()) {
                     const oneInsertData = {

+ 5 - 0
app/service/stage.js

@@ -357,6 +357,11 @@ module.exports = app => {
             return this.db.query(`SELECT * From ${this.tableName} WHERE tid = ? and status <> ?`, [tenderId, auditConst.stage.status.checked]);
         }
 
+        async getNumByChecked(tenderId) {
+            const num = await this.db.queryOne(`SELECT COUNT(*) as num From ${this.tableName} WHERE tid = ? and status = ?`, [tenderId, auditConst.stage.status.checked]);
+            return num ? num.num : 0;
+        }
+
         /**
          * 获取 最新一期 审批完成的 期计量
          * @param tenderId

+ 23 - 1
app/service/sub_project.js

@@ -20,6 +20,7 @@ const defaultFunRela = {
     needGcl: false,
     budgetZb: true,
     budgetCtrl: true,
+    budgetReal: false,
     costAnalysisType: 'book',
 };
 const funSet = require('../const/fun_set');
@@ -28,6 +29,9 @@ const pageShowConst = require('../const/sp_page_show').defaultSetting;
 const shenpiConst = require('../const/sp_shenpi');
 const defaultShenpi = shenpiConst.defaultInfo.shenpi;
 const arrayInfo = ['shenpi'];
+const ColSetConst = [
+    { key: 'cost_ledger', field: 'cost_ledger_col_set', defaultValue: { cur: 1, cur_excl_tax: 1, end: 1, end_excl_tax: 1} }
+];
 
 class DragTree {
     /**
@@ -736,7 +740,7 @@ module.exports = app => {
                 id: id, fun_rela: JSON.stringify({
                     banOver: data.banOver, hintOver: data.hintOver, banMinusChangeBills: data.banMinusChangeBills,
                     imType: data.imType,
-                    needGcl: data.needGcl, budgetCtrl: data.budgetCtrl, budgetZb: data.budgetZb,
+                    needGcl: data.needGcl, budgetCtrl: data.budgetCtrl, budgetZb: data.budgetZb, budgetReal: data.budgetReal,
                     minusNoValue: data.minusNoValue,
                     lockPayExpr: data.lockPayExpr, showMinusCol: data.showMinusCol, ledgerAss: data.ledgerAss,
                     costAnalysisType: data.costAnalysisType,
@@ -758,6 +762,24 @@ module.exports = app => {
             return result.affectedRows === 1;
         }
 
+        getColSet(type) {
+            const colSetInfo = ColSetConst.find(x => { return x.key === type; });
+            if (!colSetInfo) return null;
+
+            const result = this.ctx.subProject[colSetInfo.field] ? JSON.parse(this.ctx.subProject[colSetInfo.field]) : {};
+            this.ctx.helper._.defaults(result, colSetInfo.defaultValue);
+            return result;
+        }
+        async updateColSet(type, colSet) {
+            const colSetInfo = ColSetConst.find(x => { return x.key === type; });
+            if (!colSetInfo) throw '未定义的列设置类型';
+
+            const updateData = { id: this.ctx.subProject.id };
+            updateData[colSetInfo.field] = JSON.stringify(colSet);
+            const result = await this.db.update(this.tableName, updateData);
+            return result.affectedRows === 1;
+        }
+
         async saveCommonJson(id, field, datas) {
             const subProject = await this.getDataById(id);
             subProject.common_json = subProject.common_json ? JSON.parse(subProject.common_json) : {};

+ 96 - 0
app/service/temp_file.js

@@ -0,0 +1,96 @@
+'use strict';
+
+/**
+ *
+ *  临时文件
+ * @author Mai
+ * @date 2026/4/21
+ * @version
+ */
+
+
+module.exports = app => {
+    class TempFile extends app.BaseService {
+        /**
+         * 构造函数
+         *
+         * @param {Object} ctx - egg全局变量
+         * @return {void}
+         */
+        constructor(ctx) {
+            super(ctx);
+            this.tableName = 'temp_file';
+        }
+
+        _analysisData(files) {
+            const helper = this.ctx.helper;
+            const userId = this.ctx.session.sessionUser.accountId;
+            const ossPath = this.ctx.app.config.fujianOssPath;
+            files.forEach(x => {
+                x.viewpath = helper.canPreview(x.fileext) ? ossPath + x.filepath : '';
+                x.filepath = ossPath + x.filepath;
+                x.fileext_str = helper.fileExtStr(x.fileext);
+                x.canEdit = x.user_id === userId;
+            });
+        }
+
+        async getFiles(condition) {
+            condition.orders = [['create_time', 'desc']];
+            const result = await this.getAllDataByCondition(condition);
+            this._analysisData(result);
+            return result;
+        }
+
+        async addFiles(tenderId, fileInfo, user) {
+            const conn = await this.db.beginTransaction();
+            const result = {};
+            try {
+                const insertData = fileInfo.map(x => {
+                    return {
+                        id: this.uuid.v4(), tid: tenderId, rela_type: x.rela_type, rela_id: x.rela_id,
+                        user_id: user.id, user_name: user.name, user_company: user.company, user_role: user.role,
+                        filename: x.filename, fileext: x.fileext, filesize: x.filesize, filepath: x.filepath,
+                    };
+                });
+                await conn.insert(this.tableName, insertData);
+                await conn.commit();
+                result.files = { id: insertData.map(x => { return x.id; })};
+            } catch (err) {
+                await conn.rollback();
+                throw err;
+            }
+            return await this.getFiles({ where: result.files });
+        }
+
+        async delFiles(files) {
+            const fileDatas = await this.getAllDataByCondition({ where: { id: files } });
+            const result = {};
+
+            const conn = await this.db.beginTransaction();
+            try {
+                for (const f of fileDatas) {
+                    await this.ctx.app.fujianOss.delete(this.ctx.app.config.fujianOssFolder + f.filepath);
+                }
+                await conn.delete(this.tableName, { id: files });
+                await conn.commit();
+                result.del = files;
+            } catch (err) {
+                await conn.rollback();
+                throw err;
+            }
+            return result;
+        }
+
+        async clearFiles(days) {
+            const fileDatas = await this.db.query(`SELECT * FROM ${this.tableName} WHERE create_time < DATE_SUB(CURDATE(), INTERVAL ${days} day)`);
+            if (fileDatas.length === 0) return;
+
+            for (const f of fileDatas) {
+                await this.ctx.app.fujianOss.delete(this.ctx.app.config.fujianOssFolder + f.filepath);
+                await this.deleteById(f.id);
+            }
+        }
+    }
+
+    return TempFile;
+};

+ 1 - 0
app/service/tender_permission.js

@@ -48,6 +48,7 @@ module.exports = app => {
                 },
                 cost: {
                     view: { title: '查看', value: 1, isDefault: 1 },
+                    duty_add: { title: '责任成本上报', value: 6 },
                     ledger_add: { title: '成本台账上报', value: 2 },
                     book_add: { title: '财务账面上报', value: 3 },
                     analysis_add: { title: '成本分析上报', value: 4 },

+ 1 - 0
app/view/budget/compare.ejs

@@ -117,4 +117,5 @@
     const category = JSON.parse(unescape('<%- escape(JSON.stringify(categoryData)) %>'));
     const tenderList = JSON.parse(unescape('<%- escape(JSON.stringify(tenderList)) %>'));
     const ckColSetting = 'budget-col-visible-1.0.0';
+    const contract = JSON.parse(unescape('<%- escape(JSON.stringify(contract)) %>'));
 </script>

+ 54 - 2
app/view/budget/compare_modal.ejs

@@ -5,8 +5,60 @@
                 <h5 class="modal-title">选择决算标段</h5>
             </div>
             <div class="modal-body">
-                <h5>可选标段</h5>
-                <div id="sf-spread" style="height: 300px"></div>
+                <div class="" id="sf-stage-info">
+                    <div class="form-row align-items-center mb-2">
+                        <div class="col-auto"><span class="form-check-label">汇总时间:</span></div>
+                        <div class="col-auto">
+                            <div class="form-check form-check-inline" stage-type="stage">
+                                <input class="form-check-input" type="radio" id="tsm-source-final" value="final" name="tsm-source" checked>
+                                <label class="form-check-label" for="tsm-source-final">最新期</label>
+                            </div>
+                            <div class="form-check form-check-inline" stage-type="under-month">
+                                <input class="form-check-input" type="radio" id="tsm-source-under-month" value="under-month" name="tsm-source">
+                                <label class="form-check-label" for="tsm-source-under-month">截止月</label>
+                            </div>
+                        </div>
+                    </div>
+                    <div class="form-row align-items-center mb-2" id="tsm-stage-info-detail">
+                        <div class="col-auto"><span class="form-check-label">时间设置:</span>
+                        </div>
+                        <div class="d-flex">
+                            <div id="gather-by-under-month" name="gather-type" style="display: none">
+                                <div class="input-group input-group-sm">
+                                    <input id="gather-under-month" class="datepicker-here form-control mt-0" auto-close="true" autocomplete="off" placeholder="点击选择年月" data-view="months" data-min-view="months" data-date-format="yyyy-MM" data-language="zh" type="text" autocomplete="off">
+                                </div>
+                            </div>
+                            <div id="gather-by-final" name="gather-type">
+                                <div class="input-group input-group-sm">
+                                    <select class="form-control" style="width: 80px" disabled>
+                                    </select>
+                                </div>
+                            </div>
+                        </div>
+                        <div class="form-check form-check-inline ml-2">
+                            <input class="form-check-input" type="checkbox" id="tsm-filter-checked">
+                            <label class="form-check-label" for="tsm-filter-checked">仅统计审批完成</label>
+                        </div>
+                    </div>
+                </div>
+                <div>
+                    <ul class="nav nav-tabs">
+                        <li class="nav-item">
+                            <a class="nav-link active" data-toggle="tab" href="#sf-tender" role="tab" name="sf-tab">可选标段</a>
+                        </li>
+                        <li class="nav-item">
+                            <a class="nav-link" data-toggle="tab" href="#sf-contract" role="tab" name="sf-tab">可选合同</a>
+                        </li>
+                    </ul>
+                </div>
+                <div class="tab-content">
+                    <div id="sf-tender" class="tab-pane active">
+                        <div id="sf-tender-spread" style="height: 300px"></div>
+                    </div>
+                    <div id="sf-contract" class="tab-pane">
+                        <div id="sf-contract-spread" style="height: 300px"></div>
+                    </div>
+                </div>
             </div>
             <div class="modal-footer" style="justify-content: space-between">
                 <div class="d-flex">

+ 29 - 1
app/view/budget/detail_modal.ejs

@@ -47,7 +47,29 @@
         </div>
     </div>
 </div>
-
+<div class="modal fade" id="budget-extra-info" data-backdrop="static">
+    <div class="modal-dialog" role="document">
+        <div class="modal-content">
+            <div class="modal-header">
+                <h5 class="modal-title">附加属性</h5>
+            </div>
+            <div class="modal-body">
+                <div class="form-group">
+                    <label for="">设计优化率(%):</label>
+                    <input type="number" class="form-control form-control-sm nospin" id="bei-optimize_rate" value="<%- optimize_rate %>" onchange="checkNumberValid(this)">
+                </div>
+                <div class="form-group">
+                    <label for="">收益金额:</label>
+                    <input type="number" class="form-control form-control-sm nospin" id="bei-profit" value="<%- profit %>" onchange="checkNumberValid(this)">
+                </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="budget-extra-info-ok" data-dismiss="modal">确定</button>
+            </div>
+        </div>
+    </div>
+</div>
 <!--上传计价文件-->
 <div class="modal fade" id="import-dsk" data-backdrop="static" aria-modal="true">
     <div class="modal-dialog" role="document" style="width:400px">
@@ -113,3 +135,9 @@
     </div>
 </div>
 <% include ../shares/dsk_modal.ejs %>
+<script>
+    function checkNumberValid(obj) {
+        const value = _.toNumber(obj.value);
+        obj.value = value ? value : '';
+    }
+</script>

+ 10 - 2
app/view/budget/sub_menu_list.ejs

@@ -9,7 +9,15 @@
 <% if (ctx.subProject.fun_rela.budgetCtrl) { %>
 <nav-menu title="控制目标" url="/sp/<%= ctx.subProject.id %>/budget/ctrl" ml="3" active="<%= ctx.url.indexOf('/ctrl') %>"></nav-menu>
 <% } %>
+<% if (ctx.subProject.fun_rela.budgetReal) { %>
+<nav-menu title="实际投资" url="/sp/<%= ctx.subProject.id %>/budget/real" ml="3" active="<%= ctx.url.indexOf('/real') %>"></nav-menu>
+<% } %>
 <!--<nav-menu title="输出报表" url="/sp/<%= ctx.subProject.id %>/budget/report" ml="3" active="<%= ctx.url.indexOf('/report') %>"></nav-menu>-->
-<% if (!ctx.budget.readOnly && (ctx.url.indexOf('/gu') > 0 || ctx.url.indexOf('/gai') > 0 || ctx.url.indexOf('/yu') > 0 || ctx.url.indexOf('/zb') > 0 || ctx.url.indexOf('/ctrl') > 0)) { %>
-<div class="contarl-box"><button class="btn btn-primary btn-sm btn-block" data-toggle="modal" data-target="#budget-set">设置</button></div>
+<% if (!ctx.budget.readOnly && (ctx.url.indexOf('/gu') > 0 || ctx.url.indexOf('/gai') > 0 || ctx.url.indexOf('/yu') > 0 || ctx.url.indexOf('/zb') > 0 || ctx.url.indexOf('/ctrl') > 0 || ctx.url.indexOf('/real') > 0)) { %>
+<div class="contarl-box">
+    <button class="btn btn-primary btn-sm btn-block" data-toggle="modal" data-target="#budget-set">设置</button>
+    <% if ( ctx.url.indexOf('/ctrl') > 0 || ctx.url.indexOf('/real') > 0) { %>
+    <button class="btn btn-primary btn-sm btn-block" data-toggle="modal" data-target="#budget-extra-info">附加属性</button>
+    <% } %>
+</div>
 <% } %>

+ 23 - 12
app/view/contract/detail.ejs

@@ -79,9 +79,9 @@
                             <li class="nav-item">
                                 <a class="nav-link " data-toggle="tab" href="#htfile" role="tab">合同文件</a>
                             </li>
-<!--                            <li class="nav-item">-->
-<!--                                <a class="nav-link " data-toggle="tab" href="#htsupplement" role="tab">补充合同</a>-->
-<!--                            </li>-->
+                            <li class="nav-item">
+                                <a class="nav-link " data-toggle="tab" href="#htsupplement" role="tab">补充合同</a>
+                            </li>
                             <li class="ml-auto">
                                 <!-- 结算合同所有tab可见 ,结算后见解锁合同,2者互斥-->
                                 <a href="#cons-unlock" data-toggle="modal" data-target="#cons-unlock" style="display: none;" class="btn btn-success btn-sm pull-right mr-2">解锁合同</a>
@@ -95,8 +95,9 @@
                                 <a href="javascript:void(0);" id="cancel_contract_btn" style="display: none" class="btn btn-secondary btn-sm pull-right mr-2">取消</a>
                                 <a href="javascript:void(0);" id="save_contract_btn" style="display: none" class="btn btn-primary btn-sm pull-right mr-2">确定</a>
                                 <a href="javascript:void(0);" id="edit_contract_btn" style="display: none" class="btn btn-primary btn-sm pull-right mr-2">编辑合同</a>
+                                <a href="javascript:void(0);" id="ai_edit_contract_btn" style="display: none" class="btn btn-sm ai-btn pull-right mr-2"><i class="fa fa-magic"></i> AI识别编辑合同</a>
                                 <a href="javascript:void(0);" id="add_contract_supplement_btn" style="display: none;" class="btn btn-primary btn-sm pull-right mr-2">新增补充合同</a>
-                            <% if (shenpi_status) { %>
+                                <% if (shenpi_status) { %>
                                     <span class="pull-right mr-2" id="shenpi_btn">
                                     <a href="#sub-sp" data-toggle="modal" data-target="#sub-sp" class="btn btn-primary btn-sm">待上报</a>
                                 </span>
@@ -117,20 +118,28 @@
                                         <td width="20%" class="change-input-td" id="htdetail_tax"></td>
                                     </tr>
                                     <tr>
+                                        <th class="text-center align-middle">原合同金额</th>
+                                        <td class="change-input-td" id="htdetail_price"></td>
+                                        <th class="text-center align-middle">补充合同金额</th>
+                                        <td class="" id="htdetail_supplement_price"></td>
                                         <th class="text-center align-middle">合同金额</th>
-                                        <td class="change-input-td" id="htdetail_total_price"></td>
+                                        <td class="" id="htdetail_total_price"></td>
+                                    </tr>
+                                    <tr>
                                         <th class="text-center align-middle"><% if (ctx.contract_type === contractConst.type.income) { %>累计回款(P)<% } else if (ctx.contract_type === contractConst.type.expenses) { %>累计付款(P)<% } %></th>
                                         <td id="htdetail_pay_price"></td>
                                         <th class="text-center align-middle">累计扣款(K)</th>
                                         <td id="htdetail_debit_price"></td>
-                                    </tr>
-                                    <tr>
                                         <th class="text-center align-middle"><% if (ctx.contract_type === contractConst.type.income) { %>累计应回(S=P-K)<% } else if (ctx.contract_type === contractConst.type.expenses) { %>累计应付(S=P-K)<% } %></th>
                                         <td id="htdetail_yf_price"></td>
+                                    </tr>
+                                    <tr>
                                         <th class="text-center align-middle"><% if (ctx.contract_type === contractConst.type.income) { %>累计实回(A)<% } else if (ctx.contract_type === contractConst.type.expenses) { %>累计实付(A)<% } %></th>
                                         <td id="htdetail_sf_price"></td>
                                         <th class="text-center align-middle"><% if (ctx.contract_type === contractConst.type.income) { %>待回款(S-A)<% } else if (ctx.contract_type === contractConst.type.expenses) { %>待支付(S-A)<% } %></th>
                                         <td id="htdetail_df_price"></td>
+                                        <th class="text-center align-middle">创建时间</th>
+                                        <td id="htdetail_create_time"></td>
                                     </tr>
                                     <tr>
                                         <th class="text-center align-middle">甲方</th>
@@ -161,14 +170,11 @@
                                     <tr>
                                         <th class="text-center align-middle">结算书编号</th>
                                         <td id="htdetail_settle_code"></td>
-                                        <th class="text-center align-middle">创建时间</th>
-                                        <td id="htdetail_create_time"></td>
-                                    </tr>
-                                    <tr>
                                         <th class="text-center align-middle">备注</th>
                                         <td class="change-input-td" colspan="5" id="htdetail_remark"></td>
                                     </tr>
                                 </table>
+                                <% let tableHavePayCondition = false; %>
                                 <% if (attributeSet.length > 0) { %>
                                     <div class="mb-1">附加属性</div>
                                     <%
@@ -178,6 +184,7 @@
                                         let buffer = [];
 
                                         for (const attr of attributeSet) {
+                                            if (attr.field === 'pay_condition') tableHavePayCondition = true;
                                             if (attr.type === 'long_text') {
                                                 if (buffer.length) {
                                                     segments.push({ type: 'inline', items: [...buffer] });
@@ -222,6 +229,9 @@
                                         <% } %>
                                     </table>
                                 <% } %>
+                                <% if (!tableHavePayCondition) { %>
+                                    <textarea style="display: none" id="hidden_pay_condition"></textarea>
+                                <% } %>
                             </div>
                         </div>
                         <div class="tab-pane" id="htpay">
@@ -267,7 +277,7 @@
                                 <table class="table table-sm table-bordered" id="htsupplement-table" style="display: none;">
                                     <thead>
                                     <tr class="text-center" id="supplement-header">
-                                        <th width="3%">序号</th><th width="5%" sort="code">编号</th><th width="10%">名称</th><th width="7%">金额</th><th width="10%">甲方</th><th width="10%">乙方</th><th width="5%">签订时间</th><th width="5%">签订地点</th><th width="5%">创建人</th><th width="9%" sort="create_time">创建时间</th><th width="15%">补充原因及内容</th><th width="4%">附件</th>
+                                        <th width="3%">序号</th><th width="5%" sort="code">编号</th><th width="8%">名称</th><th width="7%">金额</th><th width="8%">甲方</th><th width="8%">乙方</th><th width="5%">签订时间</th><th width="5%">签订地点</th><th width="5%">创建人</th><th width="9%" sort="create_time">创建时间</th><th width="10%">补充原因及内容</th><th width="4%">附件</th>
                                         <% if (shenpi_status) { %>
                                             <th width="15%">审批进度</th>
                                         <% } %>
@@ -315,6 +325,7 @@
     const accountGroup3 = JSON.parse(unescape('<%- escape(JSON.stringify(accountGroup)) %>'));
     const accountList3 = JSON.parse(unescape('<%- escape(JSON.stringify(accountList)) %>'));
     const attributeSet = JSON.parse(unescape('<%- escape(JSON.stringify(attributeSet)) %>'));
+    console.log(attributeSet);
 
     const thisUrl = JSON.parse(unescape('<%- escape(JSON.stringify(thisUrl)) %>'));
     const stdChapters = JSON.parse(unescape('<%- escape(JSON.stringify(stdChapters)) %>'));

+ 301 - 29
app/view/contract/detail_modal.ejs

@@ -3,52 +3,251 @@
 <% if (ctx.session.sessionUser.is_admin || audit_permission.permission_add) { %>
 <!--新增合同-->
 <div class="modal fade" id="cons-add" data-backdrop="static">
-    <div class="modal-dialog" role="document">
+    <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">
-                    <label>合同编号<b class="text-danger">*</b></label>
-                    <input class="form-control form-control-sm" placeholder="请输入合同编号" type="text" name="code">
+                <div class="row">
+                    <div class="col-md-6">
+                        <div class="form-group">
+                            <label>合同编号<b class="text-danger">*</b></label>
+                            <input class="form-control form-control-sm" placeholder="请输入合同编号" type="text" name="c_code">
+                        </div>
+                    </div>
+                    <div class="col-md-6">
+                        <div class="form-group">
+                            <label>合同名称<b class="text-danger">*</b></label>
+                            <input class="form-control form-control-sm" placeholder="请输入合同名称" type="text" name="name">
+                        </div>
+                    </div>
                 </div>
-                <div class="form-group">
-                    <label>合同名称<b class="text-danger">*</b></label>
-                    <input class="form-control form-control-sm" placeholder="请输入合同名称" type="text" name="name">
+
+                <div class="row">
+                    <div class="col-md-6">
+                        <div class="form-group">
+                            <label>合同金额<b class="text-danger">*</b></label>
+                            <input class="form-control form-control-sm" placeholder="请输入合同金额" type="text" name="price">
+                            <div class="invalid-feedback">合同金额不能为0。</div>
+                        </div>
+                    </div>
+                    <div class="col-md-6">
+                        <div class="form-group">
+                            <label>税率(%)</label>
+                            <input class="form-control form-control-sm" placeholder="" type="text" name="tax">
+                        </div>
+                    </div>
                 </div>
-                <div class="form-group">
-                    <label>合同金额<b class="text-danger">*</b></label>
-                    <input class="form-control form-control-sm" placeholder="请输入合同金额" type="text" name="total_price">
-                    <div class="invalid-feedback">合同金额不能为0。</div>
+
+                <div class="row">
+                    <div class="col-md-6">
+                        <div class="form-group">
+                            <label>合同类型</label>
+                            <select class="form-control form-control-sm" name="type">
+                                <option value=""></option>
+                                <% for (const t of types) { %>
+                                    <option value="<%- t %>"><%- t %></option>
+                                <% } %>
+                            </select>
+                        </div>
+                    </div>
+                    <div class="col-md-6">
+                        <div class="form-group">
+                            <label>签订日期</label>
+                            <input class="datepicker-here form-control form-control-sm" name="sign_date" placeholder="点击选择时间" data-date-format="yyyy-MM-dd" data-language="zh" type="text" autocomplete="off">
+                        </div>
+                    </div>
                 </div>
-                <div class="form-group">
-                    <label>合同类型</label>
-                    <select class="form-control form-control-sm" name="type">
-                        <option value=""></option>
-                        <% for (const t of types) { %>
-                        <option value="<%- t %>"><%- t %></option>
-                        <% } %>
-                    </select>
+
+                <div class="row">
+                    <div class="col-md-6">
+                        <div class="form-group">
+                            <label>签订单位(甲方)</label>
+                            <input class="form-control form-control-sm" placeholder="" type="text" name="party_a">
+                        </div>
+                    </div>
+                    <div class="col-md-6">
+                        <div class="form-group">
+                            <label>甲方签约人</label>
+                            <input class="form-control form-control-sm" placeholder="" type="text" name="party_a_user">
+                        </div>
+                    </div>
                 </div>
-                <div class="form-group">
-                    <label>签订单位(甲方)</label>
-                    <input class="form-control form-control-sm" placeholder="请输入签订单位" type="text" name="party_a">
+
+                <div class="row">
+                    <div class="col-md-6">
+                        <div class="form-group">
+                            <label>签订单位(乙方)</label>
+                            <input class="form-control form-control-sm" placeholder="" type="text" name="party_b">
+                        </div>
+                    </div>
+                    <div class="col-md-6">
+                        <div class="form-group">
+                            <label>乙方签约人</label>
+                            <input class="form-control form-control-sm" placeholder="" type="text" name="party_b_user">
+                        </div>
+                    </div>
                 </div>
-                <div class="form-group">
-                    <label>签订单位(乙方)</label>
-                    <input class="form-control form-control-sm" placeholder="请输入签订单位" type="text" name="party_b">
+
+                <div class="row">
+                    <div class="col-md-6">
+                        <div class="form-group">
+                            <label>签约地点</label>
+                            <input class="form-control form-control-sm" placeholder="" type="text" name="address">
+                        </div>
+                    </div>
                 </div>
-                <div class="form-group">
-                    <label>签订日期</label>
-                    <input class="datepicker-here form-control form-control-sm" name="sign_date" placeholder="点击选择时间" data-date-format="yyyy-MM-dd" data-language="zh" type="text" autocomplete="off">
+
+                <div class="payee-fields-group">
+                    <hr class="my-2">
+                    <div class="row">
+                        <div class="col-md-4">
+                            <div class="form-group">
+                                <label>收款单位</label>
+                                <input class="form-control form-control-sm" placeholder="" type="text" name="entity">
+                            </div>
+                        </div>
+                        <div class="col-md-4">
+                            <div class="form-group">
+                                <label>收款开户行</label>
+                                <input class="form-control form-control-sm" placeholder="" type="text" name="bank">
+                            </div>
+                        </div>
+                        <div class="col-md-4">
+                            <div class="form-group">
+                                <label>收款账号</label>
+                                <input class="form-control form-control-sm" placeholder="" type="text" name="bank_account">
+                            </div>
+                        </div>
+                    </div>
                 </div>
+
                 <div class="form-group">
                     <label>备注</label>
-                    <textarea class="form-control form-control-sm" name="remark" rows="3"></textarea>
+                    <textarea class="form-control form-control-sm" name="remark" rows="2"></textarea>
+                </div>
+                <% let havePayCondition = false; %>
+                <% if (attributeSet.length > 0) { %>
+                    <div class="attribute-fields-group">
+                        <hr class="my-2">
+
+                        <a class="btn btn-light btn-sm w-100 d-flex justify-content-between align-items-center"
+                           data-toggle="collapse"
+                           href="#contractAttrCollapse"
+                           role="button"
+                           aria-expanded="true"
+                           aria-controls="contractAttrCollapse">
+
+                            <span>
+                                <i class="fa fa-list-alt mr-1"></i>
+                                附加属性(<%= attributeSet.length %>项)
+                            </span>
+
+                            <i class="fa fa-chevron-up attr-collapse-icon"></i>
+                        </a>
+
+                        <div class="collapse show mt-2" id="contractAttrCollapse">
+
+                            <div class="attribute-scroll-area">
+
+                        <%
+                            let colCount = 0;
+                        %>
+
+                        <div class="row">
+
+                            <% for (const attr of attributeSet) { %>
+                            <% if (attr.field === 'pay_condition') { havePayCondition = true; } %>
+
+                                <% if (attr.type === 'long_text') { %>
+
+                                    <% if (colCount > 0) { %>
+                        </div>
+                        <div class="row">
+                            <% colCount = 0; %>
+                            <% } %>
+
+                            <div class="col-md-12">
+                                <div class="form-group">
+                                    <label><%- attr.alias || attr.name %></label>
+                                    <textarea
+                                            class="form-control form-control-sm"
+                                            name="<%- attr.field %>"
+                                            rows="2"></textarea>
+                                </div>
+                            </div>
+
+                        </div>
+                        <div class="row">
+
+                            <% } else { %>
+
+                                <div class="col-md-4">
+                                    <div class="form-group">
+                                        <label><%- attr.alias || attr.name %></label>
+                                        <input
+                                                class="form-control form-control-sm"
+                                                type="<%- attr.type %>"
+                                                name="<%- attr.field %>">
+                                    </div>
+                                </div>
+
+                                <% colCount++; %>
+
+                                <% if (colCount === 3) { %>
+                        </div>
+                        <div class="row">
+                            <% colCount = 0; %>
+                            <% } %>
+
+                            <% } %>
+
+                            <% } %>
+
+                        </div>
+
+                    </div>
+
+                </div>
+
+            </div>
+                <% } %>
+                <% if (!havePayCondition) { %>
+                    <textarea style="display: none" name="pay_condition"></textarea>
+                <% } %>
+
+                <!-- AI识别文件 -->
+                <div id="ai-file-panel" class="border rounded p-2 mt-2" style="display:none;">
+                    <div class="d-flex justify-content-between align-items-center">
+                        <div>
+                            <label class="mb-1 font-weight-bold">
+                                <i class="fa fa-file-text-o text-primary"></i>
+                                AI识别文件
+                            </label>
+
+                            <div class="text-muted small">
+                                <i class="fa fa-file-o"></i>
+                                <span id="ai-file-name"></span>
+                            </div>
+                        </div>
+
+                        <div class="custom-control custom-checkbox">
+                            <input type="checkbox"
+                                   class="custom-control-input"
+                                   id="contract-add-attach"
+                                   checked>
+                            <label class="custom-control-label" for="contract-add-attach">
+                                添加至合同附件
+                            </label>
+                        </div>
+                    </div>
                 </div>
             </div>
             <div class="modal-footer">
+                <a href="javascript:void(0);" class="btn btn-sm ai-btn" id="aiFillBtn"><i class="fa fa-magic"></i> AI识别填写</a>
+                <input type="hidden" id="contract-add-cid">
+                <input type="hidden" id="contract-add-fid">
                 <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">确定</button>
             </div>
@@ -56,6 +255,77 @@
     </div>
 </div>
 <% } %>
+<div class="modal fade" id="ai-recognize-modal" data-backdrop="static">
+    <div class="modal-dialog modal-md" role="document">
+        <div class="modal-content">
+            <div class="modal-header">
+                <h5 class="modal-title"><i class="fa-solid fa-wand-magic-sparkles text-warning"></i> AI识别填写</h5>
+                <button type="button" class="close" data-dismiss="modal" aria-label="Close">
+                    <span aria-hidden="true">&times;</span>
+                </button>
+            </div>
+            <div class="modal-body">
+
+                <!-- 已有合同文件选择区(仅编辑模式且存在文件时显示) -->
+                <div class="ai-existing-files" id="aiExistingFilesArea" style="display:none;">
+                    <div class="ai-section-label">从已有合同文件(PDF、Word、图片格式)中选择一份识别</div>
+                    <div class="ai-existing-files-list" id="aiExistingFilesList"></div>
+                </div>
+
+                <div class="ai-section-label mt-2" id="aiUploadSectionLabel" style="display:none;">或上传新文件</div>
+
+                <!-- 上传区域 -->
+                <div class="ai-upload-area" id="aiUploadArea">
+                    <input type="radio" name="aiFileSource" id="aiSourceUpload" value="upload" style="display:none;">
+                    <input type="file" id="aiFileInput" accept=".pdf,.doc,.docx,.png,.jpg,.jpeg" style="display:none;">
+                    <div class="ai-upload-placeholder" id="aiUploadPlaceholder">
+                        <i class="fa-solid fa-cloud-arrow-up fa-2x text-muted mb-2"></i>
+                        <div>点击或拖拽上传合同文件</div>
+                        <div class="text-muted small">支持 PDF、Word、图片格式</div>
+                    </div>
+                    <div class="ai-upload-result" id="aiUploadResult" style="display:none;">
+                        <i class="fa-solid fa-file-lines text-primary"></i>
+                        <span id="aiFileName" class="ml-1"></span>
+                        <i class="fa-solid fa-circle-xmark text-danger ml-2" id="aiFileRemove" style="cursor:pointer;"></i>
+                    </div>
+                </div>
+
+                <!-- 注意事项 -->
+                <div class="ai-notice mt-3">
+                    <div class="ai-notice-title"><i class="fa-solid fa-circle-info"></i> 识别注意事项</div>
+                    <ul class="ai-notice-list">
+                        <li>识别结果由AI自动提取,可能存在误差,提交前请仔细核对各字段;</li>
+                        <li>手写签名、模糊印章等内容识别准确率相对较低,建议重点核对甲乙方、金额、日期信息;</li>
+                        <li>扫描件请尽量保证清晰、无遮挡,识别效果会更准确;</li>
+                        <li>识别过程可能需要几秒到一分钟,请耐心等待,请勿重复提交。</li>
+                    </ul>
+                    <div class="ai-notice-divider"></div>
+                    <div class="ai-notice-title"><i class="fa-solid fa-shield-halved"></i> 数据安全提示</div>
+                    <ul class="ai-notice-list">
+                        <li>上传的合同文件仅用于识别填写本系统合同相关表单信息,其余内容一概不做任何提取保留。</li>
+<!--                        <li>请勿上传包含他人隐私信息(如身份证号、个人手机号等非必要信息)的非合同类文件。</li>-->
+                    </ul>
+                </div>
+
+                <!-- 勾选项:仅在"上传新文件"且编辑模式下显示 -->
+                <div class="form-check mt-3" id="aiAttachCheckArea" style="display:none;">
+                    <input type="checkbox" class="form-check-input" id="aiAttachToContract" checked>
+                    <label class="form-check-label" for="aiAttachToContract">
+                        将此文件同时添加至该合同的合同文件中
+                    </label>
+                </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="aiStartRecognize" disabled>
+                    <span id="aiRecognizeBtnText">开始识别</span>
+                    <span id="aiRecognizeSpinner" class="spinner-border spinner-border-sm ml-1" style="display:none;"></span>
+                </button>
+            </div>
+        </div>
+    </div>
+</div>
 <% if (ctx.session.sessionUser.is_admin) { %>
 <% include modal.ejs %>
     <!-- 节点授权 -->
@@ -505,6 +775,7 @@
                 <button type="button" class="btn btn-secondary btn-sm" data-dismiss="modal">关闭</button>
                 <input type="hidden" id="start-sp-cid" />
                 <input type="hidden" id="start-sp-cpid" />
+                <input type="hidden" id="start-sp-csid" />
                 <button class="btn btn-primary btn-sm" type="button" id="start-sp-btn">确认上报</button>
             </div>
         </div>
@@ -536,6 +807,7 @@
                 <button type="button" class="btn btn-secondary btn-sm" data-dismiss="modal">关闭</button>
                 <input type="hidden" id="check-sp-cid" />
                 <input type="hidden" id="check-sp-cpid" />
+                <input type="hidden" id="check-sp-csid" />
                 <input type="hidden" id="check-operate" />
                 <span id="check-sp-btn-html">
                 </span>

+ 5 - 0
app/view/cost/analysis.ejs

@@ -60,6 +60,8 @@
                 <div class="tab-content">
                     <div id="search" class="tab-pane tab-select-show">
                     </div>
+                    <div id="fujian" class="tab-pane tab-select-show">
+                    </div>
                 </div>
             </div>
         </div>
@@ -69,6 +71,9 @@
                 <li class="nav-item">
                     <a class="nav-link" content="#search" href="javascript: void(0);">查找定位</a>
                 </li>
+                <li class="nav-item">
+                    <a class="nav-link" content="#fujian" href="javascript: void(0);">附件</a>
+                </li>
             </ul>
         </div>
     </div>

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

@@ -1,3 +1,6 @@
 <nav-menu title="返回" url="/sp/<%= ctx.subProject.id %>/cost/tender/<%- ctx.tender.id %>/analysis" tclass="text-primary" ml="1" icon="fa-chevron-left"></nav-menu>
 <nav-menu title="成本分析" url="/sp/<%= ctx.subProject.id %>/cost/tender/<%- ctx.tender.id %>/analysis/<%- ctx.costStage.stage_order %>/stage" ml="3" active="<%= (ctx.url.indexOf('stage') >= 0 ? 1 : -1) %>"></nav-menu>
+<% if (ctx.app.config.is_debug) { %>
+<nav-menu title="审核比较" url="/sp/<%= ctx.subProject.id %>/cost/tender/<%- ctx.tender.id %>/analysis/<%- ctx.costStage.stage_order %>/compare" ml="3" active="<%= (ctx.url.indexOf('compare') >= 0 ? 1 : -1) %>"></nav-menu>
+<% } %>
 <% include ./audit_btn.ejs %>

+ 18 - 1
app/view/cost/analysis_modal.ejs

@@ -1,5 +1,6 @@
 <% include ../shares/delete_hint_modal.ejs %>
 <% include ./audit_modal.ejs %>
+<% include ../shares/upload_att.ejs %>
 <div class="modal" id="import-pay" data-backdrop="static" aria-modal="true" role="dialog">
     <div class="modal-dialog" role="document">
         <div class="modal-content">
@@ -21,10 +22,26 @@
                 </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="import-pay-ok">确定</button>
             </div>
         </div>
     </div>
+</div>
+<div class="modal" id="import-cost" data-backdrop="static" aria-modal="true" role="dialog">
+    <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 id="import-cost-spread" class="modal-height-500">
+                </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="import-cost-ok">确定</button>
+            </div>
+        </div>
+    </div>
 </div>

+ 5 - 4
app/view/cost/audit_btn.ejs

@@ -27,15 +27,16 @@
         <% } %>
     <% } %>
 
-    <% if (ctx.costStage.finalAuditorIds.indexOf(ctx.session.sessionUser.accountId) >= 0 && ctx.costStage.audit_status === auditConst.status.checked && ctx.costStage.isLatest) { %>
-        <a href="javascript: void(0);" data-toggle="modal" data-target="#sp-down-back" class="btn btn-warning btn-sm btn-block">重新审批</a>
-    <% } %>
-
     <% if (ctx.costStage.cancancel) { %>
         <a href="javascript: void(0);" data-toggle="modal" data-target="#sp-down-cancel" class="btn btn-danger btn-sm btn-block">撤回</a>
     <% } %>
 
+    <% if (ctx.costStage.stage_type !== 'duty') { %>
+    <% if (ctx.costStage.finalAuditorIds.indexOf(ctx.session.sessionUser.accountId) >= 0 && ctx.costStage.audit_status === auditConst.status.checked && ctx.costStage.isLatest) { %>
+    <a href="javascript: void(0);" data-toggle="modal" data-target="#sp-down-back" class="btn btn-warning btn-sm btn-block">重新审批</a>
+    <% } %>
     <% if (ctx.costStage.create_user_id === ctx.session.sessionUser.accountId && ctx.costStage.isLatest && (ctx.costStage.audit_status === auditConst.status.checkNo || ctx.costStage.audit_status === auditConst.status.uncheck)) { %>
         <a href="#del-qi" data-toggle="modal" data-target="#del-qi" class="btn btn-outline-danger btn-sm btn-block mt-5">删除本期</a>
     <% } %>
+    <% } %>
 </div>

+ 3 - 1
app/view/cost/audit_modal.ejs

@@ -829,6 +829,8 @@
     const shenpiConst =  JSON.parse('<%- JSON.stringify(shenpiConst) %>');
     const createUserId = parseInt('<%= ctx.costStage.create_user_id %>');
     const auditType = JSON.parse(unescape('<%- escape(JSON.stringify(auditType)) %>'));
+    const csrf = '<%= ctx.csrf %>';
+    const authMobile = '<%= authMobile %>';
 </script>
 <script>
     $('[name=stage-start]').submit(function (e) {
@@ -895,4 +897,4 @@
         <% } %>
         $('#hide-all').hide();
     });
-</script>
+</script>

+ 5 - 0
app/view/cost/book.ejs

@@ -19,6 +19,11 @@
                     </div>
                 </div>
             </div>
+            <% if (ctx.costStage.create_user_id === ctx.session.sessionUser.accountId) { %>
+            <div class="d-inline-block">
+                <a href="" class="btn btn-sm btn-light text-primary" data-toggle="modal" data-placement="bottom" title="设置" data-target="#cost-decimal"><i class="fa fa-cog" aria-hidden="true"></i></a>
+            </div>
+            <% } %>
             <div class="ml-auto">
             </div>
         </div>

+ 23 - 11
app/view/cost/book_list.ejs

@@ -19,16 +19,24 @@
                 <table class="table table-bordered table-hover">
                     <thead>
                     <tr class="text-center">
-                        <th width="80px">期数</th>
-                        <th width="70px">报审月份</th>
-                        <th width="70px">关联成本</th>
-                        <th width="70px">创建人</th>
-                        <th width="180px">本期入账金额</th>
-                        <th width="180px">截止本期入账金额</th>
-                        <th width="180px">本期入账金额(不含税)</th>
-                        <th width="180px">截止本期入账金额(不含税)</th>
-                        <th>审批进度</th>
-                        <th width="100px">操作</th>
+                        <th width="80px" rowspan="2">期数</th>
+                        <th width="70px" rowspan="2">报审月份</th>
+                        <th width="70px" rowspan="2">关联成本</th>
+                        <th width="70px" rowspan="2">创建人</th>
+                        <th width="120px" colspan="4">本期</th>
+                        <th width="120px" colspan="4">截止本期</th>
+                        <th rowspan="2">审批进度</th>
+                        <th width="100px" rowspan="2">操作</th>
+                    </tr>
+                    <tr class="text-center">
+                        <th width="130px">应付金额</th>
+                        <th width="130px">实付金额</th>
+                        <th width="130px">入账金额</th>
+                        <th width="130px">入账金额(不含税)</th>
+                        <th width="130px">应付金额</th>
+                        <th width="130px">实付金额</th>
+                        <th width="130px">入账金额</th>
+                        <th width="130px">入账金额(不含税)</th>
                     </tr>
                     </thead>
                     <tbody>
@@ -40,9 +48,13 @@
                         <td class="text-center"><%- s.stage_date %></td>
                         <td class="text-center">第<%- s.rela_stage.sorder %>期</td>
                         <td class="text-center"><%- s.user_name %></td>
+                        <td class="text-right"><%- s.relaStage.stage_tp.yf_tp %></td>
+                        <td class="text-right"><%- s.relaStage.stage_tp.sf_tp %></td>
                         <td class="text-right"><%- s.stage_tp.in_tp %></td>
-                        <td class="text-right"><%- s.stage_end_tp.in_tp %></td>
                         <td class="text-right"><%- s.stage_tp.in_excl_tax_tp %></td>
+                        <td class="text-right"><%- s.relaStage.stage_end_tp.yf_tp %></td>
+                        <td class="text-right"><%- s.relaStage.stage_end_tp.sf_tp %></td>
+                        <td class="text-right"><%- s.stage_end_tp.in_tp %></td>
                         <td class="text-right"><%- s.stage_end_tp.in_excl_tax_tp %></td>
                         <td class="<%- auditConst.info[s.audit_status].class %>">
                             <% if (s.audit_status === auditConst.status.checked && s.final_auditor_str) { %>

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

@@ -1,3 +1,6 @@
 <nav-menu title="返回" url="/sp/<%= ctx.subProject.id %>/cost/tender/<%- ctx.tender.id %>/book" tclass="text-primary" ml="1" icon="fa-chevron-left"></nav-menu>
 <nav-menu title="财务账面" url="/sp/<%= ctx.subProject.id %>/cost/tender/<%- ctx.tender.id %>/book/<%- ctx.costStage.stage_order %>/stage" ml="3" active="<%= (ctx.url.indexOf('stage') >= 0 ? 1 : -1) %>"></nav-menu>
+<% if (ctx.app.config.is_debug) { %>
+<nav-menu title="审核比较" url="/sp/<%= ctx.subProject.id %>/cost/tender/<%- ctx.tender.id %>/book/<%- ctx.costStage.stage_order %>/compare" ml="3" active="<%= (ctx.url.indexOf('compare') >= 0 ? 1 : -1) %>"></nav-menu>
+<% } %>
 <% include ./audit_btn.ejs %>

+ 2 - 1
app/view/cost/book_modal.ejs

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

+ 205 - 0
app/view/cost/compare.ejs

@@ -0,0 +1,205 @@
+<% include ./stage_memu.ejs %>
+<div class="panel-content">
+    <div class="panel-title">
+        <div class="title-main d-flex">
+            <% include ./stage_mini_menu.ejs %>
+            <div>
+                <div class="d-inline-block">
+                    <div class="dropdown">
+                        <button class="btn btn-sm btn-light dropdown-toggle text-primary" type="button" id="dropdownMenuButton" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
+                            <i class="fa fa-list-ol"></i> 显示层级
+                        </button>
+                        <div class="dropdown-menu" aria-labelledby="dropdownMenuButton">
+                            <a class="dropdown-item" name="showLevel" tag="1" href="javascript: void(0);">第一层</a>
+                            <a class="dropdown-item" name="showLevel" tag="2" href="javascript: void(0);">第二层</a>
+                            <a class="dropdown-item" name="showLevel" tag="3" href="javascript: void(0);">第三层</a>
+                            <a class="dropdown-item" name="showLevel" tag="4" href="javascript: void(0);">第四层</a>
+                            <a class="dropdown-item" name="showLevel" tag="last" href="javascript: void(0);">最底层</a>
+                        </div>
+                    </div>
+                </div>
+            </div>
+        </div>
+    </div>
+    <div class="content-wrap row pr-46">
+        <div class="c-header p-0 col-12">
+        </div>
+        <!--核心内容(两栏)-->
+        <div class="row w-100 sub-content">
+            <!--左栏-->
+            <div class="c-body" id="left-view" style="width: 100%">
+                <div class="sjs-height-1" style="overflow: hidden" id="bills-spread">
+                </div>
+                <div class="bcontent-wrap">
+                    <div class="resize-y" id="detail-spr" r-Type="height" div1=".sjs-height-1" div2=".bcontent-wrap" r-parent="div2" title="调整大小"><!--调整上下高度条--></div>
+                    <div class="bc-bar mb-1">
+                        <ul class="nav nav-tabs">
+                            <li class="nav-item">
+                                <a class="nav-link active" href="javascript:void(0)">明细数据</a>
+                            </li>
+                        </ul>
+                    </div>
+                    <div class="sp-wrap" id="detail-spread">
+                    </div>
+                </div>
+            </div>
+            <div class="c-body" id="right-view" style="display: none; width: 33%;">
+                <div class="resize-x" id="right-spr" r-Type="width" div1="#left-view" div2="#right-view" title="调整大小" a-type="percent"><!--调整左右高度条--></div>
+                <div class="tab-content">
+                    <div id="search" class="tab-pane tab-select-show">
+                    </div>
+                </div>
+            </div>
+        </div>
+        <!--右侧菜单-->
+        <div class="side-menu">
+            <ul class="nav flex-column right-nav" id="side-menu">
+                <li class="nav-item">
+                    <a class="nav-link" content="#search" href="javascript: void(0);">查找定位</a>
+                </li>
+            </ul>
+        </div>
+    </div>
+</div>
+<script>
+    const tenderId = parseInt('<%- ctx.tender.id %>');
+    const stageId = parseInt('<%- ctx.costStage.id %>');
+    const stageType = parseInt('<%- ctx.costStage.stage_type %>');
+    const stageDate = '<%- ctx.costStage.stage_date %>';
+    <% if (ctx.costStage.stage_type === 'ledger') { %>
+    const bllsSpreadSetting = {
+        cols: [
+            {title: '编号', colSpan: '1', rowSpan: '2', field: 'code', hAlign: 0, width: 230, formatter: '@', cellType: 'tree'},
+            {title: '名称', colSpan: '1', rowSpan: '2', field: 'name', hAlign: 0, width: 185, formatter: '@', cellType: 'autoTip'},
+            {title: '单位', colSpan: '1', rowSpan: '2', field: 'unit', hAlign: 1, width: 50, formatter: '@', cellType: 'unit'},
+            {title: '税率(%)', colSpan: '1', rowSpan: '2', field: 'tax', hAlign: 2, width: 80, type: 'Number'},
+            {title: '本期|付款金额', colSpan: '4|1', rowSpan: '1|1', field: 'pay_tp', hAlign: 2, width: 80, type: 'Number'},
+            {title: '|扣款金额', colSpan: '|1', rowSpan: '|1', field: 'cut_tp', hAlign: 2, width: 80, type: 'Number'},
+            {title: '|应付金额', colSpan: '|1', rowSpan: '|1', field: 'yf_tp', hAlign: 2, width: 80, type: 'Number'},
+            {title: '|实付金额', colSpan: '|1', rowSpan: '|1', field: 'sf_tp', hAlign: 2, width: 80, type: 'Number'},
+            {title: '本期(不含税)|应付金额', colSpan: '2|1', rowSpan: '1|1', field: 'yf_excl_tax_tp', hAlign: 2, width: 100, type: 'Number'},
+            {title: '|实付金额', colSpan: '|1', rowSpan: '|1', field: 'sf_excl_tax_tp', hAlign: 2, width: 100, type: 'Number'},
+        ],
+        emptyRows: 0,
+        headRows: 2,
+        headRowHeight: [25, 25],
+        defaultRowHeight: 21,
+        headerFont: '12px 微软雅黑',
+        font: '12px 微软雅黑',
+        headColWidth: [50],
+        tipsSum: true,
+        readOnly: true,
+    };
+    const dealSpreadSetting = {
+        cols: [
+            {title: '合同编号', colSpan: '1', rowSpan: '2', field: 'code', hAlign: 0, width: 160, formatter: '@'},
+            {title: '合同名称', colSpan: '1', rowSpan: '2', field: 'name', hAlign: 0, width: 160, formatter: '@'},
+            {title: '乙方', colSpan: '1', rowSpan: '2', field: 'party_b', hAlign: 0, width: 150, formatter: '@'},
+            {title: '支付年月', colSpan: '1', rowSpan: '2', field: 'pay_date', hAlign: 1, width: 120, formatter: '@'},
+            {title: '税率(%)', colSpan: '1', rowSpan: '2', field: 'tax', hAlign: 2, width: 80, type: 'Number'},
+            {title: '本期|付款金额', colSpan: '4|1', rowSpan: '1|1', field: 'pay_tp', hAlign: 2, width: 80, type: 'Number'},
+            {title: '|扣款金额', colSpan: '|1', rowSpan: '|1', field: 'cut_tp', hAlign: 2, width: 80, type: 'Number'},
+            {title: '|应付金额', colSpan: '|1', rowSpan: '|1', field: 'yf_tp', hAlign: 2, width: 80, type: 'Number'},
+            {title: '|实付金额', colSpan: '|1', rowSpan: '|1', field: 'sf_tp', hAlign: 2, width: 80, type: 'Number'},
+            {title: '本期(不含税)|应付金额', colSpan: '2|1', rowSpan: '1|1', field: 'yf_excl_tax_tp', hAlign: 2, width: 100, type: 'Number'},
+            {title: '|实付金额', colSpan: '|1', rowSpan: '|1', field: 'sf_excl_tax_tp', hAlign: 2, width: 100, type: 'Number'},
+        ],
+        emptyRows: 0,
+        headRows: 2,
+        headRowHeight: [25, 25],
+        defaultRowHeight: 21,
+        headerFont: '12px 微软雅黑',
+        font: '12px 微软雅黑',
+        readOnly: true,
+        headColWidth: [50],
+        tipsSum: true,
+    };
+    const commonSpreadSetting = {
+        cols: [
+            {title: '编号', colSpan: '1', rowSpan: '2', field: 'code', hAlign: 0, width: 160, formatter: '@'},
+            {title: '名称', colSpan: '1', rowSpan: '2', field: 'name', hAlign: 0, width: 160, formatter: '@'},
+            {title: '税率(%)', colSpan: '1', rowSpan: '2', field: 'tax', hAlign: 2, width: 80, type: 'Number'},
+            {title: '本期|付款金额', colSpan: '4|1', rowSpan: '1|1', field: 'pay_tp', hAlign: 2, width: 80, type: 'Number'},
+            {title: '|扣款金额', colSpan: '|1', rowSpan: '|1', field: 'cut_tp', hAlign: 2, width: 80, type: 'Number'},
+            {title: '|应付金额', colSpan: '|1', rowSpan: '|1', field: 'yf_tp', hAlign: 2, width: 80, type: 'Number'},
+            {title: '|实付金额', colSpan: '|1', rowSpan: '|1', field: 'sf_tp', hAlign: 2, width: 80, type: 'Number'},
+            {title: '本期(不含税)|应付金额', colSpan: '2|1', rowSpan: '1|1', field: 'yf_excl_tax_tp', hAlign: 2, width: 100, type: 'Number'},
+            {title: '|实付金额', colSpan: '|1', rowSpan: '|1', field: 'sf_excl_tax_tp', hAlign: 2, width: 100, type: 'Number'},
+        ],
+        emptyRows: 3,
+        headRows: 2,
+        headRowHeight: [25, 25],
+        defaultRowHeight: 21,
+        headerFont: '12px 微软雅黑',
+        font: '12px 微软雅黑',
+        readOnly: true,
+        headColWidth: [50],
+        tipsSum: true,
+    };
+    <% } %>
+    <% if (ctx.costStage.stage_type === 'book') { %>
+    const bllsSpreadSetting = {
+        cols: [
+            {title: '编号', colSpan: '1', rowSpan: '1', field: 'code', hAlign: 0, width: 230, formatter: '@', cellType: 'tree'},
+            {title: '名称', colSpan: '1', rowSpan: '1', field: 'name', hAlign: 0, width: 185, formatter: '@', cellType: 'autoTip'},
+            {title: '单位', colSpan: '1', rowSpan: '1', field: 'unit', hAlign: 1, width: 50, formatter: '@', cellType: 'unit'},
+            {title: '税率(%)', colSpan: '1', rowSpan: '1', field: 'tax', hAlign: 2, width: 80, type: 'Number'},
+            {title: '入账金额', colSpan: '1', rowSpan: '1', field: 'in_tp', hAlign: 2, width: 80, type: 'Number'},
+            {title: '入账金额(不含税)', colSpan: '1', rowSpan: '1', field: 'in_excl_tax_tp', hAlign: 2, width: 100, type: 'Number'},
+        ],
+        emptyRows: 0,
+        headRows: 1,
+        headRowHeight: [25],
+        defaultRowHeight: 21,
+        headerFont: '12px 微软雅黑',
+        font: '12px 微软雅黑',
+        headColWidth: [50],
+        tipsSum: true,
+        readOnly: true,
+    };
+    const dealSpreadSetting = {
+        cols: [
+            {title: '合同编号', colSpan: '1', rowSpan: '1', field: 'code', hAlign: 0, width: 160, formatter: '@'},
+            {title: '合同名称', colSpan: '1', rowSpan: '1', field: 'name', hAlign: 0, width: 160, formatter: '@'},
+            {title: '乙方', colSpan: '1', rowSpan: '1', field: 'party_b', hAlign: 0, width: 150, formatter: '@'},
+            {title: '支付年月', colSpan: '1', rowSpan: '1', field: 'pay_date', hAlign: 1, width: 120, formatter: '@'},
+            {title: '税率(%)', colSpan: '1', rowSpan: '1', field: 'tax', hAlign: 2, width: 80, type: 'Number'},
+            {title: '入账金额', colSpan: '1', rowSpan: '1', field: 'in_tp', hAlign: 2, width: 80, type: 'Number'},
+            {title: '入账金额(不含税)', colSpan: '1', rowSpan: '1', field: 'in_excl_tax_tp', hAlign: 2, width: 100, type: 'Number'},
+        ],
+        emptyRows: 0,
+        headRows: 1,
+        headRowHeight: [25],
+        defaultRowHeight: 21,
+        headerFont: '12px 微软雅黑',
+        font: '12px 微软雅黑',
+        readOnly: true,
+        headColWidth: [50],
+        tipsSum: true,
+    };
+    const commonSpreadSetting = {
+        cols: [
+            {title: '编号', colSpan: '1', rowSpan: '1', field: 'code', hAlign: 0, width: 160, formatter: '@'},
+            {title: '名称', colSpan: '1', rowSpan: '1', field: 'name', hAlign: 0, width: 160, formatter: '@'},
+            {title: '税率(%)', colSpan: '1', rowSpan: '1', field: 'tax', hAlign: 2, width: 80, type: 'Number'},
+            {title: '入账金额', colSpan: '1', rowSpan: '1', field: 'in_tp', hAlign: 2, width: 80, type: 'Number'},
+            {title: '入账金额(不含税)', colSpan: '1', rowSpan: '1', field: 'in_excl_tax_tp', hAlign: 2, width: 100, type: 'Number'},
+        ],
+        emptyRows: 3,
+        headRows: 1,
+        headRowHeight: [25],
+        defaultRowHeight: 21,
+        headerFont: '12px 微软雅黑',
+        font: '12px 微软雅黑',
+        readOnly: true,
+        headColWidth: [50],
+        tipsSum: true,
+    };
+    <% } %>
+    <% if (ctx.costStage.stage_type === 'analysis') { %>
+    const bllsSpreadSetting = JSON.parse(unescape('<%- escape(JSON.stringify( ctx.costStage.calcTemplate.spread_cache )) %>'));
+    const dealSpreadSetting = JSON.parse(unescape('<%- escape(JSON.stringify( ctx.costStage.calcTemplate.sub_spread_cache.deal )) %>'));
+    const commonSpreadSetting = JSON.parse(unescape('<%- escape(JSON.stringify( ctx.costStage.calcTemplate.sub_spread_cache.common )) %>'));
+    <% } %>
+    const calcFields = JSON.parse('<%- JSON.stringify(calcFields) %>');
+</script>

+ 1 - 0
app/view/cost/compare_modal.ejs

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

+ 66 - 0
app/view/cost/cost_decimal.ejs

@@ -0,0 +1,66 @@
+
+<div class="modal fade show" id="cost-decimal" 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-3">
+                    <h5>小数位数</h5>
+                    <div class="row">
+                        <div class="col-6">
+                            <div class="input-group input-group-sm">
+                                <div class="input-group-prepend">
+                                    <span class="input-group-text">金额</span>
+                                </div>
+                                <input type="number" id="cost-tp" class="form-control" value="<%- ctx.costStage.decimal.tp %>" org="<%- ctx.costStage.decimal.tp %>" min="0" max="6" maxlength="1" oninput="limitDecimal(this)" <% if (ctx.costStage.readOnly) { %>disabled<% } %>>
+                            </div>
+                        </div>
+                        <div class="col-6">
+                            <div class="input-group input-group-sm">
+                                <div class="input-group-prepend">
+                                    <span class="input-group-text">金额(不含税)</span>
+                                </div>
+                                <input type="number" id="cost-excl-tax-tp" class="form-control" value="<%- ctx.costStage.decimal.excl_tax_tp %>" org="<%- ctx.costStage.decimal.excl_tax_tp %>" min="0" max="6" maxlength="1" oninput="limitDecimal(this)" <% if (ctx.costStage.readOnly) { %>disabled<% } %>>
+                            </div>
+                        </div>
+                    </div>
+                </div>
+            </div>
+            <div class="modal-footer">
+                <button type="button" class="btn btn-sm btn-secondary" data-dismiss="modal">取消</button>
+                <% if (!ctx.costStage.readOnly) { %>
+                <button type="button" class="btn btn-sm btn-primary" id="cost-decimal-ok">确认</button>
+                <% } %>
+            </div>
+        </div>
+    </div>
+</div>
+<script>
+    // 根据Min Max限制Input输入
+    function limitInputMinMax (obj) {
+        if (_.toNumber(obj.value) > _.toNumber(obj.max)) {
+            obj.value = obj.max;
+        }
+        if(_.toNumber(obj.value) < _.toNumber(obj.min)) {
+            obj.value = obj.min;
+        }
+    }
+    // 根据Maxlength限制input输入
+    function limitMaxLength (obj) {
+        if (obj.value.length > obj.maxLength) {
+            obj.value = obj.value.substr(0, obj.maxLength);
+        }
+    }
+    // 根据正则限制输入
+    function limitReg(obj, reg) {
+        obj.value = obj.value.replace(reg, '');
+    }
+    // 小数位数 input 输入限制
+    function limitDecimal(obj) {
+        limitReg(obj, /[^\d]/g);
+        limitMaxLength(obj);
+        limitInputMinMax(obj);
+    }
+</script>

+ 82 - 0
app/view/cost/duty.ejs

@@ -0,0 +1,82 @@
+<% include ./stage_memu.ejs %>
+<div class="panel-content">
+    <div class="panel-title">
+        <div class="title-main d-flex">
+            <% include ./stage_mini_menu.ejs %>
+            <div>
+                <div class="d-inline-block">
+                    <div class="dropdown">
+                        <button class="btn btn-sm btn-light dropdown-toggle text-primary" type="button" id="dropdownMenuButton" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
+                            <i class="fa fa-list-ol"></i> 显示层级
+                        </button>
+                        <div class="dropdown-menu" aria-labelledby="dropdownMenuButton">
+                            <a class="dropdown-item" name="showLevel" tag="1" href="javascript: void(0);">第一层</a>
+                            <a class="dropdown-item" name="showLevel" tag="2" href="javascript: void(0);">第二层</a>
+                            <a class="dropdown-item" name="showLevel" tag="3" href="javascript: void(0);">第三层</a>
+                            <a class="dropdown-item" name="showLevel" tag="4" href="javascript: void(0);">第四层</a>
+                            <a class="dropdown-item" name="showLevel" tag="last" href="javascript: void(0);">最底层</a>
+                        </div>
+                    </div>
+                </div>
+                <% if (ctx.costStage.create_user_id === ctx.session.sessionUser.accountId) { %>
+                <div class="d-inline-block">
+                    <a href="javascript: void(0);" name="base-opr" type="add" class="btn btn-sm btn-light text-primary" data-toggle="tooltip" data-placement="bottom" title="" data-original-title="新增"><i class="fa fa-plus" aria-hidden="true"></i></a>
+                    <a href="javascript: void(0);" name="base-opr" type="delete" class="btn btn-sm btn-light text-primary" data-toggle="tooltip" data-placement="bottom" title="" data-original-title="删除"><i class="fa fa-remove" aria-hidden="true"></i></a>
+                    <a href="javascript: void(0);" name="base-opr" type="up-level" class="btn btn-sm btn-light text-primary" data-toggle="tooltip" data-placement="bottom" title="" data-original-title="升级"><i class="fa fa-arrow-left" aria-hidden="true"></i></a>
+                    <a href="javascript: void(0);" name="base-opr" type="down-level" class="btn btn-sm btn-light text-primary" data-toggle="tooltip" data-placement="bottom" title="" data-original-title="降级"><i class="fa fa-arrow-right" aria-hidden="true"></i></a>
+                    <a href="javascript: void(0);" name="base-opr" type="down-move" class="btn btn-sm btn-light text-primary" data-toggle="tooltip" data-placement="bottom" title="" data-original-title="下移"><i class="fa fa-arrow-down" aria-hidden="true"></i></a>
+                    <a href="javascript: void(0);" name="base-opr" type="up-move" class="btn btn-sm btn-light text-primary" data-toggle="tooltip" data-placement="bottom" title="" data-original-title="上移"><i class="fa fa-arrow-up" aria-hidden="true"></i></a>
+                </div>
+                <% } %>
+            </div>
+        </div>
+    </div>
+    <div class="content-wrap row pr-46">
+        <div class="c-header p-0 col-12">
+        </div>
+        <!--核心内容(两栏)-->
+        <div class="row w-100 sub-content">
+            <!--左栏-->
+            <div class="c-body" id="left-view" style="width: 100%">
+                <div class="sjs-height-0" style="overflow: hidden" id="bills-spread">
+                </div>
+            </div>
+            <div class="c-body" id="right-view" style="display: none; width: 33%;">
+                <div class="resize-x" id="right-spr" r-Type="width" div1="#left-view" div2="#right-view" title="调整大小" a-type="percent"><!--调整左右高度条--></div>
+                <div class="tab-content">
+                    <div id="search" class="tab-pane tab-select-show">
+                    </div>
+                    <div id="extra" class="tab-pane tab-select-show">
+                        <div class="sjs-height-0" style="height: 518px;">
+                            <table class="table table-sm table-bordered">
+                                <thead>
+                                <tr class="text-center"><th>分类</th><th>金额</th></tr>
+                                </thead>
+                                <tbody id="duty-extra"></tbody>
+                            </table>
+                        </div>
+                    </div>
+                </div>
+            </div>
+        </div>
+        <!--右侧菜单-->
+        <div class="side-menu">
+            <ul class="nav flex-column right-nav" id="side-menu">
+                <li class="nav-item">
+                    <a class="nav-link" content="#search" href="javascript: void(0);">查找定位</a>
+                </li>
+                <li class="nav-item">
+                    <a class="nav-link" content="#extra" href="javascript: void(0);">附加信息</a>
+                </li>
+            </ul>
+        </div>
+    </div>
+</div>
+<script>
+    const readOnly = <%- ctx.costStage.readOnly %>;
+    const tenderId = parseInt('<%- ctx.tender.id %>');
+    const stageId = parseInt('<%- ctx.costStage.id %>');
+    const stageDate = '<%- ctx.costStage.stage_date %>';
+    const costStageComplete = <%- ctx.costStage.audit_status === auditConst.status.checked %>;
+    const colSet = JSON.parse('<%- JSON.stringify(colSet) %>');
+</script>

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

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

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

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

+ 33 - 0
app/view/cost/gather.ejs

@@ -0,0 +1,33 @@
+<% include ./list_menu.ejs %>
+<div class="panel-content">
+    <div class="panel-title">
+        <div class="title-main d-flex">
+            <% include ./list_mini_menu.ejs %>
+            <div>
+                <div class="d-inline-block">
+                    <div class="dropdown">
+                        <button class="btn btn-sm btn-light dropdown-toggle text-primary" type="button" id="dropdownMenuButton" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
+                            <i class="fa fa-list-ol"></i> 显示层级
+                        </button>
+                        <div class="dropdown-menu" aria-labelledby="dropdownMenuButton">
+                            <a class="dropdown-item" name="showLevel" tag="1" href="javascript: void(0);">第一层</a>
+                            <a class="dropdown-item" name="showLevel" tag="2" href="javascript: void(0);">第二层</a>
+                            <a class="dropdown-item" name="showLevel" tag="3" href="javascript: void(0);">第三层</a>
+                            <a class="dropdown-item" name="showLevel" tag="4" href="javascript: void(0);">第四层</a>
+                            <a class="dropdown-item" name="showLevel" tag="last" href="javascript: void(0);">最底层</a>
+                        </div>
+                    </div>
+                </div>
+            </div>
+            <div class="ml-auto">
+                <button href="#cate-set" class="btn btn-sm btn-light text-primary" data-toggle="modal" data-target="#select-qi"><i class="fa fa-clone"></i> 选择期</button>
+            </div>
+        </div>
+    </div>
+    <div class="content-wrap">
+        <div class="c-body">
+            <div class="sjs-height-0" id="ledger-gather-spread">
+            </div>
+        </div>
+    </div>
+</div>

+ 46 - 0
app/view/cost/gather_modal.ejs

@@ -0,0 +1,46 @@
+<div class="modal fade" id="select-qi" 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 scroll-y modal-height-500">
+                <table class="table table-sm" >
+                    <tr class="text-center"><th>期</th><th>报审年月</th><th width="60">选择</th></tr>
+                    <% for (const s of ledgerStages) { %>
+                    <tr stage-id="<%- s.id %>"><td><%- s.stage_order %>期</td><td><%- s.stage_date %></td><td><input type="checkbox"></td></tr>
+                    <% } %>
+                </table>
+            </div>
+            <div class="modal-body">
+                <div class="d-flex">
+                    <div class="form-check form-check-inline">汇总方式:</div>
+                    <div class="form-check form-check-inline">
+                        <input class="form-check-input" type="radio" name="gather-type" id="gather1" value="stage" checked>
+                        <label class="form-check-label" for="gather1">期</label>
+                    </div>
+                    <div class="form-check form-check-inline">
+                        <input class="form-check-input" type="radio" name="gather-type" id="gather2" value="month">
+                        <label class="form-check-label" for="gather2">月份</label>
+                    </div>
+                    <div class="form-check form-check-inline">
+                        <input class="form-check-input" type="radio" name="gather-type" id="gather3" value="quarter">
+                        <label class="form-check-label" for="gather3">季度</label>
+                    </div>
+                    <div class="form-check form-check-inline">
+                        <input class="form-check-input" type="radio" name="gather-type" id="gather4" value="year">
+                        <label class="form-check-label" for="gather4">年度</label>
+                    </div>
+                </div>
+            </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>
+        </div>
+    </div>
+</div>

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


Некоторые файлы не были показаны из-за большого количества измененных файлов