From 043251a55dc9926d2c1aca6772b7d39720fd4fe6 Mon Sep 17 00:00:00 2001 From: zhouhefeng Date: Tue, 29 Sep 2026 17:40:22 +0800 Subject: [PATCH] =?UTF-8?q?=E5=B7=A5=E4=BC=9A=E6=8A=A5=E9=94=80=E5=AF=BC?= =?UTF-8?q?=E5=87=BA=E4=BF=AE=E6=94=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/UnionReimburseExportService.java | 6 +-- .../impl/UnionReimburseExportServiceImpl.java | 40 ++++++++++++++-- .../impl/UnionReimburseServiceImpl.java | 24 +++++++--- .../unionReimburse/apply/index.html | 46 ++++++++++++++----- .../unionReimburse/apply/index.html | 36 +++++++++++---- 5 files changed, 118 insertions(+), 34 deletions(-) diff --git a/src/main/java/com/budwk/app/zhgh/dayofficework/unionReimburse/service/UnionReimburseExportService.java b/src/main/java/com/budwk/app/zhgh/dayofficework/unionReimburse/service/UnionReimburseExportService.java index 506930c1..fa63f76f 100644 --- a/src/main/java/com/budwk/app/zhgh/dayofficework/unionReimburse/service/UnionReimburseExportService.java +++ b/src/main/java/com/budwk/app/zhgh/dayofficework/unionReimburse/service/UnionReimburseExportService.java @@ -3,7 +3,7 @@ package com.budwk.app.zhgh.dayofficework.unionReimburse.service; import com.budwk.app.zhgh.dayofficework.unionReimburse.model.UnionReimburse; import java.util.Map; -/** 工会报销两类审批单共用的导出服务。 */ +/** 工会报销活动及慰问审批单共用的导出服务。 */ public interface UnionReimburseExportService { /** * @param id 当前登录人已提交的报销单ID,不接受其他人的单据或待提交单据 @@ -14,13 +14,13 @@ public interface UnionReimburseExportService { /** * 汇总页所有状态的报销单均可打印,必须具有汇总权限并符合所属工会数据范围。 * @param id 汇总页选中的报销单 ID,可为待提交或其他申请状态 - * @return 与我的报销导出使用相同模板生成的完整 PDF 字节数组,无权限或生成失败时抛出业务异常 + * @return 按慰问类型选择模板生成的完整 PDF 字节数组,活动沿用原模板;无权限或生成失败时抛出业务异常 */ byte[] exportCollectPdf(String id); /** * @param form 已加载发票明细的报销单,支付明细为空时兼容旧主表字段 - * @return 两份模板通用的平铺字段及 payments、invoices 循环行 + * @return 活动及慰问模板通用的平铺字段及 payments、invoices 循环行 */ Map buildExportData(UnionReimburse form); } diff --git a/src/main/java/com/budwk/app/zhgh/dayofficework/unionReimburse/service/impl/UnionReimburseExportServiceImpl.java b/src/main/java/com/budwk/app/zhgh/dayofficework/unionReimburse/service/impl/UnionReimburseExportServiceImpl.java index 305a57f9..6cca083e 100644 --- a/src/main/java/com/budwk/app/zhgh/dayofficework/unionReimburse/service/impl/UnionReimburseExportServiceImpl.java +++ b/src/main/java/com/budwk/app/zhgh/dayofficework/unionReimburse/service/impl/UnionReimburseExportServiceImpl.java @@ -16,6 +16,7 @@ import com.budwk.app.zhgh.dayofficework.unionReimburse.model.UnionReimburse; import com.budwk.app.zhgh.dayofficework.unionReimburse.model.UnionReimburseInvoiceDetail; import com.budwk.app.zhgh.dayofficework.unionReimburse.service.UnionReimburseExportService; import com.budwk.app.zhgh.dayofficework.unionReimburse.service.UnionReimburseService; +import com.budwk.app.zhgh.staffbenefit.condolence.model.CondolenceType; import com.deepoove.poi.XWPFTemplate; import com.deepoove.poi.config.Configure; import com.deepoove.poi.plugin.table.LoopRowTableRenderPolicy; @@ -49,6 +50,13 @@ import java.util.*; public class UnionReimburseExportServiceImpl implements UnionReimburseExportService { public static final String ACTIVITY_TEMPLATE = "union_reimburse_activity_pdf"; public static final String CONDOLENCE_TEMPLATE = "union_reimburse_condolence_pdf"; + // 使用慰问类型业务编码匹配模板,避免类型名称调整后选择错误;编码与现有申请页一致。 + private static final Map COLLECT_CONDOLENCE_TEMPLATES = Map.of( + "A", "union_reimburse_condolence_marriage_pdf", + "D", "union_reimburse_condolence_birth_pdf", + "E", "union_reimburse_condolence_hospital_pdf", + "G", "union_reimburse_condolence_member_death_pdf", + "H", "union_reimburse_condolence_relative_death_pdf"); @Inject private UnionReimburseService unionReimburseService; @Inject @@ -62,7 +70,7 @@ public class UnionReimburseExportServiceImpl implements UnionReimburseExportServ return renderDocument(loadExportForm(id, false, false), true, false); } - /** 汇总打印支持所有状态,并严格按汇总权限和所属工会范围读取,复用我的报销 PDF 模板。 */ + /** 汇总打印支持所有状态,按汇总权限和所属工会范围读取,并按慰问类型选择模板。 */ @Override public byte[] exportCollectPdf(String id) { return renderDocument(loadExportForm(id, true, true), true, true); @@ -103,10 +111,32 @@ public class UnionReimburseExportServiceImpl implements UnionReimburseExportServ return form; } - /** 两份模板共用字段和循环行配置;collectPrint 仅为汇总打印补充编号二维码。 */ + /** + * 汇总页五类慰问使用独立模板,其余类型和我的报销沿用原模板。 + * @param form 已通过数据权限校验的报销单,使用慰问类型 ID 读取业务编码 + * @param collectPrint 是否为汇总页打印,只有该入口启用分类模板 + * @return 系统导出模板管理中的模板编码;类型关联缺失时提示核对,不猜测类型名称 + */ + private String resolveTemplateCode(UnionReimburse form, boolean collectPrint) { + if (!"UNION_REIMBURSE_PROJECT_1".equals(form.getReimburseProject())) { + return ACTIVITY_TEMPLATE; + } + if (!collectPrint) { + return CONDOLENCE_TEMPLATE; + } + // 兼容旧申请将类型 ID 保存在 condolenceType 的情况,不回写历史数据。 + String typeId = StrUtil.blankToDefault(form.getCondolenceTypeId(), form.getCondolenceType()); + CondolenceType type = StrUtil.isBlank(typeId) ? null : dao.fetch(CondolenceType.class, typeId); + if (type == null || StrUtil.isBlank(type.getCode())) { + throw new BaseException("慰问类型不存在或编码为空,请核对后打印"); + } + // 五类之外的既有慰问继续使用通用模板;已匹配的专用模板缺失时由模板加载器明确报错。 + return COLLECT_CONDOLENCE_TEMPLATES.getOrDefault(type.getCode(), CONDOLENCE_TEMPLATE); + } + + /** 模板共用字段和循环行配置;汇总打印按慰问类型选模板并补充编号二维码。 */ private byte[] renderDocument(UnionReimburse form, boolean asPdf, boolean collectPrint) { - String templateCode = "UNION_REIMBURSE_PROJECT_1".equals(form.getReimburseProject()) - ? CONDOLENCE_TEMPLATE : ACTIVITY_TEMPLATE; + String templateCode = resolveTemplateCode(form, collectPrint); Path docx = null; Path pdf = null; try { @@ -114,7 +144,7 @@ public class UnionReimburseExportServiceImpl implements UnionReimburseExportServ if (asPdf) pdf = Files.createTempFile("union_reimburse_", ".pdf"); // 活动和慰问模板的支付标记均位于表头,支付明细统一循环下一行。 // 活动模板的发票标记也位于表头,循环下一行;慰问保留原有的当前行配置。 - boolean invoiceOnSameRow = CONDOLENCE_TEMPLATE.equals(templateCode); + boolean invoiceOnSameRow = "UNION_REIMBURSE_PROJECT_1".equals(form.getReimburseProject()); Configure config = Configure.builder() .bind("payments", new LoopRowTableRenderPolicy(false)) .bind("invoices", new LoopRowTableRenderPolicy(invoiceOnSameRow)).build(); diff --git a/src/main/java/com/budwk/app/zhgh/dayofficework/unionReimburse/service/impl/UnionReimburseServiceImpl.java b/src/main/java/com/budwk/app/zhgh/dayofficework/unionReimburse/service/impl/UnionReimburseServiceImpl.java index 8e47146f..41632e97 100644 --- a/src/main/java/com/budwk/app/zhgh/dayofficework/unionReimburse/service/impl/UnionReimburseServiceImpl.java +++ b/src/main/java/com/budwk/app/zhgh/dayofficework/unionReimburse/service/impl/UnionReimburseServiceImpl.java @@ -125,7 +125,7 @@ public class UnionReimburseServiceImpl extends BaseServiceImpl i /** * 保存、提交及重新提交共用支付信息校验,并在同一事务内保存申请和发票明细。 * @param unionReimburse 申请表单,paymentDetails 为支付方式、金额及对私收款资料组成的明细列表 - * @param stateId 1 表示保存草稿,2 表示提交;均要求支付信息完整、发票必填且通过服务端验真 + * @param stateId 1 表示保存草稿,2 表示提交;均要求支付信息完整,发票选填,上传后须通过服务端验真 * @return Result 成功或失败结果,msg 包含失败明细的行号及原因 */ @Override @@ -134,7 +134,7 @@ public class UnionReimburseServiceImpl extends BaseServiceImpl i if (unionReimburse == null) { return Result.error("报销数据不能为空"); } - // 保存、提交及重提统一要求发票完整,不能通过慰问类型或草稿状态跳过。 + // 发票选填;保存、提交及重提均对已上传的发票检查完整性。 Result invoiceResult = validateInvoiceDetails(unionReimburse); if (invoiceResult != null) { return invoiceResult; @@ -669,7 +669,7 @@ public class UnionReimburseServiceImpl extends BaseServiceImpl i } /** - * 所有报销类型由必填发票明细自动汇总金额和张数。 + * 有发票时汇总票面金额和张数;无发票时按已校验的支付明细汇总金额,张数为零。 */ private void fillInvoiceSummary(UnionReimburse unionReimburse) { double totalMoney = 0D; @@ -684,6 +684,14 @@ public class UnionReimburseServiceImpl extends BaseServiceImpl i invoiceFileCount++; } } + // 无发票申请从服务端已校验的支付明细计算金额,不能信任前端传入的报销总额。 + if (Lang.isEmpty(unionReimburse.getInvoiceDetails())) { + BigDecimal paymentTotal = BigDecimal.ZERO; + for (cn.hutool.json.JSONObject detail : unionReimburse.getPaymentDetails()) { + paymentTotal = paymentTotal.add(new BigDecimal(detail.get("money").toString())); + } + totalMoney = paymentTotal.setScale(2, RoundingMode.HALF_UP).doubleValue(); + } unionReimburse.setMoney(BigDecimal.valueOf(totalMoney).setScale(2, RoundingMode.HALF_UP).doubleValue()); unionReimburse.setInvoiceNumber(String.valueOf(invoiceFileCount)); } @@ -754,6 +762,10 @@ public class UnionReimburseServiceImpl extends BaseServiceImpl i */ private Result validatePaymentAmountLimit(UnionReimburse form) { List invoices = form.getInvoiceDetails(); + // 无发票时不设票面金额上限,支付明细仍须通过必填及正金额校验。 + if (Lang.isEmpty(invoices)) { + return null; + } BigDecimal invoiceTotal = BigDecimal.ZERO.setScale(2); if (invoices != null) { for (int i = 0; i < invoices.size(); i++) { @@ -821,11 +833,11 @@ public class UnionReimburseServiceImpl extends BaseServiceImpl i } /** - * 保存及提交前检查发票完整性,每条明细必须且只能包含一张可定位的发票文件。 + * 发票列表允许为空;已有明细必须且只能包含一张可定位的发票文件。 */ private Result validateInvoiceDetails(UnionReimburse unionReimburse) { if (Lang.isEmpty(unionReimburse.getInvoiceDetails())) { - return Result.error("请至少维护一条发票明细"); + return null; } for (int i = 0; i < unionReimburse.getInvoiceDetails().size(); i++) { UnionReimburseInvoiceDetail detail = unionReimburse.getInvoiceDetails().get(i); @@ -1596,7 +1608,7 @@ public class UnionReimburseServiceImpl extends BaseServiceImpl i } /** - * 区分活动类与慰问类的基本信息校验;所有类型的发票均为必填。 + * 区分活动类与慰问类的基本信息校验;所有类型的发票均为选填。 */ private boolean needInvoiceDetails(UnionReimburse unionReimburse) { return unionReimburse != null && !"UNION_REIMBURSE_PROJECT_1".equals(unionReimburse.getReimburseProject()); diff --git a/src/main/resources/views/platform/zhgh/dayofficework/unionReimburse/apply/index.html b/src/main/resources/views/platform/zhgh/dayofficework/unionReimburse/apply/index.html index 9f612138..ba139eef 100644 --- a/src/main/resources/views/platform/zhgh/dayofficework/unionReimburse/apply/index.html +++ b/src/main/resources/views/platform/zhgh/dayofficework/unionReimburse/apply/index.html @@ -438,7 +438,7 @@ layout("/layouts/platform.html"){
报销金额
- +
@@ -587,11 +587,11 @@ layout("/layouts/platform.html"){
发票明细列表
- 所有报销类型均须上传发票并通过验真;每条明细对应一张发票图片或单页 PDF,金额和张数自动汇总。 + 发票选填;无发票时按支付信息汇总报销金额。上传后须通过验真,每条明细对应一张发票图片或单页 PDF。
- 发票必须验真 + 发票选填,上传须验真 新增发票 @@ -943,6 +943,15 @@ layout("/layouts/platform.html"){ } }, watch: { + // 无发票时支付明细决定报销金额,支付行增删或金额变化后同步更新汇总。 + "formData.paymentDetails": { + handler() { + if (!(this.formData.invoiceDetails || []).length) { + this.rebuildInvoiceSummary() + } + }, + deep: true + }, "detailForm.invoiceFiles": { handler(newVal) { const file = newVal && newVal.length ? newVal[0] : null @@ -1221,7 +1230,14 @@ layout("/layouts/platform.html"){ } } this.$set(row, "historyData", accounts) - this.$set(row, "bankAccountId", selectedId) + // 新建户名的选项必须先完成渲染,再更新选中值,避免 el-select 将临时账户ID直接显示为户名。 + this.$nextTick(() => { + // 重复输入、账户请求回填或切换付款人会替换选项列表;旧回调不得覆盖最新选择。 + if (row.historyData !== accounts) { + return + } + this.$set(row, "bankAccountId", selectedId) + }) }, // 选中框只显示户名,下拉内容展示开户行和尾号,同名多卡仍按账户ID选择。 bankAccountLabel(account) { @@ -1545,17 +1561,24 @@ layout("/layouts/platform.html"){ fileCount = fileCount + 1 } } - // 所有类型都按必填发票汇总,不再使用无票慰问金额兜底。 + // 无发票时按支付明细汇总,使用整数分避免小数累加误差。 + if (!invoiceDetails.length) { + let paymentCents = 0 + const paymentDetails = this.formData.paymentDetails || [] + paymentDetails.forEach(item => { + const cents = this.moneyToCents(item && item.money) + if (cents !== null) { + paymentCents += cents + } + }) + totalMoney = paymentCents / 100 + } this.$set(this.formData, "money", totalMoney ? totalMoney.toFixed(2) : "0.00") this.$set(this.formData, "invoiceNumber", String(fileCount)) }, validateInvoiceDetailsBeforeSubmit() { const invoiceDetails = this.formData.invoiceDetails || [] - // 保存、提交及重提均要求至少一张发票,包括慰问类申请。 - if (!invoiceDetails.length) { - this.$message.warning("请至少维护一条发票明细") - return false - } + // 发票可不上传;已经添加的发票仍须填写完整并沿用验真流程。 for (let i = 0; i < invoiceDetails.length; i++) { const currentDetail = invoiceDetails[i] if (!currentDetail.invoiceFiles || currentDetail.invoiceFiles.length === 0) { @@ -1675,7 +1698,8 @@ layout("/layouts/platform.html"){ if (!Number.isSafeInteger(invoiceCents) || !Number.isSafeInteger(paymentCents)) { return "金额合计超出可处理范围,请检查填写金额" } - if (paymentCents > invoiceCents) { + // 无发票时只校验支付金额本身;有发票才限制支付合计不得超过票面总额。 + if (invoices.length && paymentCents > invoiceCents) { return "支付金额合计不能超过发票总额(支付合计:" + (paymentCents / 100).toFixed(2) + "元,发票总额:" + (invoiceCents / 100).toFixed(2) + "元)" } diff --git a/src/main/resources/views/platform/zhghh5/dayofficework/unionReimburse/apply/index.html b/src/main/resources/views/platform/zhghh5/dayofficework/unionReimburse/apply/index.html index a78124e8..92d46f97 100644 --- a/src/main/resources/views/platform/zhghh5/dayofficework/unionReimburse/apply/index.html +++ b/src/main/resources/views/platform/zhghh5/dayofficework/unionReimburse/apply/index.html @@ -761,10 +761,11 @@ layout("/layouts/platform_h5.html"){ -
电子发票
+
电子发票(选填)
+
无发票时按支付信息汇总报销金额;上传的发票仍须通过验真。
{ + const cents = this.moneyToCents(item && item.money) + if (cents !== null) { + paymentCents += cents + } + }) + totalMoney = paymentCents / 100 + } this.$set(this.formData, "money", totalMoney ? totalMoney.toFixed(2) : "0.00") this.$set(this.formData, "invoiceNumber", String(fileCount)) }, validateInvoiceDetailsBeforeSubmit() { const invoiceDetails = this.formData.invoiceDetails || [] - // 与 PC 一致:草稿、提交和重新提交均要求所有类型提供发票。 - if (!invoiceDetails.length) { - this.$toast.fail("请至少维护一条发票明细") - return false - } + // 发票可不上传;已经添加的发票仍须填写完整并沿用验真流程。 for (let i = 0; i < invoiceDetails.length; i++) { const currentDetail = invoiceDetails[i] if (!currentDetail.invoiceFiles || currentDetail.invoiceFiles.length === 0) { @@ -2167,7 +2184,8 @@ layout("/layouts/platform_h5.html"){ if (!Number.isSafeInteger(invoiceCents) || !Number.isSafeInteger(paymentCents)) { return "金额合计超出可处理范围,请检查填写金额" } - if (paymentCents > invoiceCents) { + // 无发票时只校验支付金额本身;有发票才限制支付合计不得超过票面总额。 + if (invoices.length && paymentCents > invoiceCents) { return "支付金额合计不能超过发票总额(支付合计:" + (paymentCents / 100).toFixed(2) + "元,发票总额:" + (invoiceCents / 100).toFixed(2) + "元)" }