From c1dfe9b7a132614d3417138ef3634082a29df044 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A8=8B=E8=AF=9A?= <1009578407@qq.com> Date: Fri, 16 Oct 2026 14:30:51 +0800 Subject: [PATCH] =?UTF-8?q?feat:=E5=B7=A5=E4=BC=9A=E6=8A=A5=E9=94=80?= =?UTF-8?q?=EF=BC=9A1=E3=80=81=E6=8A=A5=E9=94=80=E7=94=B3=E8=AF=B7?= =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E5=8D=8F=E4=BC=9A=E6=8A=A5=E9=94=80=EF=BC=8C?= =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E7=A4=BE=E5=9B=A2=E6=B4=BB=E5=8A=A8=EF=BC=9B?= =?UTF-8?q?2=E3=80=81=E5=8D=8F=E4=BC=9A=E6=8A=A5=E9=94=80=E5=85=B3?= =?UTF-8?q?=E8=81=94=E7=BB=8F=E8=B4=B9=E7=AE=A1=E7=90=86-=E7=A4=BE?= =?UTF-8?q?=E5=9B=A2=E9=A2=84=E7=AE=97=EF=BC=9B3=E3=80=81=E6=96=87?= =?UTF-8?q?=E4=BD=93=E6=B4=BB=E5=8A=A8=E3=80=81=E6=97=A5=E5=B8=B8=E6=B4=BB?= =?UTF-8?q?=E5=8A=A8=E6=8A=A5=E9=94=80=EF=BC=8C=E5=8E=BB=E9=99=A4=E9=99=84?= =?UTF-8?q?=E4=BB=B6=E5=BF=85=E5=A1=AB=E6=A0=A1=E9=AA=8C=EF=BC=9B4?= =?UTF-8?q?=E3=80=81=E6=B4=BB=E5=8A=A8=E5=A2=9E=E5=8A=A0=E6=8A=A5=E9=94=80?= =?UTF-8?q?=E9=87=91=E9=A2=9D=EF=BC=8C=E6=8A=A5=E9=94=80=E9=87=91=E9=A2=9D?= =?UTF-8?q?=E5=B0=8F=E4=BA=8E=E5=8F=91=E7=A5=A8=E9=87=91=E9=A2=9D=EF=BC=9B?= =?UTF-8?q?5=E3=80=81=E6=8A=A5=E9=94=80=E5=AE=A1=E6=A0=B8=E3=80=81?= =?UTF-8?q?=E6=B1=87=E6=80=BB=EF=BC=8C=E8=A1=A8=E6=A0=BC=E4=B8=AD=E5=A2=9E?= =?UTF-8?q?=E5=8A=A0=E6=B5=81=E6=B0=B4=E5=8F=B7=E5=AD=97=E6=AE=B5=EF=BC=8C?= =?UTF-8?q?=E6=89=80=E5=B1=9E=E5=8D=95=E4=BD=8D=E9=9A=90=E8=97=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../UnionReimburseApplyController.java | 11 + .../UnionReimburseCollectController.java | 13 ++ .../UnionReimburseReviewController.java | 15 ++ .../service/UnionReimburseExportService.java | 7 + .../service/UnionReimburseService.java | 9 +- .../impl/UnionReimburseExportServiceImpl.java | 10 +- .../impl/UnionReimburseServiceImpl.java | 184 ++++++++++++++--- ...union_reimburse_add_club_activity_dict.sql | 38 ++++ .../outlayManage/club/useDetail/index.html | 6 +- .../unionReimburse/apply/index.html | 195 ++++++++++++------ .../unionReimburse/collect/index.html | 4 +- .../zhgh/dayofficework/unionReimburse/info.js | 2 +- .../unionReimburse/mine/index.html | 1 + .../unionReimburse/review/index.html | 3 +- .../unionReimburse/apply/index.html | 123 ++++++++--- 15 files changed, 503 insertions(+), 118 deletions(-) create mode 100644 src/main/resources/db/union_reimburse_add_club_activity_dict.sql diff --git a/src/main/java/com/budwk/app/zhgh/dayofficework/unionReimburse/controller/UnionReimburseApplyController.java b/src/main/java/com/budwk/app/zhgh/dayofficework/unionReimburse/controller/UnionReimburseApplyController.java index fe93d84a..6829946c 100644 --- a/src/main/java/com/budwk/app/zhgh/dayofficework/unionReimburse/controller/UnionReimburseApplyController.java +++ b/src/main/java/com/budwk/app/zhgh/dayofficework/unionReimburse/controller/UnionReimburseApplyController.java @@ -313,4 +313,15 @@ public class UnionReimburseApplyController { public Result getBudgetBalance(String reimburseFundSource, String clubId) { return unionReimburseService.getBudgetBalance(reimburseFundSource, clubId, null); } + + /** + * 查询可申请社团经费报销的社团。 + * + * @return 全部未解散社团列表 + */ + @At + @SaCheckPermission(value = {"unionReimburse.apply", "h5.unionReimburse.apply"}, mode = SaMode.OR) + public Result listReimburseClubs() { + return Result.success(unionReimburseService.listReimburseClubs()); + } } diff --git a/src/main/java/com/budwk/app/zhgh/dayofficework/unionReimburse/controller/UnionReimburseCollectController.java b/src/main/java/com/budwk/app/zhgh/dayofficework/unionReimburse/controller/UnionReimburseCollectController.java index 63996dc9..01607f97 100644 --- a/src/main/java/com/budwk/app/zhgh/dayofficework/unionReimburse/controller/UnionReimburseCollectController.java +++ b/src/main/java/com/budwk/app/zhgh/dayofficework/unionReimburse/controller/UnionReimburseCollectController.java @@ -121,6 +121,16 @@ public class UnionReimburseCollectController { applyListSort(cnd, pageForm); sql.setCondition(cnd); Pagination pagination = unionReimburseService.listPageMap(pageForm.getPageNumber(), pageForm.getPageSize(), sql); + // 复用打印流水号,按当日全部申请排序,避免分页或筛选改变编号。 + for (NutMap row : pagination.getList()) { + UnionReimburse form = new UnionReimburse(); + form.setId(row.getString("id")); + // SQL映射可能返回日期字符串,兼容日期对象后再参与流水号计算。 + Object createTime = row.get("createTime"); + form.setCreateTime(createTime instanceof java.util.Date ? (java.util.Date) createTime + : (createTime == null ? null : DateUtil.parse(createTime.toString()))); + row.put("serialNo", unionReimburseExportService.buildPrintSerialNo(form)); + } return Result.success(pagination); } @@ -166,6 +176,7 @@ public class UnionReimburseCollectController { WHEN rei.reimburseProject = 'UNION_REIMBURSE_PROJECT_2' THEN '文体活动' WHEN rei.reimburseProject = 'UNION_REIMBURSE_PROJECT_3' THEN '日常活动' WHEN rei.reimburseProject = 'UNION_REIMBURSE_PROJECT_4' THEN '专项活动' + WHEN rei.reimburseProject = 'UNION_REIMBURSE_PROJECT_5' THEN '社团活动' ELSE rei.reimburseProject END as reimbursementItemName, u.mobile @@ -249,6 +260,8 @@ public class UnionReimburseCollectController { .orElse(null); if (projectDict != null) { item.setReimburseProject(projectDict.getName()); + } else if ("UNION_REIMBURSE_PROJECT_5".equals(item.getReimburseProject())) { + item.setReimburseProject("社团活动"); } } // 转换报销经费来源 diff --git a/src/main/java/com/budwk/app/zhgh/dayofficework/unionReimburse/controller/UnionReimburseReviewController.java b/src/main/java/com/budwk/app/zhgh/dayofficework/unionReimburse/controller/UnionReimburseReviewController.java index 9a223ed2..f2c7d083 100644 --- a/src/main/java/com/budwk/app/zhgh/dayofficework/unionReimburse/controller/UnionReimburseReviewController.java +++ b/src/main/java/com/budwk/app/zhgh/dayofficework/unionReimburse/controller/UnionReimburseReviewController.java @@ -2,6 +2,7 @@ package com.budwk.app.zhgh.dayofficework.unionReimburse.controller; import cn.dev33.satoken.annotation.SaCheckPermission; import cn.dev33.satoken.annotation.SaMode; +import cn.hutool.core.date.DateUtil; import cn.hutool.core.util.StrUtil; import com.budwk.app.base.annotation.SLog; import com.budwk.app.base.page.Pagination; @@ -10,6 +11,7 @@ import com.budwk.app.base.result.Result; import com.budwk.app.base.utils.PageUtil; import com.budwk.app.zhgh.dayofficework.unionReimburse.model.UnionReimburse; import com.budwk.app.zhgh.dayofficework.unionReimburse.service.UnionReimburseService; +import com.budwk.app.zhgh.dayofficework.unionReimburse.service.UnionReimburseExportService; import io.swagger.annotations.Api; import io.swagger.annotations.ApiOperation; import org.nutz.aop.interceptor.ioc.TransAop; @@ -36,6 +38,9 @@ public class UnionReimburseReviewController { @Inject private UnionReimburseService unionReimburseService; + @Inject + private UnionReimburseExportService unionReimburseExportService; + @At("/index") @Ok("beetl:/platform/zhgh/dayofficework/unionReimburse/review/index.html") @SaCheckPermission("unionReimburse.review") @@ -88,6 +93,16 @@ public class UnionReimburseReviewController { cnd.desc("info.createdAt"); sql.setCondition(cnd); Pagination pagination = unionReimburseService.listPageMap(pageForm.getPageNumber(), pageForm.getPageSize(), sql); + // 复用打印流水号,按当日全部申请排序,避免分页或筛选改变编号。 + for (NutMap row : pagination.getList()) { + UnionReimburse form = new UnionReimburse(); + form.setId(row.getString("id")); + // SQL映射可能返回日期字符串,兼容日期对象后再参与流水号计算。 + Object createTime = row.get("createTime"); + form.setCreateTime(createTime instanceof java.util.Date ? (java.util.Date) createTime + : (createTime == null ? null : DateUtil.parse(createTime.toString()))); + row.put("serialNo", unionReimburseExportService.buildPrintSerialNo(form)); + } return Result.success(pagination); } 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 8fea2773..7fc7e99a 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 @@ -24,4 +24,11 @@ public interface UnionReimburseExportService { * @return 活动及慰问模板通用的平铺字段及 payments、invoices 循环行 */ Map buildExportData(UnionReimburse form); + + /** + * 列表与打印共用按申请日期及当日申请时间排序的流水号,历史申请参与相同排序。 + * @param form 报销申请,必须包含申请时间和单据ID + * @return 年月日加至少三位当日序号,例如20261009001;缺少必要字段或申请不存在时抛出业务异常 + */ + String buildPrintSerialNo(UnionReimburse form); } diff --git a/src/main/java/com/budwk/app/zhgh/dayofficework/unionReimburse/service/UnionReimburseService.java b/src/main/java/com/budwk/app/zhgh/dayofficework/unionReimburse/service/UnionReimburseService.java index 138f5a01..2d272ba0 100644 --- a/src/main/java/com/budwk/app/zhgh/dayofficework/unionReimburse/service/UnionReimburseService.java +++ b/src/main/java/com/budwk/app/zhgh/dayofficework/unionReimburse/service/UnionReimburseService.java @@ -58,10 +58,17 @@ public interface UnionReimburseService extends BaseService { Result recognizeInvoice(String fileId, Boolean verifySwitch); /** - * 查询报销可用经费余额,同时校验是否已分配以及协会归属权限。 + * 查询报销可用经费余额,同时校验是否已分配、经费来源权限及社团有效性。 */ Result getBudgetBalance(String reimburseFundSource, String clubId, String unionId); + /** + * 查询未解散社团,供社团经费报销选择。 + * + * @return 全部未解散社团的ID、名称和编码列表 + */ + List listReimburseClubs(); + /** * 审核报销,审核通过时扣减对应预算并新增预算使用详情。 */ 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 67028ca8..a04165b0 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 @@ -185,7 +185,8 @@ public class UnionReimburseExportServiceImpl implements UnionReimburseExportServ * @param form 已通过导出权限校验的报销申请 * @return 年月日加至少三位当日序号,例如 20261009001 */ - private String buildPrintSerialNo(UnionReimburse form) { + @Override + public String buildPrintSerialNo(UnionReimburse form) { if (form.getCreateTime() == null || StrUtil.isBlank(form.getId())) { throw new BaseException("报销申请时间或单据ID为空,无法生成打印流水号"); } @@ -220,7 +221,9 @@ public class UnionReimburseExportServiceImpl implements UnionReimburseExportServ data.put("no", text(form.getDocumentNo())); data.put("serialNo", buildPrintSerialNo(form)); String unit = text(form.getUnitName()); - data.put("union", text(form.getUnionName()) + (unit.isEmpty() ? "" : "(" + unit + ")")); + data.put("union", "UNION_REIMBURSE_FUND_SOURCE_3".equals(form.getReimburseFundSource()) + ? text(form.getClubName()) + : text(form.getUnionName()) + (unit.isEmpty() ? "" : "(" + unit + ")")); data.put("handler", text(form.getUserName())); data.put("login", text(form.getLoginName())); data.put("phone", text(form.getMobile())); @@ -229,6 +232,9 @@ public class UnionReimburseExportServiceImpl implements UnionReimburseExportServ + ("UNION_REIMBURSE_PROJECT_1".equals(project) ? "√" : "□") + "慰问 " + ("UNION_REIMBURSE_PROJECT_3".equals(project) ? "√" : "□") + "日常活动 " + ("UNION_REIMBURSE_PROJECT_4".equals(project) ? "√" : "□") + "专项活动"); + if ("UNION_REIMBURSE_PROJECT_5".equals(project)) { + data.put("project", "√社团活动"); + } data.put("activity", text(form.getActivityName())); data.put("reason", text(form.getPaymentNotes())); data.put("notes", text(form.getNotes())); 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 41632e97..2137e618 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 @@ -11,7 +11,6 @@ import com.budwk.app.base.service.impl.BaseServiceImpl; import com.budwk.app.sys.models.Sys_file; import com.budwk.app.sys.views.View_user; import com.budwk.app.sys.services.SysFileService; -import com.budwk.app.zhgh.club.model.ClubUser; import com.budwk.app.zhgh.dayofficework.outlay.outlayManage.club.model.OutlayManageClub; import com.budwk.app.zhgh.dayofficework.outlay.outlayManage.model.OutlayUseDetail; import com.budwk.app.zhgh.dayofficework.outlay.outlayManage.school.model.OutlayManageSchool; @@ -148,6 +147,10 @@ public class UnionReimburseServiceImpl extends BaseServiceImpl i if (amountResult != null) { return amountResult; } + Result activityAmountResult = validateActivityApplyMoney(unionReimburse); + if (activityAmountResult != null) { + return activityAmountResult; + } UnionReimburse oldRecord = null; if (StrUtil.isNotBlank(unionReimburse.getId())) { oldRecord = this.fetch(unionReimburse.getId()); @@ -159,6 +162,18 @@ public class UnionReimburseServiceImpl extends BaseServiceImpl i } } + Result sourceResult = validateApplicantFundSource(unionReimburse); + if (sourceResult != null) { + return sourceResult; + } + if (oldRecord != null + && ("UNION_REIMBURSE_FUND_SOURCE_3".equals(oldRecord.getReimburseFundSource()) + || "UNION_REIMBURSE_FUND_SOURCE_3".equals(unionReimburse.getReimburseFundSource())) + && !Integer.valueOf(1).equals(oldRecord.getStateId()) + && !Integer.valueOf(5).equals(oldRecord.getStateId())) { + return Result.error("社团报销仅允许修改草稿或退回的申请"); + } + // 人员身份和敏感资料由服务端补齐,避免前端删减字段导致丢失或被篡改。 if (oldRecord == null) { View_user handler = dao().fetch(View_user.class, Cnd.where("id", "=", SecurityUtil.getUserId())); @@ -491,6 +506,18 @@ public class UnionReimburseServiceImpl extends BaseServiceImpl i */ @Override public Result getBudgetBalance(String reimburseFundSource, String clubId, String unionId) { + if (!canApplyFundSource(reimburseFundSource)) { + return Result.error("当前角色无权使用该经费来源"); + } + if ("UNION_REIMBURSE_FUND_SOURCE_3".equals(reimburseFundSource) + && listReimburseClubs().stream().noneMatch(club -> Objects.equals(clubId, club.getString("id")))) { + return Result.error("请选择有效的社团"); + } + return queryBudgetBalance(reimburseFundSource, clubId, unionId); + } + + // 审核人按申请单的经费归属校验预算,不要求审核人同时是该社团负责人。 + private Result queryBudgetBalance(String reimburseFundSource, String clubId, String unionId) { if (StrUtil.isBlank(reimburseFundSource)) { return Result.error("请选择经费来源"); } @@ -515,16 +542,13 @@ public class UnionReimburseServiceImpl extends BaseServiceImpl i if (StrUtil.isBlank(clubId)) { return Result.error("请选择协会"); } - List myClubIds = getCurrentUserClubIds(); - if (!isAdmin() && !myClubIds.contains(clubId)) { - return Result.error("当前用户无权使用该协会经费"); - } + OutlayManageClub outlayManageClub = dao().fetch(OutlayManageClub.class, Cnd.where("clubId", "=", clubId).and("year", "=", DateUtil.thisYear())); if (outlayManageClub == null) { return Result.error("该协会本年度未生成预算记录,请先在协会预算分配中补分配"); } - return Result.success(formatMoney(outlayManageClub.getTotalQuota().subtract(outlayManageClub.getUsedQuota()))); + return Result.success(formatMoney(safeMoney(outlayManageClub.getTotalQuota()).subtract(safeMoney(outlayManageClub.getUsedQuota())))); } return Result.error("不支持的经费来源"); } @@ -533,6 +557,7 @@ public class UnionReimburseServiceImpl extends BaseServiceImpl i * 审核通过时统一校验余额、扣减预算并写入预算使用详情,避免控制器分散业务逻辑。 */ @Override + @Aop(TransAop.READ_COMMITTED) public Result reviewApply(String reimburseId, String reviewOpinion, int submitType) { if (!List.of(3, 4, 5).contains(submitType)) { return Result.error("无效的审核操作类型"); @@ -541,6 +566,25 @@ public class UnionReimburseServiceImpl extends BaseServiceImpl i if (dbRecord == null) { return Result.error("记录不存在"); } + if ("UNION_REIMBURSE_FUND_SOURCE_3".equals(dbRecord.getReimburseFundSource())) { + // 同一申请审核串行处理,避免重复点击导致社团已使用额度累计两次。 + Sql lockSql = Sqls.create("SELECT * FROM union_reimburse WHERE id=@id FOR UPDATE"); + lockSql.setParam("id", reimburseId); + lockSql.setEntity(dao().getEntity(UnionReimburse.class)); + lockSql.setCallback(Sqls.callback.entities()); + dao().execute(lockSql); + List records = lockSql.getList(UnionReimburse.class); + if (records.isEmpty()) { + return Result.error("记录不存在"); + } + dbRecord = records.get(0); + if (submitType == 3 && Integer.valueOf(3).equals(dbRecord.getStateId())) { + return Result.success(); + } + if (!Integer.valueOf(2).equals(dbRecord.getStateId())) { + return Result.error("仅可审核待审核确认的社团报销"); + } + } if (submitType == 3) { // 已通过的单据再次触发通过时直接返回,避免重复扣减预算。 if (Integer.valueOf(3).equals(dbRecord.getStateId())) { @@ -668,8 +712,43 @@ public class UnionReimburseServiceImpl extends BaseServiceImpl i unionReimburse.setInvoiceDetails(invoiceDetails); } + private boolean isManualReimburseAmount(UnionReimburse form) { + return "UNION_REIMBURSE_PROJECT_2".equals(form.getReimburseProject()) + || "UNION_REIMBURSE_PROJECT_3".equals(form.getReimburseProject()) + || "UNION_REIMBURSE_PROJECT_4".equals(form.getReimburseProject()); + } + /** - * 有发票时汇总票面金额和张数;无发票时按已校验的支付明细汇总金额,张数为零。 + * 文体、日常、专项活动保留手填报销金额,有发票时限制票面总额,无发票不设此上限。 + * @param form 已通过发票和支付信息校验的申请 + * @return 金额必填、精度或上限校验不通过时返回错误,通过返回null + */ + private Result validateActivityApplyMoney(UnionReimburse form) { + if (!isManualReimburseAmount(form)) { + return null; + } + Double money = form.getMoney(); + if (money == null) { + return Result.error("请填写报销金额"); + } + if (!Double.isFinite(money) || money <= 0 + || BigDecimal.valueOf(money).stripTrailingZeros().scale() > 2) { + return Result.error("报销金额必须为正数,且最多两位小数"); + } + if (Lang.isNotEmpty(form.getInvoiceDetails())) { + BigDecimal invoiceTotal = BigDecimal.ZERO; + for (UnionReimburseInvoiceDetail detail : form.getInvoiceDetails()) { + invoiceTotal = invoiceTotal.add(BigDecimal.valueOf(detail.getInvoiceAmount()).setScale(2, RoundingMode.HALF_UP)); + } + if (BigDecimal.valueOf(money).compareTo(invoiceTotal) > 0) { + return Result.error("报销金额不能超过发票总额(" + invoiceTotal.toPlainString() + "元)"); + } + } + return null; + } + + /** + * 汇总票面金额和张数;三类活动保留手填金额,其余项目无发票时按支付明细汇总。 */ private void fillInvoiceSummary(UnionReimburse unionReimburse) { double totalMoney = 0D; @@ -692,7 +771,10 @@ public class UnionReimburseServiceImpl extends BaseServiceImpl i } totalMoney = paymentTotal.setScale(2, RoundingMode.HALF_UP).doubleValue(); } - unionReimburse.setMoney(BigDecimal.valueOf(totalMoney).setScale(2, RoundingMode.HALF_UP).doubleValue()); + // 三类活动使用已校验的手填申请金额,不能被发票或支付合计覆盖。 + if (!isManualReimburseAmount(unionReimburse)) { + unionReimburse.setMoney(BigDecimal.valueOf(totalMoney).setScale(2, RoundingMode.HALF_UP).doubleValue()); + } unionReimburse.setInvoiceNumber(String.valueOf(invoiceFileCount)); } @@ -826,7 +908,11 @@ public class UnionReimburseServiceImpl extends BaseServiceImpl i if (unionReimburse.getActivityTime() == null) { return Result.error("请选择活动时间"); } - if (Lang.isEmpty(unionReimburse.getFiles())) { + // 文体、日常和社团活动附件选填,其他活动保留附件必填校验。 + if (!"UNION_REIMBURSE_PROJECT_2".equals(unionReimburse.getReimburseProject()) + && !"UNION_REIMBURSE_PROJECT_3".equals(unionReimburse.getReimburseProject()) + && !"UNION_REIMBURSE_PROJECT_5".equals(unionReimburse.getReimburseProject()) + && Lang.isEmpty(unionReimburse.getFiles())) { return Result.error("请上传附件"); } return validateInvoiceDetails(unionReimburse); @@ -929,7 +1015,7 @@ public class UnionReimburseServiceImpl extends BaseServiceImpl i * 提交和审核前统一校验预算是否已分配、余额是否充足。 */ private Result validateFundSourceBeforeSubmit(UnionReimburse unionReimburse) { - Result balanceResult = getBudgetBalance(unionReimburse.getReimburseFundSource(), unionReimburse.getClubId(), unionReimburse.getUnionId()); + Result balanceResult = queryBudgetBalance(unionReimburse.getReimburseFundSource(), unionReimburse.getClubId(), unionReimburse.getUnionId()); if (balanceResult == null || balanceResult.getCode() != 0) { return balanceResult == null ? Result.error("经费余额校验失败") : balanceResult; } @@ -985,11 +1071,19 @@ public class UnionReimburseServiceImpl extends BaseServiceImpl i if (outlayManageClub == null) { return Result.error("该协会本年度未生成预算记录,请先在协会预算分配中补分配"); } - if (outlayManageClub.getTotalQuota().subtract(outlayManageClub.getUsedQuota()).compareTo(applyMoney) < 0) { - return Result.error("协会经费余额不足"); + // 本年度分配总额度保持不变,审批金额原子累加至已使用额度,余额由两者相减得到。 + Sql deductSql = Sqls.create(""" + UPDATE outlay_manage_club + SET usedQuota = COALESCE(usedQuota, 0) + @amount + WHERE id = @id + AND COALESCE(totalQuota, 0) - COALESCE(usedQuota, 0) >= @amount + """); + deductSql.setParam("id", outlayManageClub.getId()); + deductSql.setParam("amount", applyMoney); + dao().execute(deductSql); + if (deductSql.getUpdateCount() != 1) { + return Result.error("社团经费余额不足"); } - outlayManageClub.setUsedQuota(outlayManageClub.getUsedQuota().add(applyMoney)); - dao().updateIgnoreNull(outlayManageClub); insertUseDetail(unionReimburse, outlayManageClub.getId(), applyMoney); return Result.success(); } @@ -1201,6 +1295,9 @@ public class UnionReimburseServiceImpl extends BaseServiceImpl i if ("UNION_REIMBURSE_PROJECT_4".equals(reimburseProject)) { return "专项活动申请"; } + if ("UNION_REIMBURSE_PROJECT_5".equals(reimburseProject)) { + return "社团活动申请"; + } return "报销申请"; } @@ -1208,16 +1305,57 @@ public class UnionReimburseServiceImpl extends BaseServiceImpl i return money == null ? 0D : money.setScale(2, RoundingMode.HALF_UP).doubleValue(); } - private List getCurrentUserClubIds() { - if (isAdmin()) { - return new ArrayList<>(); + /** + * 查询未解散社团,社团选择和余额查询不按负责人归属过滤。 + * + * @return 全部未解散社团的ID、名称和编码 + */ + @Override + public List listReimburseClubs() { + Sql sql = Sqls.create(""" + SELECT c.id, c.clubName, c.clubCode + FROM sys_club c + WHERE c.dismiss = 0 + ORDER BY c.clubCode, c.id + """); + return listMap(sql); + } + + // 社团经费按菜单权限授权,其他经费保留原申请角色判断。 + private boolean canApplyFundSource(String source) { + if (StrUtil.isBlank(source)) { + return false; } - List clubUsers = dao().query(ClubUser.class, Cnd.where("userId", "=", SecurityUtil.getUserId())); - return clubUsers.stream() - .map(ClubUser::getClubId) - .filter(StrUtil::isNotBlank) - .distinct() - .collect(Collectors.toList()); + boolean branchRole = AuthUtil.hasRoleOr(RoleConstant.BRANCH_UNION_CHAIRMAN.name(), RoleConstant.BRANCH_UNION_ADMIN.name()); + boolean clubRole = AuthUtil.hasRoleOr(RoleConstant.CLUB_MANAGER.name(), RoleConstant.CLUB_PRESIDENT.name()); + return switch (source) { + case "UNION_REIMBURSE_FUND_SOURCE_1" -> isAdmin() + || (!branchRole && !clubRole && AuthUtil.hasRoleOr(RoleConstant.SCHOOL_OUTLAY_ADMIN.name())); + case "UNION_REIMBURSE_FUND_SOURCE_2" -> isAdmin() || branchRole; + case "UNION_REIMBURSE_FUND_SOURCE_3" -> AuthUtil.hasPermission("unionReimburse.club"); + default -> false; + }; + } + + private Result validateApplicantFundSource(UnionReimburse form) { + if (!canApplyFundSource(form.getReimburseFundSource())) { + return Result.error("当前角色无权使用该经费来源"); + } + if ("UNION_REIMBURSE_FUND_SOURCE_3".equals(form.getReimburseFundSource())) { + if (!"UNION_REIMBURSE_PROJECT_5".equals(form.getReimburseProject())) { + return Result.error("社团经费请选择社团活动"); + } + NutMap club = listReimburseClubs().stream() + .filter(item -> Objects.equals(form.getClubId(), item.getString("id"))) + .findFirst().orElse(null); + if (club == null) { + return Result.error("请选择有效的社团"); + } + form.setClubName(club.getString("clubName")); + } else if ("UNION_REIMBURSE_PROJECT_5".equals(form.getReimburseProject())) { + return Result.error("社团活动只能使用社团经费"); + } + return null; } private Result validateInvoiceDuplicate(UnionReimburse unionReimburse) { diff --git a/src/main/resources/db/union_reimburse_add_club_activity_dict.sql b/src/main/resources/db/union_reimburse_add_club_activity_dict.sql new file mode 100644 index 00000000..f488944d --- /dev/null +++ b/src/main/resources/db/union_reimburse_add_club_activity_dict.sql @@ -0,0 +1,38 @@ +-- 社团活动属于报销项目的正式字典配置,两端沿用现有字典接口读取。 +-- 仅补充缺失记录;已有记录(包括已禁用记录)不覆盖,不在查询接口中添加兜底项。 +INSERT INTO sys_dict ( + id, parentId, path, name, remark, code, disabled, location, hasChildren, + createdBy, createdAt, updatedBy, updatedAt, delFlag +) +SELECT + REPLACE(UUID(), '-', ''), + parent.id, + CONCAT(parent.path, LPAD(IFNULL(( + SELECT MAX(CAST(RIGHT(child.path, 4) AS UNSIGNED)) + FROM sys_dict child + WHERE child.path LIKE CONCAT(parent.path, '____') + ), 0) + 1, 4, '0')), + '社团活动', + '', + 'UNION_REIMBURSE_PROJECT_5', + 0, + IFNULL((SELECT MAX(child.location) FROM sys_dict child WHERE child.parentId = parent.id), 0) + 1, + 0, + '', UNIX_TIMESTAMP(NOW()) * 1000, + '', UNIX_TIMESTAMP(NOW()) * 1000, + 0 +FROM sys_dict parent +WHERE parent.code = 'UNION_REIMBURSE_PROJECT' + AND NOT EXISTS ( + SELECT 1 + FROM (SELECT code FROM sys_dict WHERE code = 'UNION_REIMBURSE_PROJECT_5') existing_item + ); + +UPDATE sys_dict parent +SET parent.hasChildren = 1 +WHERE parent.code = 'UNION_REIMBURSE_PROJECT' + AND EXISTS ( + SELECT 1 + FROM (SELECT parentId FROM sys_dict WHERE code = 'UNION_REIMBURSE_PROJECT_5') club_activity + WHERE club_activity.parentId = parent.id + ); diff --git a/src/main/resources/views/platform/zhgh/dayofficework/outlay/outlayManage/club/useDetail/index.html b/src/main/resources/views/platform/zhgh/dayofficework/outlay/outlayManage/club/useDetail/index.html index 7ac7e60e..be0784d4 100644 --- a/src/main/resources/views/platform/zhgh/dayofficework/outlay/outlayManage/club/useDetail/index.html +++ b/src/main/resources/views/platform/zhgh/dayofficework/outlay/outlayManage/club/useDetail/index.html @@ -57,8 +57,10 @@ layout("/layouts/platform.html"){ min-width="50" > + 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 ba139eef..445e570f 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 @@ -7,6 +7,11 @@ layout("/layouts/platform.html"){ margin-top: 10px; } + /* 社团表单改为两组字段后,左侧标签仍保持原三组字段表单的六分之一宽度。 */ + .club-reimbursement-form .el-descriptions-item__label:first-child { + width: calc(100% / 6); + } + .invoice-toolbar { display: flex; align-items: center; @@ -60,17 +65,21 @@ layout("/layouts/platform.html"){ 日常活动申请

专项活动申请

+

+ 社团活动申请

- - + + + + + @input="selectFundSource"> 校工会经费 @@ -88,11 +97,11 @@ layout("/layouts/platform.html"){ - + - + {{item.name}} @@ -104,24 +113,6 @@ layout("/layouts/platform.html"){ {{formData.userName}}({{formData.loginName}}) - - - - - - - - - @@ -149,7 +140,7 @@ layout("/layouts/platform.html"){ placeholder="请输入联系方式"> - + @@ -273,7 +264,34 @@ layout("/layouts/platform.html"){ - + + + + + + + + + + + + + + + + + + @@ -399,7 +417,7 @@ layout("/layouts/platform.html"){ - + @@ -438,7 +456,7 @@ layout("/layouts/platform.html"){
报销金额
- +
@@ -587,7 +605,7 @@ layout("/layouts/platform.html"){
发票明细列表
- 发票选填;无发票时按支付信息汇总报销金额。上传后须通过验真,每条明细对应一张发票图片或单页 PDF。 + {{ isManualReimburseAmount ? '发票选填;有发票时报销金额不得超过发票总额,无发票仍可提交。' : '发票选填;无发票时按支付信息汇总报销金额。' }}上传后须通过验真,每条明细对应一张发票图片或单页 PDF。
@@ -821,7 +839,13 @@ layout("/layouts/platform.html"){ // invoiceNumber: [{required: true, message: "请填写发票张数", trigger: ["change", "blur"]}], // invoice: [{required: true, message: "请填写发票号码", trigger: ["change", "blur"]}], activityName: [{required: true, message: "请填写活动名称", trigger: ["change", "blur"]}], - money: [{required: true, message: "请填写报销金额", trigger: ["change", "blur"]}], + money: [{required: true, message: "请填写报销金额", trigger: ["change", "blur"]}, { + validator: (rule, value, callback) => { + const message = this.getReimburseAmountMessage() + callback(message ? new Error(message) : undefined) + }, + trigger: ["change", "blur"] + }], condolenceRelationship: [{required: true, message: "与被慰问人关系", trigger: ["change", "blur"]}], marryTime: [{required: true, message: "结婚时间", trigger: ["change", "blur"]}], fertilityTime: [{required: true, message: "生育时间", trigger: ["change", "blur"]}], @@ -835,8 +859,8 @@ layout("/layouts/platform.html"){ ], files: [{ validator: (rule, value, callback) => { - // 慰问项目不展示附件上传,提交时无需校验附件。 - if (this.formData.reimburseProject === "UNION_REIMBURSE_PROJECT_1") { + // 慰问项目无需校验附件,文体、日常和社团活动附件选填。 + if (["UNION_REIMBURSE_PROJECT_1", "UNION_REIMBURSE_PROJECT_2", "UNION_REIMBURSE_PROJECT_3", "UNION_REIMBURSE_PROJECT_5"].includes(this.formData.reimburseProject)) { callback() return } @@ -910,6 +934,16 @@ layout("/layouts/platform.html"){ } }, computed: { + isManualReimburseAmount() { + return ["UNION_REIMBURSE_PROJECT_2", "UNION_REIMBURSE_PROJECT_3", "UNION_REIMBURSE_PROJECT_4"] + .includes(this.formData.reimburseProject) + }, + // 社团经费固定对应社团活动,复用文体活动的基本信息和发票表单。 + availableReimburseProjects() { + const clubFund = this.formData.reimburseFundSource === "UNION_REIMBURSE_FUND_SOURCE_3" + return this.reimburseProjectList.filter(item => + clubFund ? item.code === "UNION_REIMBURSE_PROJECT_5" : item.code !== "UNION_REIMBURSE_PROJECT_5") + }, // 按选中类型的业务编码控制字段,类型列表异步加载及旧申请回显后自动重新计算。 condolenceTypeCode() { if (this.formData.reimburseProject !== "UNION_REIMBURSE_PROJECT_1") return "" @@ -917,9 +951,11 @@ layout("/layouts/platform.html"){ const selectedType = this.typeOptions.find(item => item.id === typeId) return selectedType ? selectedType.code : "" }, - // 仅系统管理员和校工会管理员可查看真实余额,其他账号统一显示余额遮罩。 + // 社团经费按菜单授权查看年度剩余额度,其他经费沿用原余额可见范围。 canViewFundBalance() { return this.$auth.hasRoleOr(["SYSADMIN", "SCHOOL_UNION_ADMIN"]) + || (this.formData.reimburseFundSource === "UNION_REIMBURSE_FUND_SOURCE_3" + && this.$auth.hasPermission("unionReimburse.club")) }, showInvoiceTab() { return !!this.formData.reimburseProject @@ -1549,6 +1585,27 @@ layout("/layouts/platform.html"){ this.rebuildInvoiceSummary() }) }, + // 手填金额按分比较票面总额;未上传发票时仍允许申请。 + getReimburseAmountMessage() { + if (!this.isManualReimburseAmount) return "" + const value = String(this.formData.money == null ? "" : this.formData.money) + if (!value) return "请填写报销金额" + const cents = this.moneyToCents(value) + if (!/^\d+(\.\d{1,2})?$/.test(value) || cents === null || cents <= 0) { + return "报销金额必须为正数,且最多两位小数" + } + const invoices = this.formData.invoiceDetails || [] + let invoiceCents = 0 + for (let i = 0; i < invoices.length; i++) { + const amount = this.moneyToCents(invoices[i] && invoices[i].invoiceAmount) + if (amount === null || amount <= 0) return "请填写正确的发票金额" + invoiceCents += amount + } + if (invoices.length && cents > invoiceCents) { + return "报销金额不能超过发票总额(" + (invoiceCents / 100).toFixed(2) + "元)" + } + return "" + }, rebuildInvoiceSummary() { let totalMoney = 0 let fileCount = 0 @@ -1573,10 +1630,18 @@ layout("/layouts/platform.html"){ }) totalMoney = paymentCents / 100 } - this.$set(this.formData, "money", totalMoney ? totalMoney.toFixed(2) : "0.00") + // 三类活动保留手填申请金额,发票及支付明细变化只更新张数。 + if (!this.isManualReimburseAmount) { + this.$set(this.formData, "money", totalMoney ? totalMoney.toFixed(2) : "0.00") + } this.$set(this.formData, "invoiceNumber", String(fileCount)) }, validateInvoiceDetailsBeforeSubmit() { + const amountMessage = this.getReimburseAmountMessage() + if (amountMessage) { + this.$message.warning(amountMessage) + return false + } const invoiceDetails = this.formData.invoiceDetails || [] // 发票可不上传;已经添加的发票仍须填写完整并沿用验真流程。 for (let i = 0; i < invoiceDetails.length; i++) { @@ -1904,6 +1969,7 @@ layout("/layouts/platform.html"){ }, // 根据报销项目获取对应的附件说明 getFileDescByProject(projectCode) { + if (projectCode === "UNION_REIMBURSE_PROJECT_5") return "社团活动附件说明" if (!this.descOptions || !Array.isArray(this.descOptions)) { return ''; } @@ -1912,27 +1978,27 @@ layout("/layouts/platform.html"){ }, // 根据当前选择的报销项目获取管理端配置的提示内容。 getProjectTipByProject(projectCode) { + if (projectCode === "UNION_REIMBURSE_PROJECT_5") projectCode = "UNION_REIMBURSE_PROJECT_2" if (!this.descOptions || !Array.isArray(this.descOptions)) { return ''; } const project = this.descOptions.find(item => item.reimburseProject === projectCode); return project ? project.projectTip : ''; }, - // 新建申请默认优先选择分工会经费;若当前用户没有对应权限,则按页面可见权限回退到其它经费来源。 + // 默认来源与角色允许的按钮一致,社团负责人默认进入社团活动申请。 getDefaultFundSource() { - if (this.$auth.hasPermission("unionReimburse.fgh")) { - return "UNION_REIMBURSE_FUND_SOURCE_2" - } - if (this.$auth.hasPermission("unionReimburse.xgh")) { - return "UNION_REIMBURSE_FUND_SOURCE_1" - } - if (this.$auth.hasPermission("unionReimburse.club")) { - return "UNION_REIMBURSE_FUND_SOURCE_3" - } - return "" + const option = this.budgetTypeOption.find(item => item.code === "UNION_REIMBURSE_FUND_SOURCE_2") + || this.budgetTypeOption.find(item => item.code === "UNION_REIMBURSE_FUND_SOURCE_3") + || (this.$auth.hasPermission("unionReimburse.xgh") && this.budgetTypeOption[0]) + return option ? option.code : "" }, selectFundSource(code) { this.$set(this.formData, "reimburseFundSource", code) + if (code === "UNION_REIMBURSE_FUND_SOURCE_3") { + this.$set(this.formData, "reimburseProject", "UNION_REIMBURSE_PROJECT_5") + } else if (this.formData.reimburseProject === "UNION_REIMBURSE_PROJECT_5") { + this.$set(this.formData, "reimburseProject", "UNION_REIMBURSE_PROJECT_2") + } if (code !== "UNION_REIMBURSE_FUND_SOURCE_3") { this.$set(this.formData, "clubId", null) this.$set(this.formData, "clubName", null) @@ -1986,9 +2052,13 @@ layout("/layouts/platform.html"){ this.fundBalanceText = "查询中..." this.$axios.post("/platform/unionReimburse/apply/getBudgetBalance", params) .then(response => { + // 切换社团后忽略旧请求,避免显示上一社团的余额。 + if (fundSource !== this.formData.reimburseFundSource + || (params.clubId && params.clubId !== this.formData.clubId)) return if (response.code === 0) { this.$set(this.formData, "fundBalance", response.data || 0) - this.fundBalanceText = (response.data || 0).toString() + this.fundBalanceText = fundSource === "UNION_REIMBURSE_FUND_SOURCE_3" + ? Number(response.data || 0).toFixed(2) : (response.data || 0).toString() } else { this.$set(this.formData, "fundBalance", null) this.fundBalanceText = response.msg || "查询失败" @@ -1996,6 +2066,8 @@ layout("/layouts/platform.html"){ } }) .catch(error => { + if (fundSource !== this.formData.reimburseFundSource + || (params.clubId && params.clubId !== this.formData.clubId)) return console.error("经费余额查询失败:", error) this.$set(this.formData, "fundBalance", null) this.fundBalanceText = "查询失败" @@ -2016,7 +2088,8 @@ layout("/layouts/platform.html"){ this.formData.hospitalizationEndTime ]); } - if (this.$auth.hasRoleOr(["SCHOOL_OUTLAY_ADMIN", "SCHOOL_UNION_ADMIN"])) { + if (this.$auth.hasRoleOr(["SCHOOL_OUTLAY_ADMIN", "SCHOOL_UNION_ADMIN"]) + && !this.$auth.hasRoleOr(["BRANCH_UNION_CHAIRMAN", "BRANCH_UNION_ADMIN", "CLUB_MANAGER", "CLUB_PRESIDENT"])) { this.budgetTypeOption.map(v => { if (["UNION_REIMBURSE_FUND_SOURCE_1"].includes(v.code)) { budgetTypeOption.push(v) @@ -2030,7 +2103,7 @@ layout("/layouts/platform.html"){ } }) } - if (this.$auth.hasRoleOr(["CLUB_MANAGER", "CLUB_PRESIDENT"])) { + if (this.$auth.hasPermission("unionReimburse.club")) { this.budgetTypeOption.map(v => { if (["UNION_REIMBURSE_FUND_SOURCE_3"].includes(v.code)) { budgetTypeOption.push(v) @@ -2039,11 +2112,22 @@ layout("/layouts/platform.html"){ } this.budgetTypeOption = budgetTypeOption } + // 社团经费按菜单权限过滤,默认来源与按钮可见范围保持一致。 + this.budgetTypeOption = this.budgetTypeOption.filter(item => + item.code !== "UNION_REIMBURSE_FUND_SOURCE_3" + || this.$auth.hasPermission("unionReimburse.club")) this.reimburseProjectList = await this.$businessTool.getDictOptions("UNION_REIMBURSE_PROJECT") + if (this.budgetTypeOption.some(item => item.code === "UNION_REIMBURSE_FUND_SOURCE_3")) { + const clubs = await this.$axios.post("/platform/unionReimburse/apply/listReimburseClubs") + this.clubOptions = clubs.data || [] + } if (this.bizId) { this.$axios.post("/platform/unionReimburse/apply/info", {id: this.bizId}).then(async (res) => { if (res.code === 0) { this.formData = this.ensureFormData(res.data) + if (this.formData.reimburseFundSource === "UNION_REIMBURSE_FUND_SOURCE_3") { + this.$set(this.formData, "reimburseProject", "UNION_REIMBURSE_PROJECT_5") + } this.appendCurrentPayerOption() this.formData.paymentDetails.forEach(row => { if (row.payer) { @@ -2066,7 +2150,8 @@ layout("/layouts/platform.html"){ unionId: union?.id, unionName: union?.name, // reimburseType: "UNION_REIMBURSE_TYPE_1", - reimburseProject: 'UNION_REIMBURSE_PROJECT_1', + reimburseProject: this.getDefaultFundSource() === "UNION_REIMBURSE_FUND_SOURCE_3" + ? "UNION_REIMBURSE_PROJECT_5" : "UNION_REIMBURSE_PROJECT_1", reimburseFundSource: this.getDefaultFundSource(), paymentWay: 'UNION_REIMBURSE_PAYMENT_WAY_2', paymentDetails: [this.buildDefaultPaymentDetail({ @@ -2076,7 +2161,7 @@ layout("/layouts/platform.html"){ userId: id, }) if (this.formData.reimburseFundSource) { - this.reimburseFundSourceChange() + this.selectFundSource(this.formData.reimburseFundSource) } } } @@ -2085,17 +2170,7 @@ layout("/layouts/platform.html"){ this.init() this.queryFileDesc() this.queryCondolenceType() - //社团查询 - this.$businessTool.listClubByRole().then((res) => { - this.$set(this, "clubOptions", res || []) - if (res && res.length === 1 && !this.formData.clubId) { - this.$set(this.formData, "clubId", res[0].id) - this.$set(this.formData, "clubName", res[0].clubName) - } - if (this.formData.reimburseFundSource) { - this.reimburseFundSourceChange() - } - }) + } }) diff --git a/src/main/resources/views/platform/zhgh/dayofficework/unionReimburse/collect/index.html b/src/main/resources/views/platform/zhgh/dayofficework/unionReimburse/collect/index.html index f34e924d..1d244e97 100644 --- a/src/main/resources/views/platform/zhgh/dayofficework/unionReimburse/collect/index.html +++ b/src/main/resources/views/platform/zhgh/dayofficework/unionReimburse/collect/index.html @@ -51,10 +51,10 @@ layout("/layouts/platform.html"){ + - - +

附件信息

- + + + + @@ -762,10 +774,10 @@ layout("/layouts/platform_h5.html"){
电子发票(选填)
-
无发票时按支付信息汇总报销金额;上传的发票仍须通过验真。
+
{{ isManualReimburseAmount ? '有发票时报销金额不得超过发票总额,无发票仍可提交;上传的发票仍须通过验真。' : '无发票时按支付信息汇总报销金额;上传的发票仍须通过验真。' }}
+ + + item.id === typeId) return selectedType ? selectedType.code : "" }, - // 仅系统管理员和校工会管理员可查看真实余额,其他账号统一显示余额遮罩。 + // 社团经费按菜单授权查看年度剩余额度,其他经费沿用原余额可见范围。 canViewFundBalance() { return this.$auth.hasRoleOr(["SYSADMIN", "SCHOOL_UNION_ADMIN"]) + || (this.formData.reimburseFundSource === "UNION_REIMBURSE_FUND_SOURCE_3" + && this.$auth.hasPermission("unionReimburse.club")) + }, + // 社团经费仅可选择社团活动,其他经费不展示社团活动。 + availableReimburseProjectColumns() { + const clubFund = this.formData.reimburseFundSource === "UNION_REIMBURSE_FUND_SOURCE_3" + return this.reimburseProjectColumns.filter(item => + clubFund ? item.value === "UNION_REIMBURSE_PROJECT_5" : item.value !== "UNION_REIMBURSE_PROJECT_5") }, paymentInfoRows() { return this.formData.paymentDetails || [] @@ -1233,6 +1262,9 @@ layout("/layouts/platform_h5.html"){ }, "formData.reimburseProject"(newVal) { this.handleReimburseProjectChange(newVal) + }, + "formData.reimburseFundSource"() { + this.syncReimburseProject() } }, @@ -1852,6 +1884,27 @@ layout("/layouts/platform_h5.html"){ this.rebuildInvoiceSummary() }) }, + // 手填金额按分比较票面总额;未上传发票时仍允许申请。 + getReimburseAmountMessage() { + if (!this.isManualReimburseAmount) return "" + const value = String(this.formData.money == null ? "" : this.formData.money) + if (!value) return "请填写报销金额" + const cents = this.moneyToCents(value) + if (!/^\d+(\.\d{1,2})?$/.test(value) || cents === null || cents <= 0) { + return "报销金额必须为正数,且最多两位小数" + } + const invoices = this.formData.invoiceDetails || [] + let invoiceCents = 0 + for (let i = 0; i < invoices.length; i++) { + const amount = this.moneyToCents(invoices[i] && invoices[i].invoiceAmount) + if (amount === null || amount <= 0) return "请填写正确的发票金额" + invoiceCents += amount + } + if (invoices.length && cents > invoiceCents) { + return "报销金额不能超过发票总额(" + (invoiceCents / 100).toFixed(2) + "元)" + } + return "" + }, rebuildInvoiceSummary() { let totalMoney = 0 let fileCount = 0 @@ -1876,10 +1929,18 @@ layout("/layouts/platform_h5.html"){ }) totalMoney = paymentCents / 100 } - this.$set(this.formData, "money", totalMoney ? totalMoney.toFixed(2) : "0.00") + // 三类活动保留手填申请金额,发票及支付明细变化只更新张数。 + if (!this.isManualReimburseAmount) { + this.$set(this.formData, "money", totalMoney ? totalMoney.toFixed(2) : "0.00") + } this.$set(this.formData, "invoiceNumber", String(fileCount)) }, validateInvoiceDetailsBeforeSubmit() { + const amountMessage = this.getReimburseAmountMessage() + if (amountMessage) { + this.$toast.fail(amountMessage) + return false + } const invoiceDetails = this.formData.invoiceDetails || [] // 发票可不上传;已经添加的发票仍须填写完整并沿用验真流程。 for (let i = 0; i < invoiceDetails.length; i++) { @@ -2366,6 +2427,15 @@ layout("/layouts/platform_h5.html"){ } this.closePopup("showPaymentWayPicker") }, + // 新建、切换来源及编辑回显都保持社团经费与社团活动一致。 + syncReimburseProject() { + const clubFund = this.formData.reimburseFundSource === "UNION_REIMBURSE_FUND_SOURCE_3" + if (!clubFund && this.formData.reimburseProject !== "UNION_REIMBURSE_PROJECT_5") return + const projectCode = clubFund ? "UNION_REIMBURSE_PROJECT_5" : "UNION_REIMBURSE_PROJECT_2" + const project = this.reimburseProjectColumns.find(item => item.value === projectCode) + this.$set(this.formData, "reimburseProject", project ? project.value : "") + this.$set(this.formData, "reimburseProjectName", project ? project.text : "") + }, onReimburseProjectConfirm(o) { this.$set(this.formData, "reimburseProjectName", o.text) // 显示名称 this.$set(this.formData, "reimburseProject", o.value) // 字典值 @@ -2441,16 +2511,10 @@ layout("/layouts/platform_h5.html"){ }, // 新建申请默认优先选择分工会经费;若当前用户没有对应权限,则按页面可见权限回退到其它经费来源。 getDefaultFundSource() { - if (this.$auth.hasPermission("unionReimburse.fgh")) { - return "UNION_REIMBURSE_FUND_SOURCE_2" - } - if (this.$auth.hasPermission("unionReimburse.xgh")) { - return "UNION_REIMBURSE_FUND_SOURCE_1" - } - if (this.$auth.hasPermission("unionReimburse.club")) { - return "UNION_REIMBURSE_FUND_SOURCE_3" - } - return "" + const option = this.reimburseFundSourceColumns.find(item => item.value === "UNION_REIMBURSE_FUND_SOURCE_2") + || this.reimburseFundSourceColumns.find(item => item.value === "UNION_REIMBURSE_FUND_SOURCE_3") + || this.reimburseFundSourceColumns[0] + return option ? option.value : "" }, getReimburseFundSourceName(code) { const matchedFundSource = this.reimburseFundSourceColumns.find(item => item.value === code) @@ -2467,6 +2531,7 @@ layout("/layouts/platform_h5.html"){ }, // 根据当前选择的报销项目获取管理端配置的提示内容。 getProjectTipByProject(projectCode) { + if (projectCode === "UNION_REIMBURSE_PROJECT_5") projectCode = "UNION_REIMBURSE_PROJECT_2" if (!this.descOptions || !Array.isArray(this.descOptions)) { return "" } @@ -2485,7 +2550,7 @@ layout("/layouts/platform_h5.html"){ this.$set(this, "reimburseProjectList", reimburseProjectData) return this.$businessTool.getDictOptions("UNION_REIMBURSE_FUND_SOURCE") }).then((reimburseFundSourceData) => { - this.$set(this, "reimburseFundSourceColumns", reimburseFundSourceData.map(item => ({value: item.code, text: item.name}))) + this.$set(this, "reimburseFundSourceColumns", reimburseFundSourceData.map(item => ({value: item.code, text: item.code === "UNION_REIMBURSE_FUND_SOURCE_3" ? "协会经费" : item.name}))) this.filterReimburseFundSources() }) }, @@ -2567,7 +2632,8 @@ layout("/layouts/platform_h5.html"){ // 查询社团 queryClubs() { - return this.$businessTool.listClubByRole().then((res) => { + return this.$axios.post("/platform/unionReimburse/apply/listReimburseClubs").then((response) => { + const res = response.data || [] this.$set(this, "clubOptions", res) this.$set(this, "clubColumns", res.map(item => { return { text: item.clubName, value: item.id } @@ -2601,7 +2667,7 @@ layout("/layouts/platform_h5.html"){ url: "/platform/unionReimburse/apply/getUnionBalance" }, "UNION_REIMBURSE_FUND_SOURCE_3": { - url: "/platform/unionReimburse/apply/getClubBudget" + url: "/platform/unionReimburse/apply/getBudgetBalance" } } @@ -2630,14 +2696,20 @@ layout("/layouts/platform_h5.html"){ // 发送请求 this.$axios.post(apiInfo.url, params) .then(response => { + // 切换来源或社团后,旧请求不得覆盖当前余额。 + if (fundSource !== this.formData.reimburseFundSource + || (params.clubId && params.clubId !== this.formData.clubId)) return if (response.code === 0) { // 显示经费余额(只显示数字,不显示"元") - this.$set(this.formData, "fundBalance", response.data || "0.00") + this.$set(this.formData, "fundBalance", fundSource === "UNION_REIMBURSE_FUND_SOURCE_3" + ? Number(response.data || 0).toFixed(2) : (response.data || "0.00")) } else { this.$set(this.formData, "fundBalance", "查询失败") } }) .catch(error => { + if (fundSource !== this.formData.reimburseFundSource + || (params.clubId && params.clubId !== this.formData.clubId)) return console.error("经费余额查询失败:", error) this.$set(this.formData, "fundBalance", "查询失败") this.$toast.fail("经费余额查询失败") @@ -2807,12 +2879,11 @@ layout("/layouts/platform_h5.html"){ this.$set(this.formData, "unionId", user.union.id) this.$set(this.formData, "unionName", user.union.name) const defaultFundSource = this.getDefaultFundSource() - this.$set(this.formData, "reimburseFundSource", defaultFundSource) - this.$set(this.formData, "reimburseFundSourceName", this.getReimburseFundSourceName(defaultFundSource)) this.$set(this.formData, "paymentDetails", []) - if (this.formData.reimburseFundSource) { - this.queryFundBalance() - } + this.onReimburseFundSourceConfirm({ + value: defaultFundSource, + text: this.getReimburseFundSourceName(defaultFundSource) + }) } } },