🐛 修复收款流水继续认领与付款可付口径,并改用OA登录名提交流程

收款流水部分认领预填关联结算单并修正认领完成判定;正式结算尾款扣减预付占用;预付驳回重提纠正结算引用;临时额度取消强制项目额度校验;用户增加OA登录名供MK流程使用。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-10-19 17:12:22 +08:00
parent 68a5d581f3
commit fce582e80b
15 changed files with 337 additions and 84 deletions
@@ -39,6 +39,7 @@ import java.time.LocalDate;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Map;
/** /**
* 收款流水视图实体类 * 收款流水视图实体类
@@ -91,4 +92,9 @@ public class ReceiptFlowVO extends KingdeeReceiptFlow {
private BigDecimal receiptAmountEnd; private BigDecimal receiptAmountEnd;
@TableField(exist = false) @TableField(exist = false)
private List<ReceiptFlowRecord> claimRecords = new ArrayList<>(); private List<ReceiptFlowRecord> claimRecords = new ArrayList<>();
/**
* 本流水历史关联且仍有剩余可认领金额的应收正式结算单(继续认领时预填)
*/
@TableField(exist = false)
private List<Map<String, Object>> relatedSettlements = new ArrayList<>();
} }
@@ -132,6 +132,10 @@ public class User extends TenantEntity implements IPhone {
* 是否是oa的账号 * 是否是oa的账号
*/ */
private Integer isOa; private Integer isOa;
/**
* OA登录名
*/
private String oaLoginId;
/** /**
* OA上次更新时间(OA 的 modified 字段),仅同步比对使用 * OA上次更新时间(OA 的 modified 字段),仅同步比对使用
*/ */
@@ -11,7 +11,6 @@ import lombok.AllArgsConstructor;
import org.springblade.core.boot.ctrl.BladeController; import org.springblade.core.boot.ctrl.BladeController;
import org.springblade.core.mp.support.Condition; import org.springblade.core.mp.support.Condition;
import org.springblade.core.mp.support.Query; import org.springblade.core.mp.support.Query;
import org.springblade.core.secure.utils.AuthUtil;
import org.springblade.core.tool.api.R; import org.springblade.core.tool.api.R;
import org.springblade.core.tool.utils.StringUtil; import org.springblade.core.tool.utils.StringUtil;
import org.springblade.process.pojo.dto.ApprovalDTO; import org.springblade.process.pojo.dto.ApprovalDTO;
@@ -49,7 +48,6 @@ public class BusinessProcessController extends BladeController {
if (param == null) { if (param == null) {
param = new ApprovalDTO(); param = new ApprovalDTO();
} }
param.setLoginName(AuthUtil.getUserAccount());
IPage<ApprovalVO> pages = businessProcessService.queryMkApprovalList(Condition.getPage(query), param); IPage<ApprovalVO> pages = businessProcessService.queryMkApprovalList(Condition.getPage(query), param);
return R.data(pages); return R.data(pages);
} }
@@ -71,7 +71,7 @@ public interface IBusinessProcessService extends IService<BusinessProcess> {
* 获取流程当前节点详情 * 获取流程当前节点详情
* *
* @param processInstanceId 流程实例id * @param processInstanceId 流程实例id
* @param loginName MK登录名(手机号),可为空,为空时按流程发起人解析 * @param loginName MK登录名(OA登录名),可为空,为空时按流程发起人解析
* @return 当前节点列表 * @return 当前节点列表
*/ */
List<?> getCurrentNodes(String processInstanceId, String loginName); List<?> getCurrentNodes(String processInstanceId, String loginName);
@@ -80,7 +80,7 @@ public interface IBusinessProcessService extends IService<BusinessProcess> {
* 获取流程实例详情 * 获取流程实例详情
* *
* @param processInstanceId 流程实例id * @param processInstanceId 流程实例id
* @param loginName MK登录名(手机号),可为空,为空时按流程发起人解析 * @param loginName MK登录名(OA登录名),可为空,为空时按流程发起人解析
* @return 流程实例详情 * @return 流程实例详情
*/ */
Object getProcessInfo(String processInstanceId, String loginName); Object getProcessInfo(String processInstanceId, String loginName);
@@ -132,9 +132,9 @@ public class BusinessProcessServiceImpl extends ServiceImpl<BusinessProcessMappe
public String processSubmit(MKProcessCreateDTO param) { public String processSubmit(MKProcessCreateDTO param) {
AssertUtils.notNull(param, "提交流程参数不能为空"); AssertUtils.notNull(param, "提交流程参数不能为空");
AssertUtils.notBlank(param.getTemplateCode(), "模板编码不能为空"); AssertUtils.notBlank(param.getTemplateCode(), "模板编码不能为空");
// 前端拿到的手机号可能经 @Sensitive 脱敏(如 137****8880),这里从库取真实手机号覆盖 // MK loginName 使用当前用户 OA 登录名,未配置则不允许提交
String loginName = resolveCurrentUserPhone(); String loginName = resolveCurrentUserOaLoginId();
AssertUtils.notBlank(loginName, "当前用户手机号为空,无法提交审核流"); AssertUtils.notBlank(loginName, "当前用户OA登录名为空,无法提交审核流");
param.setSubmitIdentity(loginName); param.setSubmitIdentity(loginName);
param.setLoginName(loginName); param.setLoginName(loginName);
String bizType = param.getBizType(); String bizType = param.getBizType();
@@ -258,7 +258,7 @@ public class BusinessProcessServiceImpl extends ServiceImpl<BusinessProcessMappe
resolvedLoginName = businessProcess.getPromoterLoginName(); resolvedLoginName = businessProcess.getPromoterLoginName();
} }
if (StringUtil.isBlank(resolvedLoginName)) { if (StringUtil.isBlank(resolvedLoginName)) {
resolvedLoginName = resolveCurrentUserPhone(); resolvedLoginName = resolveCurrentUserOaLoginId();
} }
if (StringUtil.isBlank(resolvedLoginName)) { if (StringUtil.isBlank(resolvedLoginName)) {
return false; return false;
@@ -291,10 +291,10 @@ public class BusinessProcessServiceImpl extends ServiceImpl<BusinessProcessMappe
} }
String loginName = businessProcess.getPromoterLoginName(); String loginName = businessProcess.getPromoterLoginName();
if (StringUtil.isBlank(loginName)) { if (StringUtil.isBlank(loginName)) {
loginName = resolveCurrentUserPhone(); loginName = resolveCurrentUserOaLoginId();
} }
if (StringUtil.isBlank(loginName)) { if (StringUtil.isBlank(loginName)) {
log.warn("查询MK流程实例跳过,登录名为空 bizId={} processInstanceId={}", log.warn("查询MK流程实例跳过,OA登录名为空 bizId={} processInstanceId={}",
bizId, businessProcess.getProcessInstanceId()); bizId, businessProcess.getProcessInstanceId());
return false; return false;
} }
@@ -357,9 +357,9 @@ public class BusinessProcessServiceImpl extends ServiceImpl<BusinessProcessMappe
} }
String loginName = businessProcess.getPromoterLoginName(); String loginName = businessProcess.getPromoterLoginName();
if (StringUtil.isBlank(loginName)) { if (StringUtil.isBlank(loginName)) {
loginName = resolveCurrentUserPhone(); loginName = resolveCurrentUserOaLoginId();
} }
AssertUtils.notBlank(loginName, "当前用户手机号为空,无法删除审核流"); AssertUtils.notBlank(loginName, "当前用户OA登录名为空,无法删除审核流");
log.info("调用mk processDelete processInstanceId={} loginName={} bizId={} formInstanceId={}", log.info("调用mk processDelete processInstanceId={} loginName={} bizId={} formInstanceId={}",
businessProcess.getProcessInstanceId(), loginName, bizId, businessProcess.getFormInstanceId()); businessProcess.getProcessInstanceId(), loginName, bizId, businessProcess.getFormInstanceId());
boolean deleted = mkService.processDelete(businessProcess.getProcessInstanceId(), loginName); boolean deleted = mkService.processDelete(businessProcess.getProcessInstanceId(), loginName);
@@ -563,28 +563,19 @@ public class BusinessProcessServiceImpl extends ServiceImpl<BusinessProcessMappe
} }
/** /**
* 从当前登录用户实体读取真实手机号(库内 SM2 密文先解密,绕过接口脱敏) * 从当前登录用户实体读取 OA 登录名,作为 MK 接口 loginName
*/ */
private String resolveCurrentUserPhone() { private String resolveCurrentUserOaLoginId() {
Long userId = AuthUtil.getUserId(); Long userId = AuthUtil.getUserId();
if (userId != null) { if (userId == null) {
User user = userService.getById(userId);
if (user != null) {
userService.decryptPhone(user);
if (StringUtil.isNotBlank(user.getPhone()) && !user.getPhone().contains("*")) {
return user.getPhone().trim();
}
if (StringUtil.isNotBlank(user.getAccount()) && user.getAccount().matches("^1\\d{10}$")) {
return user.getAccount().trim();
}
}
}
String account = AuthUtil.getUserAccount();
if (StringUtil.isNotBlank(account) && account.matches("^1\\d{10}$")) {
return account.trim();
}
return null; return null;
} }
User user = userService.getById(userId);
if (user == null || StringUtil.isBlank(user.getOaLoginId())) {
return null;
}
return user.getOaLoginId().trim();
}
@Transactional(rollbackFor = Exception.class) @Transactional(rollbackFor = Exception.class)
@Override @Override
@@ -703,8 +694,8 @@ public class BusinessProcessServiceImpl extends ServiceImpl<BusinessProcessMappe
processInstanceId = businessProcess.getProcessInstanceId(); processInstanceId = businessProcess.getProcessInstanceId();
} }
try { try {
String loginName = resolveCurrentUserPhone(); String loginName = resolveCurrentUserOaLoginId();
AssertUtils.notBlank(loginName, "当前用户手机号为空,无法获取审核跳转地址"); AssertUtils.notBlank(loginName, "当前用户OA登录名为空,无法获取审核跳转地址");
String mkApprovalUrl = mkService.getMKApprovalUrl(processInstanceId, loginName); String mkApprovalUrl = mkService.getMKApprovalUrl(processInstanceId, loginName);
AssertUtils.notBlank(mkApprovalUrl, "获取mk审批页面链接异常"); AssertUtils.notBlank(mkApprovalUrl, "获取mk审批页面链接异常");
return mkApprovalUrl; return mkApprovalUrl;
@@ -921,6 +912,9 @@ public class BusinessProcessServiceImpl extends ServiceImpl<BusinessProcessMappe
@Override @Override
public IPage<ApprovalVO> queryMkApprovalList(IPage<ApprovalVO> page, ApprovalDTO param) { public IPage<ApprovalVO> queryMkApprovalList(IPage<ApprovalVO> page, ApprovalDTO param) {
String loginName = resolveCurrentUserOaLoginId();
AssertUtils.notBlank(loginName, "当前用户OA登录名为空,无法查询审核列表");
param.setLoginName(loginName);
if (MKDoc.RELATED.getCode().equals(param.getDocType())) { if (MKDoc.RELATED.getCode().equals(param.getDocType())) {
// 我参与的,调用流程列表接口 // 我参与的,调用流程列表接口
MKProcessDTO processParam = approvalConvert.dto2mk(param); MKProcessDTO processParam = approvalConvert.dto2mk(param);
@@ -70,6 +70,7 @@ public interface UserConvert {
@Mapping(source = "lastname", target = "realName") @Mapping(source = "lastname", target = "realName")
@Mapping(source = "mobile", target = "phone") @Mapping(source = "mobile", target = "phone")
@Mapping(source = "email", target = "email") @Mapping(source = "email", target = "email")
@Mapping(source = "loginid", target = "oaLoginId")
@Mapping(source = "modified", target = "oaModified") @Mapping(source = "modified", target = "oaModified")
User baseConvert(OAPersonResponse person); User baseConvert(OAPersonResponse person);
@@ -26,6 +26,7 @@
<result column="dept_id" property="deptId"/> <result column="dept_id" property="deptId"/>
<result column="post_id" property="postId"/> <result column="post_id" property="postId"/>
<result column="remark" property="remark"/> <result column="remark" property="remark"/>
<result column="oa_login_id" property="oaLoginId"/>
<result column="oa_person_id" property="oaPersonId"/> <result column="oa_person_id" property="oaPersonId"/>
</resultMap> </resultMap>
@@ -366,6 +366,7 @@ public class UserServiceImpl extends BaseServiceImpl<UserMapper, User> implement
user.setPostId(null); user.setPostId(null);
user.setAccount(null); user.setAccount(null);
user.setIsLeader(null); user.setIsLeader(null);
user.setOaLoginId(null);
CacheUtil.clear(USER_CACHE); CacheUtil.clear(USER_CACHE);
return submitUserInfo(user); return submitUserInfo(user);
} }
@@ -1729,7 +1729,8 @@ public class FormalSettlementServiceImpl extends BaseServiceImpl<FormalSettlemen
Map<Long, BigDecimal> receiptClaimPaidBySettlement = hasReceivable Map<Long, BigDecimal> receiptClaimPaidBySettlement = hasReceivable
? receiptClaimSettlementMapper.selectList(Wrappers.<ReceiptClaimSettlement>lambdaQuery() ? receiptClaimSettlementMapper.selectList(Wrappers.<ReceiptClaimSettlement>lambdaQuery()
.in(ReceiptClaimSettlement::getFormalSettlementId, settledIds) .in(ReceiptClaimSettlement::getFormalSettlementId, settledIds)
.eq(ReceiptClaimSettlement::getStatus, 1)).stream() .eq(ReceiptClaimSettlement::getStatus, 1)
.eq(ReceiptClaimSettlement::getIsDeleted, 0)).stream()
.collect(Collectors.groupingBy(ReceiptClaimSettlement::getFormalSettlementId, .collect(Collectors.groupingBy(ReceiptClaimSettlement::getFormalSettlementId,
Collectors.reducing(BigDecimal.ZERO, Collectors.reducing(BigDecimal.ZERO,
item -> money(item.getAllocatedReceiptAmount()), BigDecimal::add))) item -> money(item.getAllocatedReceiptAmount()), BigDecimal::add)))
@@ -2161,7 +2162,8 @@ public class FormalSettlementServiceImpl extends BaseServiceImpl<FormalSettlemen
: receiptClaimSettlementMapper.selectList( : receiptClaimSettlementMapper.selectList(
Wrappers.<ReceiptClaimSettlement>lambdaQuery() Wrappers.<ReceiptClaimSettlement>lambdaQuery()
.eq(ReceiptClaimSettlement::getFormalSettlementId, settlement.getId()) .eq(ReceiptClaimSettlement::getFormalSettlementId, settlement.getId())
.eq(ReceiptClaimSettlement::getStatus, 1)) .eq(ReceiptClaimSettlement::getStatus, 1)
.eq(ReceiptClaimSettlement::getIsDeleted, 0))
.stream().map(ReceiptClaimSettlement::getAllocatedReceiptAmount).map(this::money) .stream().map(ReceiptClaimSettlement::getAllocatedReceiptAmount).map(this::money)
.reduce(BigDecimal.ZERO, BigDecimal::add); .reduce(BigDecimal.ZERO, BigDecimal::add);
} }
@@ -321,11 +321,15 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl<PaymentApplic
BigDecimal writeOffAmount = preSettlementReference BigDecimal writeOffAmount = preSettlementReference
? projectAdvanceWriteOffService.sumBySettlement(TransportBusiConstant.SETTLEMENT_BILL_PRE, referenceId) ? projectAdvanceWriteOffService.sumBySettlement(TransportBusiConstant.SETTLEMENT_BILL_PRE, referenceId)
: projectAdvanceWriteOffService.sumBySettlement(TransportBusiConstant.SETTLEMENT_BILL_FORMAL, referenceId); : projectAdvanceWriteOffService.sumBySettlement(TransportBusiConstant.SETTLEMENT_BILL_FORMAL, referenceId);
// 正式结算尾款还需扣减来源预结算进度预付(与正式结算剩余应付口径一致)
BigDecimal advanceOccupied = preSettlementReference
? BigDecimal.ZERO
: formalSettlementAdvanceOccupied(referenceId);
BigDecimal occupiedTotal = cumulativeAppliedAmount.add(writeOffAmount).add(advanceOccupied);
PaymentApplicationReferenceAmountVO amount = new PaymentApplicationReferenceAmountVO(); PaymentApplicationReferenceAmountVO amount = new PaymentApplicationReferenceAmountVO();
amount.setSettlementAmount(settlementAmount); amount.setSettlementAmount(settlementAmount);
amount.setCumulativeAppliedAmount(cumulativeAppliedAmount.add(writeOffAmount)); amount.setCumulativeAppliedAmount(occupiedTotal);
amount.setPayableAmount(settlementAmount.subtract(cumulativeAppliedAmount).subtract(writeOffAmount) amount.setPayableAmount(settlementAmount.subtract(occupiedTotal).max(BigDecimal.ZERO));
.max(BigDecimal.ZERO));
amount.setPaymentSequence(resolvePaymentSequence(preSettlementReference, referenceId, excludeId)); amount.setPaymentSequence(resolvePaymentSequence(preSettlementReference, referenceId, excludeId));
return amount; return amount;
} }
@@ -1891,7 +1895,7 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl<PaymentApplic
} }
/** /**
* 进度预付只认预结算单。尾款付款若同时带回一个并不存在的正式结算单 ID,改为按预结算单保存。 * 进度预付只认预结算单;尾款付款若 settlementId 实际是预结算单 ID 或正式结算单已不存在,改为按预结算单保存。
*/ */
private void normalizeSettlementReference(PaymentApplicationSaveRequest request) { private void normalizeSettlementReference(PaymentApplicationSaveRequest request) {
if ("progress_advance".equals(request.getPaymentType())) { if ("progress_advance".equals(request.getPaymentType())) {
@@ -1901,33 +1905,62 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl<PaymentApplic
if (request.getPreSettlementId() == null) { if (request.getPreSettlementId() == null) {
request.setPreSettlementId(request.getSettlementId()); request.setPreSettlementId(request.getSettlementId());
} }
if (request.getPreSettlementId() == null && request.getSettlementIds() != null) {
request.setPreSettlementId(request.getSettlementIds().stream().filter(Objects::nonNull).findFirst().orElse(null));
}
request.setSettlementId(null); request.setSettlementId(null);
request.setSettlementIds(null); request.setSettlementIds(null);
return; return;
} }
if (!TransportBusiConstant.PAYMENT_TYPE_SETTLEMENT.equals(request.getPaymentType()) || request.getSettlementId() == null) { if (!TransportBusiConstant.PAYMENT_TYPE_SETTLEMENT.equals(request.getPaymentType())) {
return; return;
} }
boolean hasPreSettlement = request.getPreSettlementId() != null // settlementIds 中若含无效正式结算单 ID,先尝试纠正为预结算
|| (request.getPreSettlementIds() != null && request.getPreSettlementIds().stream().anyMatch(Objects::nonNull)); if (request.getSettlementId() == null && request.getSettlementIds() != null
if (!hasPreSettlement) { && !request.getSettlementIds().isEmpty()) {
request.setSettlementId(request.getSettlementIds().stream().filter(Objects::nonNull).findFirst().orElse(null));
}
if (request.getSettlementId() == null) {
return; return;
} }
FormalSettlement formalSettlement = formalSettlementMapper.selectById(request.getSettlementId()); FormalSettlement formalSettlement = formalSettlementMapper.selectById(request.getSettlementId());
if (formalSettlement == null || Objects.equals(formalSettlement.getIsDeleted(), 1)) { if (formalSettlement != null && !Objects.equals(formalSettlement.getIsDeleted(), 1)) {
return;
}
// 正式结算单不存在:若该 ID 实为预结算单,或请求已带预结算引用,则清空正式结算引用
PreSettlement preSettlement = preSettlementMapper.selectById(request.getSettlementId());
boolean isPreSettlement = preSettlement != null && !Objects.equals(preSettlement.getIsDeleted(), 1);
boolean hasPreSettlement = request.getPreSettlementId() != null
|| (request.getPreSettlementIds() != null && request.getPreSettlementIds().stream().anyMatch(Objects::nonNull));
if (isPreSettlement && request.getPreSettlementId() == null) {
request.setPreSettlementId(request.getSettlementId());
if (request.getPreSettlementIds() == null || request.getPreSettlementIds().isEmpty()) {
request.setPreSettlementIds(List.of(request.getSettlementId()));
}
}
if (isPreSettlement || hasPreSettlement) {
request.setSettlementId(null); request.setSettlementId(null);
request.setSettlementIds(null); request.setSettlementIds(null);
} }
} }
/** 预结算付款不挂正式结算单时,清掉历史误写入的正式结算单 ID,避免再次提交按正式结算单刷新。 */ /**
* 预结算付款不挂正式结算单时,清掉历史误写入的正式结算单 ID,避免再次提交按正式结算单刷新。
*/
private void clearDetachedFormalReference(PaymentApplication entity) { private void clearDetachedFormalReference(PaymentApplication entity) {
if (entity.getId() == null || entity.getPreSettlementId() == null || entity.getSettlementId() != null) { if (entity.getId() == null) {
return;
}
boolean progressAdvance = "progress_advance".equals(entity.getPaymentType());
boolean preOnly = entity.getPreSettlementId() != null && entity.getSettlementId() == null;
if (!progressAdvance && !preOnly) {
return; return;
} }
baseMapper.update(null, Wrappers.<PaymentApplication>lambdaUpdate() baseMapper.update(null, Wrappers.<PaymentApplication>lambdaUpdate()
.eq(PaymentApplication::getId, entity.getId()) .eq(PaymentApplication::getId, entity.getId())
.setSql("settlement_id = null, settlement_no = null")); .setSql("settlement_id = null, settlement_no = null"));
entity.setSettlementId(null);
entity.setSettlementNo(null);
} }
private void fillReference(PaymentApplication entity, PaymentApplicationSaveRequest request) { private void fillReference(PaymentApplication entity, PaymentApplicationSaveRequest request) {
@@ -1938,15 +1971,26 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl<PaymentApplic
entity.setProjectId(request.getProjectId()); entity.setProjectName(request.getProjectName()); entity.setDeptId(request.getDeptId()); entity.setDeptName(request.getDeptName()); entity.setContractId(request.getContractId()); entity.setContractNo(request.getContractNo()); entity.setContractName(request.getContractName()); entity.setPayerName(request.getPayerName()); entity.setPayeeName(request.getPayeeName()); entity.setSettlementAmount(request.getSettlementAmount()); entity.setPayableAmount(money(project.getFundDemand()).multiply(BigDecimal.valueOf(10000))); entity.setBillType(Func.isEmpty(project.getSettlementMode()) ? null : "项目预付"); entity.setProjectId(request.getProjectId()); entity.setProjectName(request.getProjectName()); entity.setDeptId(request.getDeptId()); entity.setDeptName(request.getDeptName()); entity.setContractId(request.getContractId()); entity.setContractNo(request.getContractNo()); entity.setContractName(request.getContractName()); entity.setPayerName(request.getPayerName()); entity.setPayeeName(request.getPayeeName()); entity.setSettlementAmount(request.getSettlementAmount()); entity.setPayableAmount(money(project.getFundDemand()).multiply(BigDecimal.valueOf(10000))); entity.setBillType(Func.isEmpty(project.getSettlementMode()) ? null : "项目预付");
} else if (request.getSettlementId() != null) { } else if (request.getSettlementId() != null) {
FormalSettlement source = formalSettlementMapper.selectById(request.getSettlementId()); FormalSettlement source = formalSettlementMapper.selectById(request.getSettlementId());
if (source == null) throw new ServiceException("正式结算单不存在"); if (source == null || Objects.equals(source.getIsDeleted(), 1)) {
// 驳回重提等场景:settlementId 可能误存为预结算单 ID,降级走预结算分支
PreSettlement preSettlement = preSettlementMapper.selectById(request.getSettlementId());
if (preSettlement != null && !Objects.equals(preSettlement.getIsDeleted(), 1)) {
request.setPreSettlementId(preSettlement.getId());
request.setSettlementId(null);
fillPreSettlementReference(entity, preSettlement);
} else if (request.getPreSettlementId() != null) {
request.setSettlementId(null);
fillPreSettlementReference(entity, requireApprovedPreSettlement(request.getPreSettlementId()));
} else {
throw new ServiceException("正式结算单不存在");
}
} else {
if (!TransportBusiConstant.APPROVED.equals(source.getApprovalStatus())) throw new ServiceException("只能选择审批通过、未作废的正式结算单"); if (!TransportBusiConstant.APPROVED.equals(source.getApprovalStatus())) throw new ServiceException("只能选择审批通过、未作废的正式结算单");
entity.setSettlementId(source.getId()); entity.setSettlementNo(source.getFormalSettlementNo()); entity.setPreSettlementId(null); entity.setPreSettlementNo(null); entity.setSettlementId(source.getId()); entity.setSettlementNo(source.getFormalSettlementNo()); entity.setPreSettlementId(null); entity.setPreSettlementNo(null);
entity.setProjectId(source.getProjectId()); entity.setProjectName(source.getProjectName()); entity.setDeptId(source.getDeptId()); entity.setDeptName(source.getDeptName()); entity.setContractId(source.getContractId()); entity.setContractNo(source.getContractNo()); entity.setContractName(source.getContractName()); entity.setPayerName(source.getPayerName()); entity.setPayeeName(source.getPayeeName()); entity.setSettlementAmount(source.getSettlementAmount()); entity.setPayableAmount(payableOfFormalSettlement(source.getId(), source.getSettlementAmount(), entity.getId())); entity.setBillType("正式结算单"); entity.setProjectId(source.getProjectId()); entity.setProjectName(source.getProjectName()); entity.setDeptId(source.getDeptId()); entity.setDeptName(source.getDeptName()); entity.setContractId(source.getContractId()); entity.setContractNo(source.getContractNo()); entity.setContractName(source.getContractName()); entity.setPayerName(source.getPayerName()); entity.setPayeeName(source.getPayeeName()); entity.setSettlementAmount(source.getSettlementAmount()); entity.setPayableAmount(payableOfFormalSettlement(source.getId(), source.getSettlementAmount(), entity.getId())); entity.setBillType("正式结算单");
}
} else if (request.getPreSettlementId() != null) { } else if (request.getPreSettlementId() != null) {
PreSettlement source = preSettlementMapper.selectById(request.getPreSettlementId()); fillPreSettlementReference(entity, requireApprovedPreSettlement(request.getPreSettlementId()));
if (source == null) throw new ServiceException("预结算单不存在");
if (!TransportBusiConstant.APPROVED.equals(source.getApprovalStatus())) throw new ServiceException("只能选择审批通过、未作废的预结算单");
entity.setPreSettlementId(source.getId()); entity.setPreSettlementNo(source.getPreSettlementNo()); entity.setSettlementId(null); entity.setSettlementNo(null); entity.setProjectId(source.getProjectId()); entity.setProjectName(source.getProjectName()); entity.setDeptId(source.getDeptId()); entity.setDeptName(source.getDeptName()); entity.setContractId(source.getContractId()); entity.setContractNo(source.getContractNo()); entity.setContractName(source.getContractName()); entity.setPayerName(source.getPayerName()); entity.setPayeeName(source.getPayeeName()); entity.setSettlementAmount(source.getSettlementAmount()); entity.setPayableAmount(payableOfPreSettlement(source.getId(), source.getSettlementAmount(), entity.getId())); entity.setBillType("预结算单");
} else { } else {
entity.setProjectId(request.getProjectId()); entity.setProjectName(request.getProjectName()); entity.setDeptId(request.getDeptId()); entity.setDeptName(request.getDeptName()); entity.setContractId(request.getContractId()); entity.setContractNo(request.getContractNo()); entity.setContractName(request.getContractName()); entity.setPayerName(request.getPayerName()); entity.setPayeeName(request.getPayeeName()); entity.setSettlementAmount(request.getSettlementAmount()); entity.setPayableAmount(request.getPayableAmount()); entity.setBillType(request.getBillType()); entity.setProjectId(request.getProjectId()); entity.setProjectName(request.getProjectName()); entity.setDeptId(request.getDeptId()); entity.setDeptName(request.getDeptName()); entity.setContractId(request.getContractId()); entity.setContractNo(request.getContractNo()); entity.setContractName(request.getContractName()); entity.setPayerName(request.getPayerName()); entity.setPayeeName(request.getPayeeName()); entity.setSettlementAmount(request.getSettlementAmount()); entity.setPayableAmount(request.getPayableAmount()); entity.setBillType(request.getBillType());
} }
@@ -1955,6 +1999,36 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl<PaymentApplic
&& money(entity.getPayableAmount()).compareTo(BigDecimal.ZERO) > 0) throw new ServiceException("申请付款金额不能超过可付款金额"); && money(entity.getPayableAmount()).compareTo(BigDecimal.ZERO) > 0) throw new ServiceException("申请付款金额不能超过可付款金额");
} }
private PreSettlement requireApprovedPreSettlement(Long preSettlementId) {
PreSettlement source = preSettlementMapper.selectById(preSettlementId);
if (source == null || Objects.equals(source.getIsDeleted(), 1)) {
throw new ServiceException("预结算单不存在");
}
if (!TransportBusiConstant.APPROVED.equals(source.getApprovalStatus())) {
throw new ServiceException("只能选择审批通过、未作废的预结算单");
}
return source;
}
private void fillPreSettlementReference(PaymentApplication entity, PreSettlement source) {
entity.setPreSettlementId(source.getId());
entity.setPreSettlementNo(source.getPreSettlementNo());
entity.setSettlementId(null);
entity.setSettlementNo(null);
entity.setProjectId(source.getProjectId());
entity.setProjectName(source.getProjectName());
entity.setDeptId(source.getDeptId());
entity.setDeptName(source.getDeptName());
entity.setContractId(source.getContractId());
entity.setContractNo(source.getContractNo());
entity.setContractName(source.getContractName());
entity.setPayerName(source.getPayerName());
entity.setPayeeName(source.getPayeeName());
entity.setSettlementAmount(source.getSettlementAmount());
entity.setPayableAmount(payableOfPreSettlement(source.getId(), source.getSettlementAmount(), entity.getId()));
entity.setBillType("预结算单");
}
private List<FormalSettlement> resolveFormalSettlements(PaymentApplicationSaveRequest request, PaymentApplication entity) { private List<FormalSettlement> resolveFormalSettlements(PaymentApplicationSaveRequest request, PaymentApplication entity) {
if (!TransportBusiConstant.PAYMENT_TYPE_SETTLEMENT.equals(request.getPaymentType())) return List.of(); if (!TransportBusiConstant.PAYMENT_TYPE_SETTLEMENT.equals(request.getPaymentType())) return List.of();
List<Long> ids = request.getSettlementIds() == null ? List.of() : request.getSettlementIds(); List<Long> ids = request.getSettlementIds() == null ? List.of() : request.getSettlementIds();
@@ -2358,15 +2432,45 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl<PaymentApplic
return relationAmount.add(legacyAmount); return relationAmount.add(legacyAmount);
} }
/** 正式结算可付款 = 结算金额 − 已申请付款 − 项目预付核销 */ /**
* 正式结算可付款 = 结算金额 − 已申请付款 − 项目预付核销 − 来源预结算进度预付占用。
* 进度预付占用与正式结算台账 remainingPayable 口径对齐,避免转正式后仍可按全额申请尾款。
*/
private BigDecimal payableOfFormalSettlement(Long settlementId, BigDecimal settlementAmount, Long excludeId) { private BigDecimal payableOfFormalSettlement(Long settlementId, BigDecimal settlementAmount, Long excludeId) {
return money(settlementAmount) return money(settlementAmount)
.subtract(cumulativeFormalSettlementApplied(settlementId, excludeId)) .subtract(cumulativeFormalSettlementApplied(settlementId, excludeId))
.subtract(projectAdvanceWriteOffService.sumBySettlement( .subtract(projectAdvanceWriteOffService.sumBySettlement(
TransportBusiConstant.SETTLEMENT_BILL_FORMAL, settlementId)) TransportBusiConstant.SETTLEMENT_BILL_FORMAL, settlementId))
.subtract(formalSettlementAdvanceOccupied(settlementId))
.max(BigDecimal.ZERO); .max(BigDecimal.ZERO);
} }
/**
* 正式结算来源预结算的进度预付占用合计:优先取来源行快照,快照为空时回退预结算实时已申请/已付。
*/
private BigDecimal formalSettlementAdvanceOccupied(Long settlementId) {
if (settlementId == null) {
return BigDecimal.ZERO.setScale(2, RoundingMode.HALF_UP);
}
List<FormalSettlementSource> sources = formalSettlementSourceMapper.selectList(
Wrappers.<FormalSettlementSource>lambdaQuery()
.eq(FormalSettlementSource::getFormalSettlementId, settlementId)
.eq(FormalSettlementSource::getIsDeleted, 0));
if (sources.isEmpty()) {
return BigDecimal.ZERO.setScale(2, RoundingMode.HALF_UP);
}
BigDecimal total = BigDecimal.ZERO;
for (FormalSettlementSource source : sources) {
BigDecimal occupied = money(source.getAdvanceAppliedAmount()).max(money(source.getAdvancePaidAmount()));
if (occupied.compareTo(BigDecimal.ZERO) <= 0 && source.getPreSettlementId() != null) {
occupied = findPreSettlementApplied(source.getPreSettlementId())
.max(findPreSettlementPaid(source.getPreSettlementId()));
}
total = total.add(occupied);
}
return total;
}
/** 预结算可付款 = 结算金额 − 已申请付款 − 项目预付核销 */ /** 预结算可付款 = 结算金额 − 已申请付款 − 项目预付核销 */
private BigDecimal payableOfPreSettlement(Long preSettlementId, BigDecimal settlementAmount, Long excludeId) { private BigDecimal payableOfPreSettlement(Long preSettlementId, BigDecimal settlementAmount, Long excludeId) {
return money(settlementAmount) return money(settlementAmount)
@@ -252,8 +252,8 @@ public class ReceiptClaimRecordServiceImpl extends BaseServiceImpl<ReceiptClaimM
BigDecimal flowClaimedAfter = money(flow.getClaimedAmount()) BigDecimal flowClaimedAfter = money(flow.getClaimedAmount())
.subtract(money(claim.getClaimAmount())).max(BigDecimal.ZERO); .subtract(money(claim.getClaimAmount())).max(BigDecimal.ZERO);
flow.setClaimedAmount(flowClaimedAfter); flow.setClaimedAmount(flowClaimedAfter);
// 认领状态对照公告未认领金额重算(作废分摊不计入累计) // 认领状态对照可认领上限重算(作废分摊不计入累计)
flow.setClaimStatus(amountClaimStatus(flowClaimedAfter, flow.getUnclaimedAmount())); flow.setClaimStatus(amountClaimStatus(flowClaimedAfter, claimCeiling(flow)));
receiptFlowMapper.updateById(flow); receiptFlowMapper.updateById(flow);
// 作废不生成金蝶认领冲单:金蝶单号/状态保留原值(未推送为空,已推送为真实收款单信息), // 作废不生成金蝶认领冲单:金蝶单号/状态保留原值(未推送为空,已推送为真实收款单信息),
@@ -411,13 +411,28 @@ public class ReceiptClaimRecordServiceImpl extends BaseServiceImpl<ReceiptClaimM
} }
/** /**
* 累计认领金额 → 流水认领状态:对照公告未认领金额判定(与收款流水侧口径一致) * 累计认领金额 → 流水认领状态:对照可认领上限判定(与收款流水侧口径一致)
*/ */
private String amountClaimStatus(BigDecimal claimedAmount, BigDecimal unclaimedAmount) { private String amountClaimStatus(BigDecimal claimedAmount, BigDecimal claimCeiling) {
if (claimedAmount.compareTo(BigDecimal.ZERO) <= 0) { if (claimedAmount.compareTo(BigDecimal.ZERO) <= 0) {
return "unclaimed"; return "unclaimed";
} }
return claimedAmount.compareTo(money(unclaimedAmount)) >= 0 ? "claimed" : "partial"; return claimedAmount.compareTo(money(claimCeiling)) >= 0 ? "claimed" : "partial";
}
/**
* 可认领上限:公告未认领金额与收款金额取较小正值(与收款流水侧口径一致)
*/
private BigDecimal claimCeiling(KingdeeReceiptFlow flow) {
BigDecimal unclaimed = money(flow.getUnclaimedAmount());
BigDecimal receipt = money(flow.getReceiptAmount());
if (receipt.compareTo(BigDecimal.ZERO) <= 0) {
return unclaimed;
}
if (unclaimed.compareTo(BigDecimal.ZERO) <= 0) {
return unclaimed;
}
return unclaimed.min(receipt);
} }
private String amountStatus(BigDecimal paidAmount, BigDecimal settlementAmount) { private String amountStatus(BigDecimal paidAmount, BigDecimal settlementAmount) {
@@ -194,12 +194,16 @@ public class ReceiptFlowServiceImpl extends BaseServiceImpl<KingdeeReceiptFlowMa
.eq(ReceiptFlowRecord::getReceiptFlowId, id) .eq(ReceiptFlowRecord::getReceiptFlowId, id)
.in(ReceiptFlowRecord::getActionType, List.of("claim", "push")) .in(ReceiptFlowRecord::getActionType, List.of("claim", "push"))
.orderByDesc(ReceiptFlowRecord::getCreateTime))); .orderByDesc(ReceiptFlowRecord::getCreateTime)));
// 继续认领:带回本流水历史关联且仍有剩余可认领金额的结算单
vo.setRelatedSettlements(relatedSettlementCandidates(id));
return vo; return vo;
} }
@Override @Override
public List<Map<String, Object>> settlementCandidates(String keyword, Long flowId) { public List<Map<String, Object>> settlementCandidates(String keyword, Long flowId) {
existing(flowId); existing(flowId);
// 本流水历史关联结算单优先保留(避免仅按创建时间 limit 截断后选不到)
Set<Long> relatedIds = relatedSettlementIds(flowId);
List<FormalSettlement> settlements = formalSettlementMapper.selectList( List<FormalSettlement> settlements = formalSettlementMapper.selectList(
Wrappers.<FormalSettlement>lambdaQuery() Wrappers.<FormalSettlement>lambdaQuery()
.eq(FormalSettlement::getSettlementType, RECEIVABLE) .eq(FormalSettlement::getSettlementType, RECEIVABLE)
@@ -210,7 +214,24 @@ public class ReceiptFlowServiceImpl extends BaseServiceImpl<KingdeeReceiptFlowMa
.or().like(FormalSettlement::getProjectName, keyword) .or().like(FormalSettlement::getProjectName, keyword)
.or().like(FormalSettlement::getContractName, keyword)) .or().like(FormalSettlement::getContractName, keyword))
.orderByDesc(FormalSettlement::getCreateTime) .orderByDesc(FormalSettlement::getCreateTime)
.last("limit 200")); .last("limit 500"));
if (!relatedIds.isEmpty()) {
Set<Long> loadedIds = settlements.stream().map(FormalSettlement::getId)
.filter(Objects::nonNull).collect(Collectors.toSet());
List<Long> missingIds = relatedIds.stream().filter(id -> !loadedIds.contains(id)).toList();
if (!missingIds.isEmpty()) {
List<FormalSettlement> relatedSettlements = formalSettlementMapper.selectList(
Wrappers.<FormalSettlement>lambdaQuery()
.in(FormalSettlement::getId, missingIds)
.eq(FormalSettlement::getSettlementType, RECEIVABLE)
.eq(FormalSettlement::getApprovalStatus, APPROVED)
.eq(FormalSettlement::getStatus, 1));
List<FormalSettlement> merged = new ArrayList<>(relatedSettlements.size() + settlements.size());
merged.addAll(relatedSettlements);
merged.addAll(settlements);
settlements = merged;
}
}
// 候选结算单的累计认领金额一次聚合查询(避免逐单 N+1) // 候选结算单的累计认领金额一次聚合查询(避免逐单 N+1)
Map<Long, BigDecimal> claimedMap = receiptClaimedAmounts(settlements.stream() Map<Long, BigDecimal> claimedMap = receiptClaimedAmounts(settlements.stream()
.map(FormalSettlement::getId).toList()); .map(FormalSettlement::getId).toList());
@@ -218,6 +239,14 @@ public class ReceiptFlowServiceImpl extends BaseServiceImpl<KingdeeReceiptFlowMa
.map(settlement -> candidateRow(settlement, .map(settlement -> candidateRow(settlement,
claimedMap.getOrDefault(settlement.getId(), BigDecimal.ZERO))) claimedMap.getOrDefault(settlement.getId(), BigDecimal.ZERO)))
.filter(row -> ((BigDecimal) row.get("remainingReceiptAmount")).compareTo(BigDecimal.ZERO) > 0) .filter(row -> ((BigDecimal) row.get("remainingReceiptAmount")).compareTo(BigDecimal.ZERO) > 0)
.sorted((left, right) -> {
boolean leftRelated = relatedIds.contains(toLong(left.get("id")));
boolean rightRelated = relatedIds.contains(toLong(right.get("id")));
if (leftRelated == rightRelated) {
return 0;
}
return leftRelated ? -1 : 1;
})
.toList(); .toList();
} }
@@ -234,6 +263,7 @@ public class ReceiptFlowServiceImpl extends BaseServiceImpl<KingdeeReceiptFlowMa
Wrappers.<ReceiptClaimSettlement>lambdaQuery() Wrappers.<ReceiptClaimSettlement>lambdaQuery()
.eq(ReceiptClaimSettlement::getFormalSettlementId, formalSettlementId) .eq(ReceiptClaimSettlement::getFormalSettlementId, formalSettlementId)
.eq(ReceiptClaimSettlement::getStatus, 1) .eq(ReceiptClaimSettlement::getStatus, 1)
.eq(ReceiptClaimSettlement::getIsDeleted, 0)
.orderByDesc(ReceiptClaimSettlement::getCreateTime)); .orderByDesc(ReceiptClaimSettlement::getCreateTime));
if (relations.isEmpty()) { if (relations.isEmpty()) {
return List.of(); return List.of();
@@ -315,19 +345,19 @@ public class ReceiptFlowServiceImpl extends BaseServiceImpl<KingdeeReceiptFlowMa
allocatedTotal = allocatedTotal.add(allocated); allocatedTotal = allocatedTotal.add(allocated);
} }
// 公告未认领金额(认领上限与认领完成判定基准,A301-2 口径) // 认领上限:公告未认领金额与收款金额取较小值(避免未认领金额异常偏大导致无法认领完成)
if (flow.getUnclaimedAmount() == null) { if (flow.getUnclaimedAmount() == null) {
throw new ServiceException("公告未认领金额缺失,请先手动同步金蝶收款流水"); throw new ServiceException("公告未认领金额缺失,请先手动同步金蝶收款流水");
} }
BigDecimal unclaimedAmount = money(flow.getUnclaimedAmount()); BigDecimal claimCeiling = claimCeiling(flow);
if (unclaimedAmount.compareTo(BigDecimal.ZERO) <= 0) { if (claimCeiling.compareTo(BigDecimal.ZERO) <= 0) {
throw new ServiceException("该流水公告未认领金额为0,无需认领"); throw new ServiceException("该流水可认领金额为0,无需认领");
} }
// 流水认领前已认领金额 // 流水认领前已认领金额
BigDecimal previousFlowClaimed = flowClaimedAmount(flow.getId()); BigDecimal previousFlowClaimed = flowClaimedAmount(flow.getId());
if (previousFlowClaimed.add(allocatedTotal).compareTo(unclaimedAmount) > 0) { if (previousFlowClaimed.add(allocatedTotal).compareTo(claimCeiling) > 0) {
throw new ServiceException("累计认领金额不能超过公告未认领金额,剩余可认领 " throw new ServiceException("累计认领金额不能超过可认领上限,剩余可认领 "
+ unclaimedAmount.subtract(previousFlowClaimed).max(BigDecimal.ZERO)); + claimCeiling.subtract(previousFlowClaimed).max(BigDecimal.ZERO));
} }
// 当前用户部门 // 当前用户部门
@@ -380,6 +410,9 @@ public class ReceiptFlowServiceImpl extends BaseServiceImpl<KingdeeReceiptFlowMa
relation.setClaimedReceiptAmount(previousClaimed); relation.setClaimedReceiptAmount(previousClaimed);
// 本次分摊金额 // 本次分摊金额
relation.setAllocatedReceiptAmount(allocated); relation.setAllocatedReceiptAmount(allocated);
// 有效分摊(与汇总/候选查询 status=1 口径一致)
relation.setStatus(1);
relation.setIsDeleted(0);
claimSettlementMapper.insert(relation); claimSettlementMapper.insert(relation);
} }
@@ -390,8 +423,8 @@ public class ReceiptFlowServiceImpl extends BaseServiceImpl<KingdeeReceiptFlowMa
String fromStatus = normalizeClaimStatus(flow.getClaimStatus()); String fromStatus = normalizeClaimStatus(flow.getClaimStatus());
// 认领后累计金额 // 认领后累计金额
BigDecimal claimedAfter = previousFlowClaimed.add(allocatedTotal); BigDecimal claimedAfter = previousFlowClaimed.add(allocatedTotal);
// 认领后状态(对照公告未认领金额) // 认领后状态(对照可认领上限)
String toStatus = amountClaimStatus(claimedAfter, unclaimedAmount); String toStatus = amountClaimStatus(claimedAfter, claimCeiling);
// 更新流水已认领金额 // 更新流水已认领金额
flow.setClaimedAmount(claimedAfter); flow.setClaimedAmount(claimedAfter);
// 更新流水认领状态 // 更新流水认领状态
@@ -499,9 +532,9 @@ public class ReceiptFlowServiceImpl extends BaseServiceImpl<KingdeeReceiptFlowMa
// 复制公告数据到实体(不覆盖本地认领字段) // 复制公告数据到实体(不覆盖本地认领字段)
copyAnnounceRow(entity, announce, detailSerialNo, receiptAmount); copyAnnounceRow(entity, announce, detailSerialNo, receiptAmount);
// 根据本地累计认领金额与最新公告未认领金额重新计算认领状态 // 根据本地累计认领金额与可认领上限重新计算认领状态
entity.setClaimStatus(amountClaimStatus(money(entity.getClaimedAmount()), entity.setClaimStatus(amountClaimStatus(money(entity.getClaimedAmount()),
entity.getUnclaimedAmount())); claimCeiling(entity)));
if (created) { if (created) {
toInsert.add(entity); toInsert.add(entity);
@@ -621,6 +654,46 @@ public class ReceiptFlowServiceImpl extends BaseServiceImpl<KingdeeReceiptFlowMa
} }
} }
/**
* 本流水历史关联过的结算单 id(有效分摊)
*/
private Set<Long> relatedSettlementIds(Long flowId) {
if (flowId == null) {
return Set.of();
}
return claimSettlementMapper.selectList(Wrappers.<ReceiptClaimSettlement>lambdaQuery()
.select(ReceiptClaimSettlement::getFormalSettlementId)
.eq(ReceiptClaimSettlement::getReceiptFlowId, flowId)
.eq(ReceiptClaimSettlement::getStatus, 1)
.eq(ReceiptClaimSettlement::getIsDeleted, 0)).stream()
.map(ReceiptClaimSettlement::getFormalSettlementId)
.filter(Objects::nonNull)
.collect(Collectors.toCollection(HashSet::new));
}
/**
* 继续认领预填:本流水历史关联且仍有剩余可认领金额的结算单
*/
private List<Map<String, Object>> relatedSettlementCandidates(Long flowId) {
Set<Long> relatedIds = relatedSettlementIds(flowId);
if (relatedIds.isEmpty()) {
return List.of();
}
List<FormalSettlement> settlements = formalSettlementMapper.selectList(
Wrappers.<FormalSettlement>lambdaQuery()
.in(FormalSettlement::getId, relatedIds)
.eq(FormalSettlement::getSettlementType, RECEIVABLE)
.eq(FormalSettlement::getApprovalStatus, APPROVED)
.eq(FormalSettlement::getStatus, 1));
Map<Long, BigDecimal> claimedMap = receiptClaimedAmounts(settlements.stream()
.map(FormalSettlement::getId).toList());
return settlements.stream()
.map(settlement -> candidateRow(settlement,
claimedMap.getOrDefault(settlement.getId(), BigDecimal.ZERO)))
.filter(row -> ((BigDecimal) row.get("remainingReceiptAmount")).compareTo(BigDecimal.ZERO) > 0)
.toList();
}
/** /**
* 批量聚合结算单的累计有效认领金额:一次 IN 查询按结算单分组求和,避免逐单 N+1 * 批量聚合结算单的累计有效认领金额:一次 IN 查询按结算单分组求和,避免逐单 N+1
*/ */
@@ -631,7 +704,8 @@ public class ReceiptFlowServiceImpl extends BaseServiceImpl<KingdeeReceiptFlowMa
} }
return claimSettlementMapper.selectList(Wrappers.<ReceiptClaimSettlement>lambdaQuery() return claimSettlementMapper.selectList(Wrappers.<ReceiptClaimSettlement>lambdaQuery()
.in(ReceiptClaimSettlement::getFormalSettlementId, ids) .in(ReceiptClaimSettlement::getFormalSettlementId, ids)
.eq(ReceiptClaimSettlement::getStatus, 1)).stream() .eq(ReceiptClaimSettlement::getStatus, 1)
.eq(ReceiptClaimSettlement::getIsDeleted, 0)).stream()
.collect(Collectors.groupingBy(ReceiptClaimSettlement::getFormalSettlementId, .collect(Collectors.groupingBy(ReceiptClaimSettlement::getFormalSettlementId,
Collectors.reducing(BigDecimal.ZERO, Collectors.reducing(BigDecimal.ZERO,
item -> money(item.getAllocatedReceiptAmount()), BigDecimal::add))); item -> money(item.getAllocatedReceiptAmount()), BigDecimal::add)));
@@ -640,12 +714,42 @@ public class ReceiptFlowServiceImpl extends BaseServiceImpl<KingdeeReceiptFlowMa
private BigDecimal flowClaimedAmount(Long flowId) { private BigDecimal flowClaimedAmount(Long flowId) {
return claimSettlementMapper.selectList(Wrappers.<ReceiptClaimSettlement>lambdaQuery() return claimSettlementMapper.selectList(Wrappers.<ReceiptClaimSettlement>lambdaQuery()
.eq(ReceiptClaimSettlement::getReceiptFlowId, flowId) .eq(ReceiptClaimSettlement::getReceiptFlowId, flowId)
.eq(ReceiptClaimSettlement::getStatus, 1)).stream() .eq(ReceiptClaimSettlement::getStatus, 1)
.eq(ReceiptClaimSettlement::getIsDeleted, 0)).stream()
.map(ReceiptClaimSettlement::getAllocatedReceiptAmount) .map(ReceiptClaimSettlement::getAllocatedReceiptAmount)
.map(this::money) .map(this::money)
.reduce(BigDecimal.ZERO, BigDecimal::add); .reduce(BigDecimal.ZERO, BigDecimal::add);
} }
/**
* 可认领上限:公告未认领金额与收款金额取较小正值。
* 主口径仍为 A301-2 公告未认领金额;与收款金额取 min,避免未认领金额异常偏大导致无法进入「认领完成」。
*/
private BigDecimal claimCeiling(KingdeeReceiptFlow flow) {
BigDecimal unclaimed = money(flow.getUnclaimedAmount());
BigDecimal receipt = money(flow.getReceiptAmount());
if (receipt.compareTo(BigDecimal.ZERO) <= 0) {
return unclaimed;
}
if (unclaimed.compareTo(BigDecimal.ZERO) <= 0) {
return unclaimed;
}
return unclaimed.min(receipt);
}
private Long toLong(Object value) {
if (value == null) {
return null;
}
if (value instanceof Long longValue) {
return longValue;
}
if (value instanceof Number number) {
return number.longValue();
}
return Func.toLong(String.valueOf(value));
}
private KingdeeReceiptFlow lockedFlow(Long flowId) { private KingdeeReceiptFlow lockedFlow(Long flowId) {
KingdeeReceiptFlow flow = baseMapper.selectOne(Wrappers.<KingdeeReceiptFlow>lambdaQuery() KingdeeReceiptFlow flow = baseMapper.selectOne(Wrappers.<KingdeeReceiptFlow>lambdaQuery()
.eq(KingdeeReceiptFlow::getId, flowId) .eq(KingdeeReceiptFlow::getId, flowId)
@@ -824,18 +928,20 @@ public class ReceiptFlowServiceImpl extends BaseServiceImpl<KingdeeReceiptFlowMa
flow.setClaimMe(announce.getClaimme()); flow.setClaimMe(announce.getClaimme());
BigDecimal claimedTotal = flowClaimedAmount(flow.getId()); BigDecimal claimedTotal = flowClaimedAmount(flow.getId());
flow.setClaimedAmount(claimedTotal); flow.setClaimedAmount(claimedTotal);
String claimStatus = amountClaimStatus(claimedTotal, flow.getUnclaimedAmount()); BigDecimal claimCeiling = claimCeiling(flow);
String claimStatus = amountClaimStatus(claimedTotal, claimCeiling);
flow.setClaimStatus(claimStatus); flow.setClaimStatus(claimStatus);
updateById(flow); updateById(flow);
if (!CLAIMED.equals(claimStatus)) { if (!CLAIMED.equals(claimStatus)) {
return ReceiptClaimPushResult.fail("累计认领金额(" + claimedTotal + ")与公告未认领金额(" return ReceiptClaimPushResult.fail("累计认领金额(" + claimedTotal + ")与可认领上限("
+ money(flow.getUnclaimedAmount()) + ")不一致,不满足推送条件"); + claimCeiling + ")不一致,不满足推送条件");
} }
// 分录取该流水全部有效分摊行(结算单id升序,跨认领主单归集) // 分录取该流水全部有效分摊行(结算单id升序,跨认领主单归集)
List<ReceiptClaimSettlement> allocations = claimSettlementMapper.selectList( List<ReceiptClaimSettlement> allocations = claimSettlementMapper.selectList(
Wrappers.<ReceiptClaimSettlement>lambdaQuery() Wrappers.<ReceiptClaimSettlement>lambdaQuery()
.eq(ReceiptClaimSettlement::getReceiptFlowId, flow.getId()) .eq(ReceiptClaimSettlement::getReceiptFlowId, flow.getId())
.eq(ReceiptClaimSettlement::getStatus, 1) .eq(ReceiptClaimSettlement::getStatus, 1)
.eq(ReceiptClaimSettlement::getIsDeleted, 0)
.orderByAsc(ReceiptClaimSettlement::getFormalSettlementId)); .orderByAsc(ReceiptClaimSettlement::getFormalSettlementId));
if (allocations.isEmpty()) { if (allocations.isEmpty()) {
return ReceiptClaimPushResult.fail("该流水无有效认领分摊,无法推送"); return ReceiptClaimPushResult.fail("该流水无有效认领分摊,无法推送");
@@ -1075,13 +1181,13 @@ public class ReceiptFlowServiceImpl extends BaseServiceImpl<KingdeeReceiptFlowMa
} }
/** /**
* 累计认领金额 → 流水认领状态:对照公告未认领金额判定(claimed 即「认领完成」,唯一可推送状态) * 累计认领金额 → 流水认领状态:对照可认领上限判定(claimed 即「认领完成」,唯一可推送状态)
*/ */
private String amountClaimStatus(BigDecimal claimedAmount, BigDecimal unclaimedAmount) { private String amountClaimStatus(BigDecimal claimedAmount, BigDecimal claimCeiling) {
if (claimedAmount.compareTo(BigDecimal.ZERO) <= 0) { if (claimedAmount.compareTo(BigDecimal.ZERO) <= 0) {
return UNCLAIMED; return UNCLAIMED;
} }
return claimedAmount.compareTo(money(unclaimedAmount)) >= 0 ? CLAIMED : PARTIAL; return claimedAmount.compareTo(money(claimCeiling)) >= 0 ? CLAIMED : PARTIAL;
} }
private String amountStatus(BigDecimal claimedAmount, BigDecimal settlementAmount) { private String amountStatus(BigDecimal claimedAmount, BigDecimal settlementAmount) {
@@ -246,10 +246,8 @@ public class TemporaryCreditLimitServiceImpl extends BaseServiceImpl<TemporaryCr
TransportBusinessSupport.validateRequired(temporaryCreditLimit.getProjectCode(), "项目编号不能为空"); TransportBusinessSupport.validateRequired(temporaryCreditLimit.getProjectCode(), "项目编号不能为空");
TransportBusinessSupport.validateRequired(temporaryCreditLimit.getUndertakeDeptName(), "承办部门不能为空"); TransportBusinessSupport.validateRequired(temporaryCreditLimit.getUndertakeDeptName(), "承办部门不能为空");
TransportBusinessSupport.validateRequired(temporaryCreditLimit.getValidUntil() == null ? null : temporaryCreditLimit.getValidUntil().toString(), "请选择申请有效期至"); TransportBusinessSupport.validateRequired(temporaryCreditLimit.getValidUntil() == null ? null : temporaryCreditLimit.getValidUntil().toString(), "请选择申请有效期至");
if (temporaryCreditLimit.getProjectFundLimit() == null || temporaryCreditLimit.getUsedFundLimit() == null || temporaryCreditLimit.getRemainingFundLimit() == null) { if (temporaryCreditLimit.getRemainingFundLimit() != null
throw new ServiceException("项目额度数据异常,请重新选择项目"); && temporaryCreditLimit.getRemainingFundLimit().compareTo(BigDecimal.ZERO) < 0) {
}
if (temporaryCreditLimit.getRemainingFundLimit().compareTo(BigDecimal.ZERO) < 0) {
throw new ServiceException("项目剩余额度不能小于0"); throw new ServiceException("项目剩余额度不能小于0");
} }
} }
@@ -70,10 +70,15 @@ public class ReceiptFlowWrapper extends BaseEntityWrapper<KingdeeReceiptFlow, Re
case "failed" -> "处理失败"; case "failed" -> "处理失败";
default -> "未推送"; default -> "未推送";
}); });
// 剩余可认领金额 = 公告未认领金额 − 累计认领金额(认领上限为公告口径) // 剩余可认领金额 = 可认领上限 − 累计认领金额(上限取公告未认领与收款金额较小值)
BigDecimal unclaimedAmount = money(entity.getUnclaimedAmount()); BigDecimal unclaimedAmount = money(entity.getUnclaimedAmount());
BigDecimal receiptAmount = money(entity.getReceiptAmount());
BigDecimal claimCeiling = unclaimedAmount;
if (receiptAmount.compareTo(BigDecimal.ZERO) > 0 && unclaimedAmount.compareTo(BigDecimal.ZERO) > 0) {
claimCeiling = unclaimedAmount.min(receiptAmount);
}
BigDecimal claimedAmount = money(entity.getClaimedAmount()); BigDecimal claimedAmount = money(entity.getClaimedAmount());
vo.setRemainingAmount(unclaimedAmount.subtract(claimedAmount).max(BigDecimal.ZERO)); vo.setRemainingAmount(claimCeiling.subtract(claimedAmount).max(BigDecimal.ZERO));
vo.setCreateUserName(UserCache.getUserRealName(entity.getCreateUser())); vo.setCreateUserName(UserCache.getUserRealName(entity.getCreateUser()));
vo.setUpdateUserName(UserCache.getUserRealName(entity.getUpdateUser())); vo.setUpdateUserName(UserCache.getUserRealName(entity.getUpdateUser()));
return vo; return vo;
@@ -0,0 +1,18 @@
-- 用户表:新增 OA 登录名,支持用户管理后台维护
-- 排序规则 utf8mb4_general_ci
ALTER TABLE `blade_user`
ADD COLUMN `oa_login_id` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT 'OA登录名' AFTER `is_oa`;
-- 从用户部门关联表回填已有 OA 登录账号
UPDATE `blade_user` bu
INNER JOIN (
SELECT `user_id`, MAX(`oa_login_id`) AS `oa_login_id`
FROM `blade_user_dept`
WHERE `is_deleted` = 0
AND `oa_login_id` IS NOT NULL
AND `oa_login_id` <> ''
GROUP BY `user_id`
) bud ON bud.`user_id` = bu.`id`
SET bu.`oa_login_id` = bud.`oa_login_id`
WHERE bu.`oa_login_id` IS NULL OR bu.`oa_login_id` = '';