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 10254b56..dc5ea19a 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 @@ -180,6 +180,14 @@ public class UnionReimburseApplyController { return Result.success(pagination.getList()); } + @At + @ApiOperation("查询付款人") + @SaCheckPermission(value = {"unionReimburse.apply", "h5.unionReimburse.apply"}, mode = SaMode.OR) + @SLog(type = "unionReimburse", tag = "查询付款人", msg = "按经费来源查询付款人") + public Result listPayerUser(String keyword, String reimburseFundSource, String clubId) { + return Result.success(unionReimburseService.listPayerUser(keyword, reimburseFundSource, clubId)); + } + @At @ApiOperation("查询职工慰问类型") @SaCheckPermission(value = {"unionReimburse.apply", "h5.unionReimburse.apply"}, mode = SaMode.OR) 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 05b9046b..766f6aff 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 @@ -27,12 +27,14 @@ import io.swagger.annotations.Api; import io.swagger.annotations.ApiOperation; import lombok.extern.slf4j.Slf4j; import org.apache.poi.ss.usermodel.Workbook; +import org.nutz.aop.interceptor.ioc.TransAop; import org.nutz.dao.Chain; import org.nutz.dao.Cnd; import org.nutz.dao.Dao; import org.nutz.dao.Sqls; import org.nutz.dao.sql.Sql; import org.nutz.dao.util.cri.SqlExpressionGroup; +import org.nutz.ioc.aop.Aop; import org.nutz.ioc.loader.annotation.Inject; import org.nutz.ioc.loader.annotation.IocBean; import org.nutz.lang.util.NutMap; @@ -118,12 +120,10 @@ public class UnionReimburseCollectController { } @At + @Aop(TransAop.READ_COMMITTED) @SaCheckPermission(value = {"unionReimburse.collect", "h5.unionReimburse.collect"}, mode = SaMode.OR) public Result updateActuallyAmount(String id,Double realMoney) { - UnionReimburse unionReimburse = unionReimburseService.fetch(id); - unionReimburse.setRealMoney(realMoney); - unionReimburseService.updateIgnoreNull(unionReimburse); - return Result.success(); + return unionReimburseService.updateActuallyAmount(id, realMoney); } @At 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 d2dfdcc5..9d3f4bbc 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 @@ -6,6 +6,7 @@ import com.budwk.app.zhgh.dayofficework.unionReimburse.model.UnionReimburse; import com.budwk.app.zhgh.dayofficework.unionReimburse.param.UnionReimbursePageForm; import com.budwk.app.zhgh.dayofficework.unionReimburse.vo.UnionReimburseBankHistoryVO; import org.nutz.dao.sql.Sql; +import org.nutz.lang.util.NutMap; import java.util.List; @@ -28,6 +29,16 @@ public interface UnionReimburseService extends BaseService { */ List getUserBankHistory(String payer); + /** + * 查询付款人候选人;协会经费按所选协会过滤,只返回该协会成员。 + * + * @param keyword 姓名或工号关键字 + * @param reimburseFundSource 当前选择的经费来源 + * @param clubId 协会经费对应的协会ID + * @return 可选付款人列表 + */ + List listPayerUser(String keyword, String reimburseFundSource, String clubId); + /** * 校验发票号码是否与历史报销记录重复。 */ @@ -48,6 +59,11 @@ public interface UnionReimburseService extends BaseService { */ Result reviewApply(String reimburseId, String reviewOpinion, int submitType); + /** + * 管理员修改实际报销金额,已报销成功的数据需要同步调整经费预算管理台账。 + */ + Result updateActuallyAmount(String id, Double realMoney); + /** * 批量审核选中的待审核报销申请。 * 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 5dad4838..3be043ba 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 @@ -4,6 +4,7 @@ import cn.hutool.core.date.DateUtil; import cn.hutool.core.util.ObjectUtil; import cn.hutool.core.util.StrUtil; import com.budwk.app.base.constant.RoleConstant; +import com.budwk.app.base.page.Pagination; import com.budwk.app.base.result.Result; import com.budwk.app.base.service.impl.BaseServiceImpl; import com.budwk.app.sys.models.Sys_file; @@ -123,6 +124,7 @@ public class UnionReimburseServiceImpl extends BaseServiceImpl i if (needInvoiceDetails(unionReimburse)) { fillInvoiceSummary(unionReimburse); } + fillDefaultRealMoney(unionReimburse); if (stateId == 2) { Result validateResult = validateBeforeSubmit(unionReimburse); if (validateResult != null) { @@ -146,11 +148,7 @@ public class UnionReimburseServiceImpl extends BaseServiceImpl i unionReimburse.setDocumentNo(oldRecord.getDocumentNo()); } unionReimburse.setStateId(stateId); - if ("UNION_REIMBURSE_PROJECT_1".equals(unionReimburse.getReimburseProject())) { - unionReimburse.setRealMoney(unionReimburse.getCondolenceMoney()); - } else { - unionReimburse.setRealMoney(unionReimburse.getMoney()); - } + fillDefaultRealMoney(unionReimburse); this.dao().insertOrUpdate(unionReimburse); saveInvoiceDetails(unionReimburse.getId(), unionReimburse.getInvoiceDetails()); return Result.success(stateId == 2 ? "提交成功" : "保存成功"); @@ -209,6 +207,55 @@ public class UnionReimburseServiceImpl extends BaseServiceImpl i return this.listVO(sql, UnionReimburseBankHistoryVO.class); } + /** + * 付款人查询要跟随经费来源;选择协会经费时,只允许从当前协会会员中选择付款人。 + */ + @Override + public List listPayerUser(String keyword, String reimburseFundSource, String clubId) { + boolean clubFundSource = "UNION_REIMBURSE_FUND_SOURCE_3".equals(reimburseFundSource); + if (clubFundSource && StrUtil.isBlank(clubId)) { + return new ArrayList<>(); + } + String clubJoinSql = clubFundSource ? "INNER JOIN club_user cu ON cu.userId = u.id" : ""; + Sql sql = Sqls.create(""" + SELECT + u.id, + u.username AS userName, + u.loginname AS loginName, + u.sex, + u.mobile, + u.technicalTitle, + IFNULL(u.unitname, '暂无') AS unitName, + u.unitid AS unitId, + u.unionid AS unionId, + u.unionname AS unionName, + DATE(u.birthday) AS birthday, + u.idCard, + u.unionCode + FROM + vw_user u + %s + $condition + """.formatted(clubJoinSql)); + Cnd cnd = Cnd.NEW(); + if (StrUtil.isNotBlank(keyword)) { + SqlExpressionGroup seg = new SqlExpressionGroup(); + seg.or("u.loginname", "like", "%" + keyword + "%"); + seg.or("u.username", "like", "%" + keyword + "%"); + cnd.and(seg); + } + cnd.and("u.id", "!=", SecurityUtil.getUserId()); + if (clubFundSource) { + cnd.and("cu.clubId", "=", clubId); + } else if (!AuthUtil.hasRoleOr(RoleConstant.SYSADMIN.name(), RoleConstant.SCHOOL_UNION_ADMIN.name())) { + cnd.and("u.unionid", "=", SecurityUtil.getUnionId()); + } + cnd.groupBy("u.id"); + sql.setCondition(cnd); + Pagination pagination = this.listPageMap(1, 50, sql); + return pagination.getList(); + } + @Override public Result checkInvoiceDuplicate(String invoiceNo, String reimburseId) { String currentInvoiceNo = StrUtil.blankToDefault(StrUtil.trim(invoiceNo), ""); @@ -347,6 +394,37 @@ public class UnionReimburseServiceImpl extends BaseServiceImpl i return Result.success("一键审核完成,共处理" + reimbursements.size() + "条记录"); } + /** + * 管理员修改实际金额时,报销成功的数据已经写入预算台账,需要按差额同步已使用额度和使用明细。 + */ + @Override + public Result updateActuallyAmount(String id, Double realMoney) { + if (StrUtil.isBlank(id)) { + return Result.error("报销记录ID不能为空"); + } + BigDecimal newMoney = normalizeActualMoney(realMoney); + if (newMoney == null || newMoney.compareTo(BigDecimal.ZERO) <= 0) { + return Result.error("实际报销金额必须大于0"); + } + UnionReimburse dbRecord = this.fetch(id); + if (dbRecord == null) { + return Result.error("未找到对应的报销记录"); + } + if (Integer.valueOf(3).equals(dbRecord.getStateId())) { + OutlayUseDetail oldDetail = dao().fetch(OutlayUseDetail.class, Cnd.where("outlayReimburseId", "=", dbRecord.getId())); + BigDecimal oldMoney = oldDetail != null && oldDetail.getAdjustMoney() != null + ? oldDetail.getAdjustMoney() + : getApplyMoney(dbRecord); + Result syncResult = syncApprovedActualAmount(dbRecord, oldDetail, oldMoney, newMoney); + if (syncResult != null && syncResult.getCode() != 0) { + return syncResult; + } + } + dbRecord.setRealMoney(newMoney.doubleValue()); + this.updateIgnoreNull(dbRecord); + return Result.success(); + } + private void normalizeInvoiceDetails(UnionReimburse unionReimburse) { if (unionReimburse.getInvoiceDetails() == null) { unionReimburse.setInvoiceDetails(new ArrayList<>()); @@ -392,6 +470,13 @@ public class UnionReimburseServiceImpl extends BaseServiceImpl i unionReimburse.setInvoiceNumber(String.valueOf(invoiceFileCount)); } + /** + * 默认实际金额跟随申请金额,后续管理员可在汇总页按最终报销金额调整。 + */ + private void fillDefaultRealMoney(UnionReimburse unionReimburse) { + unionReimburse.setRealMoney(getDeclaredApplyMoney(unionReimburse).doubleValue()); + } + /** * 提交前补一层后端校验,防止前端绕过校验直接提交。 */ @@ -525,6 +610,117 @@ public class UnionReimburseServiceImpl extends BaseServiceImpl i return Result.error("不支持的经费来源"); } + /** + * 已通过报销单修改实际金额时,用差额同步预算主表和使用明细,避免重复全量扣减。 + */ + private Result syncApprovedActualAmount(UnionReimburse unionReimburse, OutlayUseDetail oldDetail, + BigDecimal oldMoney, BigDecimal newMoney) { + BigDecimal diffMoney = newMoney.subtract(safeMoney(oldMoney)).setScale(2, RoundingMode.HALF_UP); + String reimburseFundSource = unionReimburse.getReimburseFundSource(); + if ("UNION_REIMBURSE_FUND_SOURCE_1".equals(reimburseFundSource)) { + OutlayManageSchool outlayManageSchool = dao().fetch(OutlayManageSchool.class, + Cnd.where("year", "=", DateUtil.thisYear())); + if (outlayManageSchool == null) { + return Result.error("校工会经费未分配"); + } + Result quotaResult = updateSchoolUsedQuota(outlayManageSchool, diffMoney); + if (quotaResult.getCode() != 0) { + return quotaResult; + } + saveOrUpdateUseDetail(unionReimburse, oldDetail, outlayManageSchool.getId(), newMoney); + return Result.success(); + } + if ("UNION_REIMBURSE_FUND_SOURCE_2".equals(reimburseFundSource)) { + OutlayManageUnion outlayManageUnion = dao().fetch(OutlayManageUnion.class, + Cnd.where("unionId", "=", unionReimburse.getUnionId()).and("year", "=", DateUtil.thisYear())); + if (outlayManageUnion == null) { + return Result.error("分工会经费未分配"); + } + Result quotaResult = updateUnionUsedQuota(outlayManageUnion, diffMoney); + if (quotaResult.getCode() != 0) { + return quotaResult; + } + saveOrUpdateUseDetail(unionReimburse, oldDetail, outlayManageUnion.getId(), newMoney); + return Result.success(); + } + if ("UNION_REIMBURSE_FUND_SOURCE_3".equals(reimburseFundSource)) { + OutlayManageClub outlayManageClub = dao().fetch(OutlayManageClub.class, + Cnd.where("clubId", "=", unionReimburse.getClubId()).and("year", "=", DateUtil.thisYear())); + if (outlayManageClub == null) { + return Result.error("该协会本年度未生成预算记录,请先在协会预算分配中补分配"); + } + Result quotaResult = updateClubUsedQuota(outlayManageClub, diffMoney); + if (quotaResult.getCode() != 0) { + return quotaResult; + } + saveOrUpdateUseDetail(unionReimburse, oldDetail, outlayManageClub.getId(), newMoney); + return Result.success(); + } + return Result.error("不支持的经费来源"); + } + + private Result updateSchoolUsedQuota(OutlayManageSchool outlayManageSchool, BigDecimal diffMoney) { + BigDecimal newUsedQuota = buildNewUsedQuota(outlayManageSchool.getTotalQuota(), outlayManageSchool.getUsedQuota(), diffMoney, "校工会经费"); + if (newUsedQuota == null) { + return Result.error("校工会经费余额不足"); + } + outlayManageSchool.setUsedQuota(newUsedQuota); + dao().updateIgnoreNull(outlayManageSchool); + return Result.success(); + } + + private Result updateUnionUsedQuota(OutlayManageUnion outlayManageUnion, BigDecimal diffMoney) { + BigDecimal newUsedQuota = buildNewUsedQuota(outlayManageUnion.getTotalQuota(), outlayManageUnion.getUsedQuota(), diffMoney, "分工会经费"); + if (newUsedQuota == null) { + return Result.error("分工会经费余额不足"); + } + outlayManageUnion.setUsedQuota(newUsedQuota); + dao().updateIgnoreNull(outlayManageUnion); + return Result.success(); + } + + private Result updateClubUsedQuota(OutlayManageClub outlayManageClub, BigDecimal diffMoney) { + BigDecimal newUsedQuota = buildNewUsedQuota(outlayManageClub.getTotalQuota(), outlayManageClub.getUsedQuota(), diffMoney, "协会经费"); + if (newUsedQuota == null) { + return Result.error("协会经费余额不足"); + } + outlayManageClub.setUsedQuota(newUsedQuota); + dao().updateIgnoreNull(outlayManageClub); + return Result.success(); + } + + /** + * 计算调整后的已使用额度,调增时不能超过总额度,调减时不能小于0。 + */ + private BigDecimal buildNewUsedQuota(BigDecimal totalQuota, BigDecimal usedQuota, BigDecimal diffMoney, String outlayName) { + BigDecimal safeTotalQuota = safeMoney(totalQuota); + BigDecimal safeUsedQuota = safeMoney(usedQuota); + BigDecimal safeDiffMoney = safeMoney(diffMoney); + BigDecimal newUsedQuota = safeUsedQuota.add(safeDiffMoney).setScale(2, RoundingMode.HALF_UP); + if (safeDiffMoney.compareTo(BigDecimal.ZERO) == 0) { + return newUsedQuota; + } + if (newUsedQuota.compareTo(BigDecimal.ZERO) < 0) { + return null; + } + if (newUsedQuota.compareTo(safeTotalQuota) > 0) { + log.warn("{}实际金额调增后超出预算,总额度:{}, 已使用:{}, 调整差额:{}", outlayName, safeTotalQuota, safeUsedQuota, diffMoney); + return null; + } + return newUsedQuota; + } + + private void saveOrUpdateUseDetail(UnionReimburse unionReimburse, OutlayUseDetail oldDetail, + String outlayManageId, BigDecimal newMoney) { + if (oldDetail == null) { + insertUseDetail(unionReimburse, outlayManageId, newMoney); + return; + } + oldDetail.setOutlayManageId(outlayManageId); + oldDetail.setAdjustMoney(newMoney); + dao().updateIgnoreNull(oldDetail); + } + /** * 一张报销单只写入一条预算使用详情,避免重复通过或重复点击造成重复明细。 */ @@ -548,11 +744,29 @@ public class UnionReimburseServiceImpl extends BaseServiceImpl i } private BigDecimal getApplyMoney(UnionReimburse unionReimburse) { + if (unionReimburse.getRealMoney() != null) { + return BigDecimal.valueOf(unionReimburse.getRealMoney()).setScale(2, RoundingMode.HALF_UP); + } + return getDeclaredApplyMoney(unionReimburse); + } + + private BigDecimal getDeclaredApplyMoney(UnionReimburse unionReimburse) { Double money = "UNION_REIMBURSE_PROJECT_1".equals(unionReimburse.getReimburseProject()) ? unionReimburse.getCondolenceMoney() : unionReimburse.getMoney(); return BigDecimal.valueOf(money == null ? 0D : money).setScale(2, RoundingMode.HALF_UP); } + private BigDecimal normalizeActualMoney(Double realMoney) { + if (realMoney == null || realMoney.isNaN() || realMoney.isInfinite()) { + return null; + } + return realMoney == null ? null : BigDecimal.valueOf(realMoney).setScale(2, RoundingMode.HALF_UP); + } + + private BigDecimal safeMoney(BigDecimal money) { + return money == null ? BigDecimal.ZERO : money.setScale(2, RoundingMode.HALF_UP); + } + private String buildUseDetailProjectName(UnionReimburse unionReimburse) { if (StrUtil.isNotBlank(unionReimburse.getActivityName())) { return unionReimburse.getActivityName(); 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 92b8e922..a9e92b45 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 @@ -839,12 +839,30 @@ layout("/layouts/platform.html"){ }, selectQueryUserForPayer(keyword, options) { options.length = 0 - this.$axios.post("/platform/unionReimburse/apply/listUser", {keyword: keyword}).then((res) => { + if (this.formData.reimburseFundSource === "UNION_REIMBURSE_FUND_SOURCE_3" && !this.formData.clubId) { + this.$message.warning("请先选择协会") + return + } + this.$axios.post("/platform/unionReimburse/apply/listPayerUser", { + keyword: keyword, + reimburseFundSource: this.formData.reimburseFundSource, + clubId: this.formData.clubId + }).then((res) => { if (res.code === 0) { options.push(...res.data) } }) }, + resetPayerInfo() { + this.$set(this.formData, "payer", null) + this.$set(this.formData, "payerName", null) + this.$set(this.formData, "payerLoginname", null) + this.$set(this.formData, "bankUserName", null) + this.$set(this.formData, "bankCardNumber", null) + this.$set(this.formData, "bankOfDeposit", null) + this.$set(this, "historyData", []) + this.$set(this, "payerOptions", []) + }, appendCurrentPayerOption() { if (!this.formData.payer) { return @@ -929,7 +947,12 @@ layout("/layouts/platform.html"){ if (this.formData.payer) { await this.getUserBankHistory(); } else { - this.historyData = [] + this.$set(this, "historyData", []) + this.$set(this.formData, "payerName", null) + this.$set(this.formData, "payerLoginname", null) + this.$set(this.formData, "bankUserName", null) + this.$set(this.formData, "bankCardNumber", null) + this.$set(this.formData, "bankOfDeposit", null) } const user = this.payerOptions.find(o => o.id === this.formData.payer) if (user) { @@ -1355,6 +1378,7 @@ layout("/layouts/platform.html"){ this.$set(this.formData, "clubId", this.clubOptions[0].id) this.$set(this.formData, "clubName", this.clubOptions[0].clubName) } + this.resetPayerInfo() this.reimburseFundSourceChange() }, clubChange() { @@ -1363,11 +1387,13 @@ layout("/layouts/platform.html"){ if (selectedClub) { this.$set(this.formData, "clubName", selectedClub.clubName) } + this.resetPayerInfo() this.reimburseFundSourceChange() return } this.$set(this.formData, "clubName", null) this.$set(this.formData, "fundBalance", null) + this.resetPayerInfo() this.fundBalanceText = "请选择协会" }, // 经费来源查询 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 3075245b..1e3181b3 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 @@ -456,15 +456,15 @@ layout("/layouts/platform_h5.html"){ - @@ -1423,9 +1423,6 @@ layout("/layouts/platform_h5.html"){ this.normalizeContactFields() this.normalizeCondolenceDateFields() this.rebuildInvoiceSummary() - if (!this.validateInvoiceDetailsBeforeSubmit()) { - return - } this.$axios.post("/platform/unionReimburse/apply/save", { data: JSON.stringify(this.formData) }) .then((res) => { this.$toast.clear() diff --git a/src/test/java/com/budwk/app/JugTest.java b/src/test/java/com/budwk/app/JugTest.java index ec41b19f..96705393 100644 --- a/src/test/java/com/budwk/app/JugTest.java +++ b/src/test/java/com/budwk/app/JugTest.java @@ -78,7 +78,7 @@ public class JugTest { @Test public void sendMsg() { - smsService.send("20182040", "这是一条由智慧工会系统发出的测试消息。"); +// smsService.send("20182040", "这是一条由智慧工会系统发出的测试消息。"); } @Test