This commit is contained in:
=
2026-05-08 20:08:14 +08:00
parent 9d22c1c4e2
commit 7e3fca6a7f
7 changed files with 279 additions and 18 deletions
@@ -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)
@@ -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
@@ -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<UnionReimburse> {
*/
List<UnionReimburseBankHistoryVO> getUserBankHistory(String payer);
/**
* 查询付款人候选人;协会经费按所选协会过滤,只返回该协会成员。
*
* @param keyword 姓名或工号关键字
* @param reimburseFundSource 当前选择的经费来源
* @param clubId 协会经费对应的协会ID
* @return 可选付款人列表
*/
List<NutMap> listPayerUser(String keyword, String reimburseFundSource, String clubId);
/**
* 校验发票号码是否与历史报销记录重复。
*/
@@ -48,6 +59,11 @@ public interface UnionReimburseService extends BaseService<UnionReimburse> {
*/
Result reviewApply(String reimburseId, String reviewOpinion, int submitType);
/**
* 管理员修改实际报销金额,已报销成功的数据需要同步调整经费预算管理台账。
*/
Result updateActuallyAmount(String id, Double realMoney);
/**
* 批量审核选中的待审核报销申请。
*
@@ -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<UnionReimburse> 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<UnionReimburse> 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<UnionReimburse> i
return this.listVO(sql, UnionReimburseBankHistoryVO.class);
}
/**
* 付款人查询要跟随经费来源;选择协会经费时,只允许从当前协会会员中选择付款人。
*/
@Override
public List<NutMap> 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<UnionReimburse> 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<UnionReimburse> 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<UnionReimburse> 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<UnionReimburse> 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();
@@ -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 = "请选择协会"
},
// 经费来源查询
@@ -456,15 +456,15 @@ layout("/layouts/platform_h5.html"){
<van-field
v-model="formData.deathTime"
name="deathTime"
label="去时间"
label="去时间"
readonly
placeholder="请选择去时间"
placeholder="请选择去时间"
@click="openDatePicker('showDeathTimePicker')"
clickable
is-link
></van-field>
<van-popup position="bottom" round v-model:show="showDeathTimePicker">
<van-datetime-picker type="date" title="选择去时间" :min-date="minDate" :max-date="maxDate"
<van-datetime-picker type="date" title="选择去时间" :min-date="minDate" :max-date="maxDate"
@cancel="closePopup('showDeathTimePicker')"
@confirm="onDateConfirm('deathTime', 'showDeathTimePicker', $event)"></van-datetime-picker>
</van-popup>
@@ -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()
+1 -1
View File
@@ -78,7 +78,7 @@ public class JugTest {
@Test
public void sendMsg() {
smsService.send("20182040", "这是一条由智慧工会系统发出的测试消息。");
// smsService.send("20182040", "这是一条由智慧工会系统发出的测试消息。");
}
@Test