From d6e3815b3df7a1ac95c2b384eab64e8dd09821d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=B0=8C=E5=85=88=E8=88=9C?= <2499071264@qq.com> Date: Fri, 6 Nov 2026 18:08:21 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=B7=A5=E4=BC=9A=E6=8A=A5=E9=94=80?= =?UTF-8?q?=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 5 +- .../旅行社导入模板.xlsx | Bin 8767 -> 0 bytes .../sys/controller/SysLoginController.java | 5 +- .../app/sys/controller/SysUnitController.java | 13 +- .../impl/OutlayAnnualAllocateService.java | 168 ++++++++++++------ ...utlayManageUnionStatisticsServiceImpl.java | 26 +-- .../OutlayAnnualAllocateController.java | 18 +- .../OutlayManageUnionUseDetailController.java | 10 ++ .../impl/UnionReimburseServiceImpl.java | 51 +++--- src/main/resources/application-dev.yaml | 7 + .../script/20261103/annual_union_allocate.sql | 21 ++- .../views/platform/sys/unit/index.html | 2 +- .../outlayManage/union/allocate/annual.html | 139 ++++++++++++--- .../useDetail/quarterlyOutlayAllocate.js | 18 +- .../unionReimburse/apply/index.html | 66 ++++--- .../zhgh/dayofficework/unionReimburse/info.js | 4 +- .../unionReimburse/apply/index.html | 25 +-- 17 files changed, 384 insertions(+), 194 deletions(-) delete mode 100644 outputs/therapy_travel_agency_20261026/旅行社导入模板.xlsx diff --git a/.gitignore b/.gitignore index ce1d174..91e4468 100644 --- a/.gitignore +++ b/.gitignore @@ -49,4 +49,7 @@ Thumbs.db .trae/pr-creator/SKILL.md .trae/update-docs/SKILL.md .trae/webapp-testing/SKILL.md -outputs \ No newline at end of file +outputs + +semgrep.json +trivy.json \ No newline at end of file diff --git a/outputs/therapy_travel_agency_20261026/旅行社导入模板.xlsx b/outputs/therapy_travel_agency_20261026/旅行社导入模板.xlsx deleted file mode 100644 index 2a4c51e93eb520881a27225fe6cc663b984ec45e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 8767 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 list = sysUnitService.query(cnd); + boolean defaultRoot = "0".equals(rootId); + boolean hasDefaultRoot = defaultRoot && list.stream().anyMatch(unit -> rootId.equals(unit.getId())); List> nodeList = CollUtil.newArrayList(); for (int i = 0; i < list.size(); i++) { Sys_unit unit = list.get(i); - /* - * 单位根节点存在 id 与 parentId 都为 0 的自引用数据。 - * 构建树时把当前查询根挂到虚拟根下,避免“中国地质大学”和 parentId=0 的学院被构造成同级。 - */ - String parentId = rootId.equals(unit.getId()) ? virtualRootId : unit.getParentId(); + String parentId = unit.getParentId(); + if (rootId.equals(unit.getId()) || (defaultRoot && (StrUtil.isBlank(parentId) + || (!hasDefaultRoot && rootId.equals(parentId))))) { + parentId = virtualRootId; + } nodeList.add(new TreeNode<>(unit.getId(), parentId, unit.getName(), i) .setExtra( Map.of( diff --git a/src/main/java/com/budwk/app/zhgh/dayofficework/outlay/outlayManage/service/impl/OutlayAnnualAllocateService.java b/src/main/java/com/budwk/app/zhgh/dayofficework/outlay/outlayManage/service/impl/OutlayAnnualAllocateService.java index 402775b..133abad 100644 --- a/src/main/java/com/budwk/app/zhgh/dayofficework/outlay/outlayManage/service/impl/OutlayAnnualAllocateService.java +++ b/src/main/java/com/budwk/app/zhgh/dayofficework/outlay/outlayManage/service/impl/OutlayAnnualAllocateService.java @@ -5,6 +5,7 @@ import cn.hutool.core.util.StrUtil; import com.budwk.app.base.result.Result; import com.budwk.app.base.service.impl.BaseServiceImpl; import com.budwk.app.zhgh.dayofficework.outlay.outlayManage.union.model.OutlayManageUnion; +import com.budwk.app.zhgh.dayofficework.outlay.outlayManage.union.vo.OutlayUnionAllocateImportVo; import org.nutz.dao.Dao; import org.nutz.dao.Sqls; import org.nutz.dao.sql.Sql; @@ -14,6 +15,8 @@ import org.nutz.lang.util.NutMap; import java.math.BigDecimal; import java.time.Year; import java.util.List; +import java.util.Map; +import java.util.TreeMap; import java.util.UUID; @IocBean(args = {"refer:dao"}) @@ -37,17 +40,54 @@ public class OutlayAnnualAllocateService extends BaseServiceImpl0)" : "") + + ("special".equals(type) ? " AND (@projectName='' OR LOCATE(@projectName,a.projectName)>0)" : " AND a.status<>2" + amountMatchCondition(amountMatch)) + " ORDER BY un.unionCode,a.createdAt DESC", "year", year, "unionId", StrUtil.blankToDefault(unionId, ""), "projectName", StrUtil.blankToDefault(StrUtil.trim(projectName), "")); } + private String amountMatchCondition(String amountMatch) { + if ("equal".equals(amountMatch)) return " AND a.calculatedMoney=a.allocateMoney"; + if ("different".equals(amountMatch)) return " AND a.calculatedMoney<>a.allocateMoney"; + return ""; + } + + public Result importBaseAmounts(Integer year, String unionId, List items, String remark) { + if (year == null || items == null || items.isEmpty() || StrUtil.isBlank(remark) || remark.length() > 1000) { + return Result.error("请选择年度和导入文件,并填写1000字以内的调整说明"); + } + Map records = new TreeMap<>(); + Map amounts = new TreeMap<>(); + for (OutlayUnionAllocateImportVo item : items) { + if (item == null || StrUtil.isBlank(item.getUnionCode()) || !moneyValid(item.getAllocateMoney())) { + return Result.error("工会编码和分配额度必填,金额须为有效的非负数且最多两位小数"); + } + List matches = listMap(sql(""" + SELECT a.id,a.unionId,a.standard,a.version FROM outlay_annual_base a + JOIN sys_union un ON un.id=a.unionId + WHERE a.year=@year AND a.status IN (0,1) AND un.unionCode=@code AND (@unionId='' OR a.unionId=@unionId) + """, "year", year, "code", item.getUnionCode().trim(), "unionId", StrUtil.blankToDefault(unionId, ""))); + if (matches.size() != 1) return Result.error("工会编码 " + item.getUnionCode() + " 未匹配到唯一有效基础经费记录"); + NutMap row = matches.get(0); + String uid = row.getString("unionId"); + if (records.putIfAbsent(uid, row) != null) return Result.error("工会编码 " + item.getUnionCode() + " 重复"); + amounts.put(uid, item.getAllocateMoney()); + } + for (Map.Entry entry : records.entrySet()) { + String uid = entry.getKey(); + NutMap row = entry.getValue(); + Result result = save("base", row.getString("id"), year, uid, null, null, + decimal(row.get("standard")), amounts.get(uid), remark.trim(), row.getInt("version")); + if (result.getCode() != 0) throw new IllegalArgumentException(result.getMsg()); + } + return Result.success("已导入" + records.size() + "条记录"); + } + public List summary(Integer year, String unionId) { return listMap(sql(""" SELECT un.id, un.name unionName, COALESCE(b.money,0) baseMoney, @@ -61,6 +101,17 @@ public class OutlayAnnualAllocateService extends BaseServiceImpl confirmedRecords(Integer year, String unionId) { + return listMap(sql(""" + SELECT id,'base' allocateType,'年度基础经费' projectName,allocateMoney + FROM outlay_annual_base WHERE year=@year AND unionId=@unionId AND status=1 + UNION ALL + SELECT id,'special' allocateType,projectName,allocateMoney + FROM outlay_annual_special WHERE year=@year AND unionId=@unionId AND status=1 + ORDER BY allocateType,projectName,id + """, "year", year, "unionId", unionId)); + } + public List historyRecords(Integer year, String unionId) { return listMap(sql(""" SELECT a.year,un.name unionName,a.quarterly,a.allocateHeadMoney,a.allocateMoney @@ -82,20 +133,33 @@ public class OutlayAnnualAllocateService extends BaseServiceImpl 100 || allocateMoney.signum() <= 0) { return Result.error("请填写专项名称(最多100字)和大于0的金额"); } - if (confirmed && StrUtil.isBlank(remark)) return Result.error("调整已下拨记录必须填写原因"); - if (confirmed) syncBudget(old, allocateMoney.subtract(new BigDecimal(old.get("allocateMoney").toString()))); + if (confirmed && StrUtil.isBlank(remark)) return Result.error("调整已计入预算的经费必须填写原因"); + BigDecimal budgetDelta = allocateMoney.subtract(confirmed ? decimal(old.get("allocateMoney")) : BigDecimal.ZERO); + if (!confirmed || budgetDelta.signum() != 0) syncBudget(year, unionId, budgetDelta); long now = System.currentTimeMillis(); String actor = StpUtil.getLoginIdAsString(); if (old == null) { execute(sql(""" INSERT INTO outlay_annual_special (id,year,unionId,projectName,allocateMoney,remark,status,version,createdAt,createdBy,updatedAt,updatedBy) - VALUES (@id,@year,@unionId,@name,@money,@remark,0,0,@now,@actor,@now,@actor) + VALUES (@id,@year,@unionId,@name,@money,@remark,1,0,@now,@actor,@now,@actor) """, "id", id, "year", year, "unionId", unionId, "name", projectName.trim(), "money", allocateMoney, "remark", remark, "now", now, "actor", actor)); } else { - execute(sql("UPDATE " + table + " SET allocateMoney=@money,remark=@remark,updatedAt=@now,updatedBy=@actor,version=version+1" + + execute(sql("UPDATE " + table + " SET allocateMoney=@money,remark=@remark,status=1,updatedAt=@now,updatedBy=@actor,version=version+1" + ("base".equals(type) ? ",memberCount=@count,standard=@standard,calculatedMoney=@calculated" : ",projectName=@name") + " WHERE id=@id", "money", allocateMoney, "remark", remark, "now", now, "actor", actor, "count", memberCount, "standard", standard, "calculated", calculated, "name", StrUtil.trim(projectName), "id", id)); } - audit(type, id, confirmed ? "调整下拨" : "保存", old, first(sql("SELECT * FROM " + table + " WHERE id=@id", "id", id)), remark); + audit(type, id, confirmed ? "调整经费" : "保存", old, first(sql("SELECT * FROM " + table + " WHERE id=@id", "id", id)), remark); return Result.success(); } - public Result confirm(String type, String id, Integer version) { - String table = table(type); - NutMap snapshot = first(sql("SELECT * FROM " + table + " WHERE id=@id", "id", id)); - if (snapshot == null) return Result.error("记录不存在"); - lockUnion(snapshot.getString("unionId")); - NutMap row = first(sql("SELECT * FROM " + table + " WHERE id=@id FOR UPDATE", "id", id)); - if (row.getInt("status") == 1) return Result.success("该记录已下拨,无需重复操作"); - if (version == null || row.getInt("version") != version) return Result.error("记录已更新,请刷新后重试"); - confirmRecord(type, table, row); - return Result.success(); - } - - public Result batchConfirm(String type, Integer year, String unionId, String projectName) { - String table = table(type); - if (year == null || year < 1900 || year > 9999) return Result.error("请选择有效年度"); - List pending = listMap(sql("SELECT * FROM " + table + - " WHERE year=@year AND status=0 AND (@unionId='' OR unionId=@unionId)" + - ("special".equals(type) ? " AND (@projectName='' OR LOCATE(@projectName,projectName)>0)" : "") + - " ORDER BY unionId,id", - "year", year, "unionId", StrUtil.blankToDefault(unionId, ""), - "projectName", StrUtil.blankToDefault(StrUtil.trim(projectName), ""))); + public Result resetBaseRecords(Integer year, String unionId, String amountMatch) { + if (year == null || year != Year.now().getValue()) return Result.error("只能重置当前年度的分配记录"); + List candidates = listMap(sql(""" + SELECT id,unionId,version FROM outlay_annual_base a + WHERE year=@year AND status IN (0,1) AND (@unionId='' OR unionId=@unionId) + """ + amountMatchCondition(amountMatch) + " ORDER BY unionId,id", "year", year, "unionId", StrUtil.blankToDefault(unionId, ""))); int count = 0; - for (NutMap candidate : pending) { + for (NutMap candidate : candidates) { + String id = candidate.getString("id"); lockUnion(candidate.getString("unionId")); - NutMap row = first(sql("SELECT * FROM " + table + " WHERE id=@id FOR UPDATE", "id", candidate.getString("id"))); - if (row == null || row.getInt("status") == 1) continue; - if (row.getInt("version") != candidate.getInt("version")) - throw new IllegalArgumentException("待下拨记录已被修改,本次操作已取消,请刷新后重试"); - confirmRecord(type, table, row); + NutMap row = first(sql("SELECT * FROM outlay_annual_base WHERE id=@id FOR UPDATE", "id", id)); + if (row == null || row.getInt("status") == 2) continue; + if (row.getInt("version") != candidate.getInt("version")) { + throw new IllegalArgumentException("分配记录发生变更,本次重置已取消,请刷新后重试"); + } + BigDecimal amount = row.getInt("status") == 1 ? decimal(row.get("allocateMoney")) : BigDecimal.ZERO; + if (amount.signum() != 0) syncBudget(year, row.getString("unionId"), amount.negate()); + execute(sql(""" + UPDATE outlay_annual_base + SET status=2,updatedAt=@now,updatedBy=@actor,version=version+1 + WHERE id=@id + """, "id", id, "now", System.currentTimeMillis(), "actor", StpUtil.getLoginIdAsString())); + NutMap after = first(sql("SELECT * FROM outlay_annual_base WHERE id=@id", "id", id)); + audit("base", id, "重置分配记录", row, after, "扣回年度预算 " + amount.toPlainString() + " 元"); count++; } - return Result.success(count == 0 ? "当前筛选范围没有待下拨记录" : "成功下拨" + count + "条记录"); + return Result.success(count == 0 ? "当前筛选范围没有可重置记录" : "已重置" + count + "条记录"); } - private void confirmRecord(String type, String table, NutMap row) { - String id = row.getString("id"); - syncBudget(row, new BigDecimal(row.get("allocateMoney").toString())); - execute(sql("UPDATE " + table + " SET status=1,allocatedAt=@now,updatedAt=@now,updatedBy=@actor,version=version+1 WHERE id=@id", - "id", id, "now", System.currentTimeMillis(), "actor", StpUtil.getLoginIdAsString())); - audit(type, id, "确认下拨", row, first(sql("SELECT * FROM " + table + " WHERE id=@id", "id", id)), "确认下拨"); - } - - private void syncBudget(NutMap row, BigDecimal delta) { + private void syncBudget(int year, String unionId, BigDecimal delta) { List budgets = listMap(sql("SELECT * FROM outlay_manage_union WHERE year=@year AND unionId=@unionId FOR UPDATE", - "year", row.getInt("year"), "unionId", row.getString("unionId"))); + "year", year, "unionId", unionId)); if (budgets.size() > 1) throw new IllegalArgumentException("该分工会年度预算存在重复记录,不能下拨"); if (budgets.isEmpty()) { if (delta.signum() < 0) throw new IllegalArgumentException("年度预算不存在,不能扣减"); // 新建预算承接上年结余;已有预算仅增加本次差额,避免重复结转。 NutMap previous = first(sql("SELECT COALESCE(totalQuota,0)-COALESCE(usedQuota,0) surplus FROM outlay_manage_union WHERE year=@year AND unionId=@unionId", - "year", row.getInt("year") - 1, "unionId", row.getString("unionId"))); + "year", year - 1, "unionId", unionId)); BigDecimal carry = previous == null ? BigDecimal.ZERO : new BigDecimal(previous.get("surplus").toString()); if (!moneyValid(carry.add(delta))) throw new IllegalArgumentException("下拨后年度总额度超出允许范围"); execute(sql(""" INSERT INTO outlay_manage_union (id,year,unionId,unionName,unionCode,totalQuota,usedQuota,delFlag,createdAt,updatedAt,createdBy,updatedBy) SELECT @id,@year,id,name,unionCode,@money,0,0,@now,@now,@actor,@actor FROM sys_union WHERE id=@unionId - """, "id", UUID.randomUUID().toString().replace("-", ""), "year", row.getInt("year"), - "unionId", row.getString("unionId"), "money", carry.add(delta), "now", System.currentTimeMillis(), "actor", StpUtil.getLoginIdAsString())); + """, "id", UUID.randomUUID().toString().replace("-", ""), "year", year, + "unionId", unionId, "money", carry.add(delta), "now", System.currentTimeMillis(), "actor", StpUtil.getLoginIdAsString())); } else { NutMap budget = budgets.get(0); BigDecimal total = decimal(budget.get("totalQuota")).add(delta); diff --git a/src/main/java/com/budwk/app/zhgh/dayofficework/outlay/outlayManage/service/impl/OutlayManageUnionStatisticsServiceImpl.java b/src/main/java/com/budwk/app/zhgh/dayofficework/outlay/outlayManage/service/impl/OutlayManageUnionStatisticsServiceImpl.java index c11ace8..84b4e3a 100644 --- a/src/main/java/com/budwk/app/zhgh/dayofficework/outlay/outlayManage/service/impl/OutlayManageUnionStatisticsServiceImpl.java +++ b/src/main/java/com/budwk/app/zhgh/dayofficework/outlay/outlayManage/service/impl/OutlayManageUnionStatisticsServiceImpl.java @@ -70,20 +70,18 @@ public class OutlayManageUnionStatisticsServiceImpl extends BaseServiceImpl impl } private String incomeExpenseQuery() { - // 历史会费并入年度基础经费,历史奖励并入专项经费;分别汇总避免关联后重复累计。 - // 年初数由年度总额度扣除当年各类下拨计算,兼容没有历史第一期记录的新流程。 return """ SELECT mu.year, mu.unionId, COALESCE(mu.unionName,su.name) AS unionName, COALESCE(mu.unionCode,su.unionCode) AS unionCode, - COALESCE(mu.totalQuota,0) - (COALESCE(b.money,0) + COALESCE(sp.money,0) + COALESCE(au.feeJanApr,0) + COALESCE(au.feeMayAug,0) + COALESCE(au.feeSepDec,0) + COALESCE(au.awardMoney,0)) AS beginMoney, - COALESCE(b.money,0) + COALESCE(au.feeJanApr,0) + COALESCE(au.feeMayAug,0) + COALESCE(au.feeSepDec,0) AS baseMoney, - COALESCE(sp.money,0) + COALESCE(au.awardMoney,0) AS specialMoney, - (COALESCE(b.money,0) + COALESCE(sp.money,0) + COALESCE(au.feeJanApr,0) + COALESCE(au.feeMayAug,0) + COALESCE(au.feeSepDec,0) + COALESCE(au.awardMoney,0)) AS incomeMoney, - COALESCE(au.feeJanApr,0) AS feeJanApr, - COALESCE(au.feeMayAug,0) AS feeMayAug, - COALESCE(au.feeSepDec,0) AS feeSepDec, - COALESCE(au.awardMoney,0) AS awardMoney, + COALESCE(mu.totalQuota,0) - (COALESCE(b.money,0) + COALESCE(sp.money,0)) AS beginMoney, + COALESCE(b.money,0) AS baseMoney, + COALESCE(sp.money,0) AS specialMoney, + COALESCE(b.money,0) + COALESCE(sp.money,0) AS incomeMoney, + 0 AS feeJanApr, + 0 AS feeMayAug, + 0 AS feeSepDec, + 0 AS awardMoney, COALESCE(mu.usedQuota,0) AS usedMoney, COALESCE(mu.totalQuota,0)-COALESCE(mu.usedQuota,0) AS remainMoney FROM outlay_manage_union mu @@ -96,14 +94,6 @@ public class OutlayManageUnionStatisticsServiceImpl extends BaseServiceImpl impl SELECT year,unionId,SUM(allocateMoney) money FROM outlay_annual_special WHERE year=@incomeYear AND status=1 GROUP BY year,unionId ) sp ON sp.year=mu.year AND sp.unionId=mu.unionId - LEFT JOIN ( - SELECT year,unionId, - SUM(CASE WHEN quarterly=1 THEN allocateMoney ELSE 0 END) feeJanApr, - SUM(CASE WHEN quarterly=2 THEN allocateMoney ELSE 0 END) feeMayAug, - SUM(CASE WHEN quarterly=3 THEN allocateMoney ELSE 0 END) feeSepDec, - SUM(CASE WHEN quarterly=4 THEN allocateMoney ELSE 0 END) awardMoney - FROM outlay_allocate_union WHERE year=@incomeYear AND delFlag=0 GROUP BY year,unionId - ) au ON au.year=mu.year AND au.unionId=mu.unionId $condition """; } diff --git a/src/main/java/com/budwk/app/zhgh/dayofficework/outlay/outlayManage/union/controller/OutlayAnnualAllocateController.java b/src/main/java/com/budwk/app/zhgh/dayofficework/outlay/outlayManage/union/controller/OutlayAnnualAllocateController.java index eec68e7..4fb1bf4 100644 --- a/src/main/java/com/budwk/app/zhgh/dayofficework/outlay/outlayManage/union/controller/OutlayAnnualAllocateController.java +++ b/src/main/java/com/budwk/app/zhgh/dayofficework/outlay/outlayManage/union/controller/OutlayAnnualAllocateController.java @@ -10,6 +10,8 @@ import com.budwk.app.sys.models.Sys_dict; import com.budwk.app.sys.services.SysDictService; import org.apache.poi.ss.usermodel.Workbook; import org.nutz.lang.util.NutMap; +import org.nutz.json.Json; +import com.budwk.app.zhgh.dayofficework.outlay.outlayManage.union.vo.OutlayUnionAllocateImportVo; import javax.servlet.http.HttpServletResponse; import java.util.ArrayList; import java.util.List; @@ -38,9 +40,9 @@ public class OutlayAnnualAllocateController { @At @SaCheckPermission("outlay.outlayManage.union.allocate") - public Result pageData(PageForm pageForm, String type, Integer year, String unionId, String projectName) { + public Result pageData(PageForm pageForm, String type, Integer year, String unionId, String projectName, String amountMatch) { return Result.success(outlayAnnualAllocateService.listPageMap(pageForm.getPageNumber(), pageForm.getPageSize(), - outlayAnnualAllocateService.pageSql(type, year, unionId, projectName))); + outlayAnnualAllocateService.pageSql(type, year, unionId, projectName, amountMatch))); } @At @@ -117,16 +119,18 @@ public class OutlayAnnualAllocateController { @SaCheckPermission("outlay.outlayManage.union.allocate") @POST @Aop(TransAop.READ_COMMITTED) - public Result confirm(String type, String id, Integer version) { - return outlayAnnualAllocateService.confirm(type, id, version); + public Result resetBaseRecords(Integer year, String unionId, String amountMatch) { + return outlayAnnualAllocateService.resetBaseRecords(year, unionId, amountMatch); } @At - @SaCheckPermission("outlay.outlayManage.union.allocate") @POST @Aop(TransAop.READ_COMMITTED) - public Result batchConfirm(String type, Integer year, String unionId, String projectName) { - return outlayAnnualAllocateService.batchConfirm(type, year, unionId, projectName); + @SaCheckPermission("outlay.outlayManage.union.allocate") + public Result importBaseAmounts(Integer year, String unionId, String data, String remark) { + if (data == null || data.isBlank()) return Result.error("请先上传导入文件"); + return outlayAnnualAllocateService.importBaseAmounts(year, unionId, + Json.fromJsonAsList(OutlayUnionAllocateImportVo.class, data), remark); } @At diff --git a/src/main/java/com/budwk/app/zhgh/dayofficework/outlay/outlayManage/union/controller/OutlayManageUnionUseDetailController.java b/src/main/java/com/budwk/app/zhgh/dayofficework/outlay/outlayManage/union/controller/OutlayManageUnionUseDetailController.java index 6688cf7..098dd91 100644 --- a/src/main/java/com/budwk/app/zhgh/dayofficework/outlay/outlayManage/union/controller/OutlayManageUnionUseDetailController.java +++ b/src/main/java/com/budwk/app/zhgh/dayofficework/outlay/outlayManage/union/controller/OutlayManageUnionUseDetailController.java @@ -13,6 +13,7 @@ import com.budwk.app.web.commons.auth.utils.SecurityUtil; import com.budwk.app.zhgh.dayofficework.outlay.outlayManage.model.OutlayUseDetail; import com.budwk.app.zhgh.dayofficework.outlay.outlayManage.service.OutlayUseDetailService; import com.budwk.app.zhgh.dayofficework.outlay.outlayManage.service.OutlayManageUnionStatisticsService; +import com.budwk.app.zhgh.dayofficework.outlay.outlayManage.service.impl.OutlayAnnualAllocateService; import com.budwk.app.zhgh.dayofficework.outlay.outlayManage.union.model.OutLayAllocateUnion; import io.swagger.annotations.Api; import io.swagger.annotations.ApiOperation; @@ -49,6 +50,8 @@ public class OutlayManageUnionUseDetailController { private OutlayUseDetailService outlayUseDetailService; @Inject private OutlayManageUnionStatisticsService outlayManageUnionStatisticsService; + @Inject + private OutlayAnnualAllocateService outlayAnnualAllocateService; @At("") @Ok("beetl:/platform/zhgh/dayofficework/outlay/outlayManage/union/useDetail/index.html") @@ -128,6 +131,13 @@ public class OutlayManageUnionUseDetailController { return Result.success(); } + @At + @ApiOperation("查询分工会年度已下拨经费明细") + @SaCheckPermission("outlay.outlayManage.union.useDetail") + public Result queryAnnualAllocateList(Integer year, String unionId) { + return Result.success(outlayAnnualAllocateService.confirmedRecords(year, unionId)); + } + @At @ApiOperation("查询分工会今年每个分配项目的分配情况") @SaCheckPermission("outlay.outlayManage.union.useDetail") 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 fbaa7f7..1d49215 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 @@ -122,9 +122,9 @@ public class UnionReimburseServiceImpl extends BaseServiceImpl i } /** - * 保存、提交及重新提交共用支付信息校验,并在同一事务内保存申请和发票明细。 + * 在同一事务内保存申请和发票明细,正式提交及重新提交时校验支付信息。 * @param unionReimburse 申请表单,paymentDetails 为支付方式、金额及对私收款资料组成的明细列表 - * @param stateId 1 表示保存草稿,2 表示提交;均要求支付信息完整,发票选填,上传后须通过服务端验真 + * @param stateId 1 表示保存草稿,2 表示提交;提交时至少需要一张发票 * @return Result 成功或失败结果,msg 包含失败明细的行号及原因 */ @Override @@ -133,23 +133,27 @@ public class UnionReimburseServiceImpl extends BaseServiceImpl i if (unionReimburse == null) { return Result.error("报销数据不能为空"); } - // 发票选填;保存、提交及重提均对已上传的发票检查完整性。 + if (stateId == 2 && Lang.isEmpty(unionReimburse.getInvoiceDetails())) { + return Result.error("提交前请至少上传一张发票"); + } + // 保存、提交及重提均对已上传的发票检查完整性。 Result invoiceResult = validateInvoiceDetails(unionReimburse); if (invoiceResult != null) { return invoiceResult; } - Result paymentResult = validatePaymentDetails(unionReimburse); - if (paymentResult != null) { - return paymentResult; - } - // 保存、提交及重提均在写入前检查支付总额,不能只依赖页面的金额限制。 - Result amountResult = validatePaymentAmountLimit(unionReimburse); - if (amountResult != null) { - return amountResult; - } - Result activityAmountResult = validateActivityApplyMoney(unionReimburse); - if (activityAmountResult != null) { - return activityAmountResult; + if (stateId == 2) { + Result paymentResult = validatePaymentDetails(unionReimburse); + if (paymentResult != null) { + return paymentResult; + } + Result amountResult = validatePaymentAmountLimit(unionReimburse); + if (amountResult != null) { + return amountResult; + } + Result activityAmountResult = validateActivityApplyMoney(unionReimburse); + if (activityAmountResult != null) { + return activityAmountResult; + } } UnionReimburse oldRecord = null; if (StrUtil.isNotBlank(unionReimburse.getId())) { @@ -760,15 +764,18 @@ public class UnionReimburseServiceImpl extends BaseServiceImpl i invoiceFileCount++; } } - // 无发票申请从服务端已校验的支付明细计算金额,不能信任前端传入的报销总额。 - if (Lang.isEmpty(unionReimburse.getInvoiceDetails())) { + // 无发票时由支付明细汇总,草稿中未填完的金额暂不计入。 + if (Lang.isEmpty(unionReimburse.getInvoiceDetails()) && Lang.isNotEmpty(unionReimburse.getPaymentDetails())) { BigDecimal paymentTotal = BigDecimal.ZERO; for (JSONObject detail : unionReimburse.getPaymentDetails()) { - paymentTotal = paymentTotal.add(new BigDecimal(detail.get("money").toString())); + String money = detail == null ? "" : detail.getStr("money", ""); + if (money.matches("[0-9]+(\\.[0-9]{1,2})?")) { + paymentTotal = paymentTotal.add(new BigDecimal(money)); + } } totalMoney = paymentTotal.setScale(2, RoundingMode.HALF_UP).doubleValue(); } - // 三类活动使用已校验的手填申请金额,不能被发票或支付合计覆盖。 + // 三类活动保留手填申请金额。 if (!isManualReimburseAmount(unionReimburse)) { unionReimburse.setMoney(BigDecimal.valueOf(totalMoney).setScale(2, RoundingMode.HALF_UP).doubleValue()); } @@ -783,8 +790,8 @@ public class UnionReimburseServiceImpl extends BaseServiceImpl i } /** - * 校验支付明细,防止绕过 PC/H5 校验直接保存。对公只需方式和金额,对私必须补齐人员及银行资料。 - * @param form 本次保存的申请,paymentDetails 必须至少包含一条完整支付明细 + * 校验提交的支付明细。对公只需方式和金额,对私必须补齐人员及银行资料。 + * @param form 本次提交的申请,paymentDetails 必须至少包含一条完整支付明细 * @return 校验通过返回 null,失败返回带行号和原因的 Result */ private Result validatePaymentDetails(UnionReimburse form) { @@ -916,7 +923,7 @@ public class UnionReimburseServiceImpl extends BaseServiceImpl i } /** - * 发票列表允许为空;已有明细必须且只能包含一张可定位的发票文件。 + * 校验已填写的发票明细,每条明细必须且只能包含一张可定位的发票文件。 */ private Result validateInvoiceDetails(UnionReimburse unionReimburse) { if (Lang.isEmpty(unionReimburse.getInvoiceDetails())) { diff --git a/src/main/resources/application-dev.yaml b/src/main/resources/application-dev.yaml index 90f083c..4e0444e 100644 --- a/src/main/resources/application-dev.yaml +++ b/src/main/resources/application-dev.yaml @@ -80,3 +80,10 @@ custom: encrypted-secret-key: '' connect-timeout: 5000 read-timeout: 10000 + +invoice: + tencent: + # 发票 OCR 使用腾讯云 SecretId/SecretKey;为空时会回退读取 secret-file 指向的 csv。 + secret-id: AKID1473y253saFsMelgS4TjnfLofMrAHDyX + secret-key: TlrlTzIRNjtIj296riPuISbmqplnhQDy + secret-file: C:/Users/Administrator/Desktop \ No newline at end of file diff --git a/src/main/resources/db/script/20261103/annual_union_allocate.sql b/src/main/resources/db/script/20261103/annual_union_allocate.sql index a089f32..fe1f590 100644 --- a/src/main/resources/db/script/20261103/annual_union_allocate.sql +++ b/src/main/resources/db/script/20261103/annual_union_allocate.sql @@ -10,9 +10,8 @@ CREATE TABLE IF NOT EXISTS `outlay_annual_base` ( `calculatedMoney` decimal(10,2) NOT NULL, `allocateMoney` decimal(10,2) NOT NULL, `remark` varchar(1000) DEFAULT NULL, - `status` tinyint NOT NULL DEFAULT 0 COMMENT '0待下拨 1已下拨', + `status` tinyint NOT NULL DEFAULT 0 COMMENT '0待下拨 1已下拨 2已重置', `version` int NOT NULL DEFAULT 0, - `allocatedAt` bigint DEFAULT NULL, `createdAt` bigint NOT NULL, `createdBy` varchar(32) NOT NULL, `updatedAt` bigint NOT NULL, @@ -30,7 +29,6 @@ CREATE TABLE IF NOT EXISTS `outlay_annual_special` ( `remark` varchar(1000) DEFAULT NULL, `status` tinyint NOT NULL DEFAULT 0 COMMENT '0待下拨 1已下拨', `version` int NOT NULL DEFAULT 0, - `allocatedAt` bigint DEFAULT NULL, `createdAt` bigint NOT NULL, `createdBy` varchar(32) NOT NULL, `updatedAt` bigint NOT NULL, @@ -53,6 +51,23 @@ CREATE TABLE IF NOT EXISTS `outlay_annual_allocate_log` ( KEY `idx_annual_log_record_time` (`allocateType`,`allocateId`,`createdAt`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='经费下拨及调整记录'; +-- 兼容已执行旧版建表脚本的数据库。 +SET @drop_annual_allocated_at = IF(EXISTS ( + SELECT 1 FROM information_schema.COLUMNS + WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'outlay_annual_base' AND COLUMN_NAME = 'allocatedAt' +), 'ALTER TABLE `outlay_annual_base` DROP COLUMN `allocatedAt`', 'DO 0'); +PREPARE drop_annual_allocated_at FROM @drop_annual_allocated_at; +EXECUTE drop_annual_allocated_at; +DEALLOCATE PREPARE drop_annual_allocated_at; + +SET @drop_annual_allocated_at = IF(EXISTS ( + SELECT 1 FROM information_schema.COLUMNS + WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'outlay_annual_special' AND COLUMN_NAME = 'allocatedAt' +), 'ALTER TABLE `outlay_annual_special` DROP COLUMN `allocatedAt`', 'DO 0'); +PREPARE drop_annual_allocated_at FROM @drop_annual_allocated_at; +EXECUTE drop_annual_allocated_at; +DEALLOCATE PREPARE drop_annual_allocated_at; + -- 沿用现有权限与入口,仅调整菜单名称。 UPDATE sys_menu SET name='年度经费下拨', aliasName='年度经费下拨' WHERE permission='outlay.outlayManage.union.allocate'; diff --git a/src/main/resources/views/platform/sys/unit/index.html b/src/main/resources/views/platform/sys/unit/index.html index 4bd3a4c..2a51be9 100644 --- a/src/main/resources/views/platform/sys/unit/index.html +++ b/src/main/resources/views/platform/sys/unit/index.html @@ -177,7 +177,7 @@ layout("/layouts/platform.html"){ getTreeData() { this.$set(this, "treeLoading", true) // tree 返回单位树数组;失败时保留现有内容,成功后按 ID 恢复当前单位。 - return this.$axios.post(loc() + "/tree").then((res) => { + return this.$axios.post("/platform/sys/unit/tree").then((res) => { if (res.code !== 0 || !Array.isArray(res.data)) return const selectedId = this.currentTreeData?.id this.$set(this, "treeData", res.data) diff --git a/src/main/resources/views/platform/zhgh/dayofficework/outlay/outlayManage/union/allocate/annual.html b/src/main/resources/views/platform/zhgh/dayofficework/outlay/outlayManage/union/allocate/annual.html index 9e1bda1..826c4a2 100644 --- a/src/main/resources/views/platform/zhgh/dayofficework/outlay/outlayManage/union/allocate/annual.html +++ b/src/main/resources/views/platform/zhgh/dayofficework/outlay/outlayManage/union/allocate/annual.html @@ -6,6 +6,14 @@ layout("/layouts/platform.html"){ .fund-log-dialog .el-dialog { width: 94%; max-width: 1800px; height: 86vh; display: flex; flex-direction: column; } .fund-log-dialog .el-dialog__body { padding: 16px 24px 24px; flex: 1; min-height: 0; display: flex; flex-direction: column; } .fund-log-dialog .el-pagination { margin-top: 16px; text-align: right; flex: 0 0 auto; } + .fund-import-dialog .el-dialog { max-width: calc(100vw - 32px); } + .fund-import-dialog .el-dialog__body { padding: 16px 24px; max-height: 68vh; overflow-y: auto; } + .fund-import-dialog .el-dialog__footer { border-top: 1px solid #ebeef5; } + .fund-import-toolbar { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin: 16px 0 8px; } + .fund-import-upload .el-upload, + .fund-import-upload .el-upload-dragger { display: block; width: 100%; } + .fund-import-upload .el-upload-dragger { height: 140px; } + .fund-import-upload .el-icon-upload { margin: 16px 0 8px; } .fund-edit-dialog .el-dialog { max-width: calc(100vw - 32px); } .fund-edit-dialog .el-dialog__body { padding: 20px 28px; max-height: 65vh; overflow-y: auto; } .fund-edit-dialog .el-dialog__footer { padding: 16px 28px; border-top: 1px solid #ebeef5; } @@ -23,6 +31,14 @@ layout("/layouts/platform.html"){ .fund-edit-amount { display: flex; align-items: center; flex-wrap: wrap; gap: 16px; } .fund-edit-amount .el-input-number { flex: 1; min-width: 200px; } .fund-edit-calculated { display: block; padding: 0 15px; background: #f5f7fa; border: 1px solid #e4e7ed; border-radius: 4px; font-size: 20px; font-weight: 600; color: #303133; } + .fund-adjust-dialog .el-dialog__header { padding-bottom: 8px; } + .fund-adjust-dialog .el-dialog__body { padding: 12px 24px; } + .fund-adjust-dialog .el-dialog__footer { padding: 12px 24px; } + .fund-adjust-dialog .el-alert { margin-bottom: 12px; } + .fund-adjust-dialog .fund-edit-form { gap: 10px 20px; } + .fund-adjust-dialog .fund-edit-section { padding-bottom: 6px; } + .fund-adjust-dialog .el-form-item__label { padding-bottom: 4px; line-height: 20px; } + .fund-adjust-dialog .fund-edit-difference { padding: 6px 12px; } @media (max-width: 600px) { .fund-edit-form { grid-template-columns: minmax(0, 1fr); } } @@ -35,6 +51,13 @@ layout("/layouts/platform.html"){ + + + + + + + @@ -47,8 +70,9 @@ layout("/layouts/platform.html"){ 生成年度基础经费 新增专项经费 - 一键下拨 + 重置分配记录 导出Excel + Excel导入金额 @@ -71,14 +95,11 @@ layout("/layouts/platform.html"){ @@ -89,7 +110,7 @@ layout("/layouts/platform.html"){ - + {{query.year}} {{selectedUnionName}} @@ -97,8 +118,40 @@ layout("/layouts/platform.html"){ 取消生成 - - + + + 请在模板中仅保留需调整的分工会并填写分配额度。导入会更新已有基础经费记录并同步年度预算。任一行未匹配到唯一有效记录时,整批导入失败。 + +
+ 导入文件 + 下载模板 +
+ + +
将 Excel 文件拖到此处,或点击上传
+
支持 .xls、.xlsx,每次上传一个文件
+
+ + + + + + + + + + + + + 取消 + 确认导入 + +
+ + +
基础信息 · {{tab==='base'?'年度基础经费':'专项经费'}}
@@ -139,12 +192,14 @@ layout("/layouts/platform.html"){ mixins: [OUTLAY_UNION_FIXED_LIST_MIXIN], dicts: ['OUTLAY_QUARTERLY'], data() { return { - tab: 'base', query: {year: this.$moment().format('YYYY'), unionId: '', projectName: '', pageNumber: 1, pageSize: 20}, + tab: 'base', query: {year: this.$moment().format('YYYY'), unionId: '', projectName: '', amountMatch: '', pageNumber: 1, pageSize: 20}, unionList: [], rows: [], total: 0, loading: false, busy: false, requestSequence: 0, standard: 800, generateVisible: false, editVisible: false, isNew: false, form: {}, originalAllocateMoney: 0, + baseImportVisible: false, baseImportRows: [], baseImportRemark: '', logsVisible: false, logsLoading: false, logs: [], logPage: 1, logTotal: 0, logRecord: null }}, computed: { + isCurrentYear() { return this.query.year===this.$moment().format('YYYY') }, calculated() { return ((this.form.memberCount || 0) * (this.form.standard || 0)).toFixed(2) }, amountDifference() { const amount=Number(this.form.allocateMoney) @@ -153,13 +208,17 @@ layout("/layouts/platform.html"){ return Number((amount-reference).toFixed(2)) }, remarkRequired() { return this.form.status===1 || (this.tab==='base' && this.amountDifference!==0) }, + amountMatchScope() { + if (!this.query.amountMatch) return '' + return this.query.amountMatch==='equal'?' / 金额一致':' / 金额不一致' + }, selectedUnionName() { const u=this.unionList.find(u=>u.id===this.query.unionId); return u?u.name:'全部分工会' } }, methods: { time(value) { return value?this.$moment(Number(value)).format('YYYY-MM-DD HH:mm:ss'):'—' }, snapshot(value) { if (!value) return '—' - try { const v=JSON.parse(value); return v.year+'年 / '+(v.memberCount==null?(v.projectName || ''):v.memberCount+'人 × '+v.standard+'元,计算 '+v.calculatedMoney+'元')+' / 实际 '+v.allocateMoney+'元 / '+(Number(v.status)===1?'已下拨':'待下拨') } + try { const v=JSON.parse(value); return v.year+'年 / '+(v.memberCount==null?(v.projectName || ''):v.memberCount+'人 × '+v.standard+'元,计算 '+v.calculatedMoney+'元')+' / 实际 '+v.allocateMoney+'元 / '+(Number(v.status)===2?'已重置':Number(v.status)===1?'已下拨':'待下拨') } catch(e) { return value } }, doExport() { @@ -221,27 +280,59 @@ layout("/layouts/platform.html"){ try { await this.request('save',payload); this.$message.success('保存成功'); this.editVisible=false; this.doSearch() } catch(e) {} finally { this.busy=false } }, - async batchConfirm() { - if (this.busy) return - const params={type:this.tab,year:this.query.year,unionId:this.query.unionId || '',projectName:this.tab==='special'?(this.query.projectName || '').trim():''} - const typeName=this.tab==='base'?'年度基础经费':'专项经费' - const scope=this.selectedUnionName+(params.projectName?' / 专项名称包含:'+params.projectName:'') - if (!params.year) { this.$message.warning('请选择年度'); return } + openBaseImport() { + this.clearBaseImportPreview() + this.baseImportRemark='' + this.baseImportVisible=true + }, + downloadBaseImportTemplate() { + window.open('/platform/outlay/outlayManage/unionAllocate/downloadImportTemplate') + }, + clearBaseImportPreview() { this.baseImportRows=[] }, + validateBaseImportFile(file) { + if (/\.(xls|xlsx)$/i.test(file.name)) return true + this.$message.warning('请上传 .xls 或 .xlsx 文件') + return false + }, + onBaseImportExceed() { + this.$message.warning('每次只能上传一个 Excel 文件,更换时请先移除已选文件') + }, + onBaseImportPreview(resp) { + this.clearBaseImportPreview() + if (resp.code===0) this.baseImportRows=resp.data || [] + else this.$message.error(resp.msg || 'Excel读取失败') + }, + onBaseImportError() { + this.clearBaseImportPreview() + this.$message.error('Excel上传失败') + }, + async saveBaseImport() { + if (this.busy || !this.baseImportRows.length) return + if (this.baseImportRows.some(row=>row.errMsg)) { this.$message.warning('请修正校验未通过的记录后重新上传'); return } + if (!this.baseImportRemark.trim()) { this.$message.warning('请填写调整说明'); return } this.busy=true try { - await this.$confirm('确认一键下拨【'+params.year+'年 / '+scope+'】的全部待下拨'+typeName+'?范围包含所有分页,已下拨记录自动跳过,请先核对金额及历史下拨记录。','一键下拨',{type:'warning'}) - const resp=await this.request('batchConfirm',params) + const resp=await this.request('importBaseAmounts',{ + year:this.query.year,unionId:this.query.unionId || '', + data:JSON.stringify(this.baseImportRows),remark:this.baseImportRemark.trim() + }) + this.$message.success(resp.msg) + this.baseImportVisible=false + this.doSearch() + } catch(e) {} finally { this.busy=false } + }, + async resetBaseRecords() { + if (this.busy || !this.isCurrentYear) return + const params={year:this.query.year,unionId:this.query.unionId || '',amountMatch:this.query.amountMatch} + const scope=params.year+'年 / '+this.selectedUnionName+this.amountMatchScope + this.busy=true + try { + await this.$confirm('确认重置【'+scope+'】全部分页中的基础经费分配记录?已计入预算的金额将扣回,重置后可重新生成。','重置分配记录',{type:'warning',confirmButtonText:'确认重置'}) + const resp=await this.request('resetBaseRecords',params) this.$message.success(resp.msg) this.load() } catch(e) {} finally { this.busy=false } }, - async confirm(row) { - try { await this.$confirm('确认向【'+row.unionName+'】下拨 '+row.allocateMoney+' 元?此操作将计入年度预算,请核对历史下拨记录。','确认下拨',{type:'warning'}) } - catch(e) { return } - this.busy=true - try { await this.request('confirm',{type:this.tab,id:row.id,version:row.version}); this.$message.success('下拨成功'); this.load() } - catch(e) {} finally { this.busy=false } - }, showLogs(row) { this.logRecord={id:row.id,type:this.tab}; this.logPage=1; this.logsVisible=true; this.loadLogs() }, async loadLogs() { this.logsLoading=true diff --git a/src/main/resources/views/platform/zhgh/dayofficework/outlay/outlayManage/union/useDetail/quarterlyOutlayAllocate.js b/src/main/resources/views/platform/zhgh/dayofficework/outlay/outlayManage/union/useDetail/quarterlyOutlayAllocate.js index 6d79b60..a9abe62 100644 --- a/src/main/resources/views/platform/zhgh/dayofficework/outlay/outlayManage/union/useDetail/quarterlyOutlayAllocate.js +++ b/src/main/resources/views/platform/zhgh/dayofficework/outlay/outlayManage/union/useDetail/quarterlyOutlayAllocate.js @@ -4,22 +4,15 @@ let OUTLAY_MANAGE_UNION_QUARTERLY_ALLOCATE = { `
`, - dicts: ["OUTLAY_QUARTERLY"], data() { return { rowData: {}, @@ -29,11 +22,12 @@ let OUTLAY_MANAGE_UNION_QUARTERLY_ALLOCATE = { methods: { open(row) { this.rowData = row - this.$axios.post("/platform/outlay/outlayManage/unionUseDetail/queryQuarterlyList", { + this.tableData = [] + this.$axios.post("/platform/outlay/outlayManage/unionUseDetail/queryAnnualAllocateList", { year: row.year, unionId: row.unionId }).then(resp => { - if (resp.code === 0) { + if (this.rowData === row && resp.code === 0) { this.tableData = resp.data } }) 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 f610381..13880a2 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 @@ -52,6 +52,21 @@ layout("/layouts/platform.html"){ .payment-info-table .el-form-item { margin-bottom: 0; } + + .invoice-detail-dialog { + max-width: calc(100vw - 32px); + } + + .invoice-detail-dialog .el-dialog__body { + max-height: 70vh; + overflow-y: auto; + padding: 12px 24px; + } + + .invoice-detail-dialog .el-form-item__label { + line-height: 22px; + padding-bottom: 6px; + }
@@ -266,7 +281,7 @@ layout("/layouts/platform.html"){ - +
发票明细列表
- {{ isManualReimburseAmount ? '发票选填;有发票时报销金额不得超过发票总额,无发票仍可提交。' : '发票选填;无发票时按支付信息汇总报销金额。' }}上传后须通过验真,每条明细对应一张发票图片或单页 PDF。 + 提交时至少上传一张发票,上传后须通过验真。每条明细对应一张发票图片或单页 PDF。
- 发票选填,上传须验真 + 提交时必填 新增发票 @@ -677,10 +692,11 @@ layout("/layouts/platform.html"){ :title="invoiceDialogMode === 'edit' ? '编辑发票明细' : '新增发票明细'" :visible.sync="invoiceDialogVisible" append-to-body - width="820px"> - - + custom-class="invoice-detail-dialog" + top="5vh" + width="1000px"> + + - + - + - + - + - + - + - + @@ -1647,7 +1663,10 @@ layout("/layouts/platform.html"){ return false } 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) { @@ -1710,6 +1729,11 @@ layout("/layouts/platform.html"){ this.syncLegacyPaymentFields(submitData.paymentDetails, submitData) return submitData }, + updateReimburseAmount(value) { + this.$set(this.formData, "money", value) + const details = this.formData.paymentDetails || [] + if (details.length === 1) this.$set(details[0], "money", value) + }, // 金额输入允许编辑中的空值和末尾小数点;非法字符或超出两位小数时恢复原值,避免改变金额含义。 inputPaymentMoney(row, value) { const previous = row.money @@ -1831,18 +1855,8 @@ layout("/layouts/platform.html"){ // 保存 onSave() { if (this.formLoading || this.recognizingInvoice) return - if (!this.validateInvoiceDetailsBeforeSubmit()) { - this.$set(this, "activeTabName", "invoice") - return - } this.rebuildInvoiceSummary() this.normalizeFundBalance() - const paymentDetails = this.preparePaymentDetailsForSubmit() - // 保存草稿也必须填写完整支付信息,避免非法金额和空明细入库。 - if (!this.validatePaymentDetailsBeforeSubmit(paymentDetails)) { - this.$set(this, "activeTabName", "invoice") - return - } this.$confirm("您确定保存吗?", "提示", { confirmButtonText: "确定", cancelButtonText: "取消", @@ -1850,7 +1864,7 @@ layout("/layouts/platform.html"){ }).then(() => { if (this.formLoading) return this.$set(this, "formLoading", true) - this.$axios.post("/platform/unionReimburse/apply/save", {data: JSON.stringify(this.buildSubmitFormData(paymentDetails))}).then(res => { + this.$axios.post("/platform/unionReimburse/apply/save", {data: JSON.stringify(this.buildSubmitFormData())}).then(res => { if (res.code === 0) { this.$message.success(res.msg) commonUtil.pjaxPush("/platform/unionReimburse/mine/index") diff --git a/src/main/resources/views/platform/zhgh/dayofficework/unionReimburse/info.js b/src/main/resources/views/platform/zhgh/dayofficework/unionReimburse/info.js index c42facd..0fbdd2e 100644 --- a/src/main/resources/views/platform/zhgh/dayofficework/unionReimburse/info.js +++ b/src/main/resources/views/platform/zhgh/dayofficework/unionReimburse/info.js @@ -1,8 +1,8 @@ const unionReimburseInfo = { template: /*language=HTML*/ `
-
- 申请信息 +
+ 申请信息 点击查看流程图
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 3a4fb98..6fcd2b4 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 @@ -682,7 +682,8 @@ layout("/layouts/platform_h5.html"){ > @@ -773,8 +774,8 @@ layout("/layouts/platform_h5.html"){ -
电子发票(选填)
-
{{ isManualReimburseAmount ? '有发票时报销金额不得超过发票总额,无发票仍可提交;上传的发票仍须通过验真。' : '无发票时按支付信息汇总报销金额;上传的发票仍须通过验真。' }}
+
电子发票(提交时必填)
+
提交时至少上传一张发票,上传后须通过验真。
{ this.$toast.clear() if (res.code === 0) {