调整收付款问题

This commit is contained in:
2026-09-01 17:35:27 +08:00
parent 1363a526ff
commit 3edfb72d7d
21 changed files with 656 additions and 105 deletions
@@ -462,6 +462,11 @@ public class FormalSettlementServiceImpl extends BaseServiceImpl<FormalSettlemen
private String createPayment(FormalSettlement settlement, BigDecimal appliedAmount, String remark) {
if (!APPROVED.equals(settlement.getApprovalStatus())) throw new ServiceException("仅审批通过的正式结算单允许发起付款申请");
if (!"payable".equals(settlement.getSettlementType())) throw new ServiceException("仅应付正式结算单允许发起付款申请");
long activePaymentCount = paymentMapper.selectCount(Wrappers.<FormalSettlementPayment>lambdaQuery()
.eq(FormalSettlementPayment::getFormalSettlementId, settlement.getId())
.eq(FormalSettlementPayment::getIsDeleted, 0)
.notIn(FormalSettlementPayment::getBillStatus, RETURNED, VOIDED));
if (activePaymentCount > 0) throw new ServiceException("该结算单正在申请结算尾款,不允许再次提交申请付款");
BigDecimal amount = positive(appliedAmount, "申请付款金额");
BigDecimal available = money(settlement.getSettlementAmount()).subtract(money(settlement.getAppliedPaymentAmount()));
if (amount.compareTo(available) > 0) throw new ServiceException("申请付款金额不能超过剩余可申请金额" + available);
@@ -701,9 +706,10 @@ public class FormalSettlementServiceImpl extends BaseServiceImpl<FormalSettlemen
if (Integer.valueOf(1).equals(requestRow.getManualFlag())) {
String feeType = requiredText(requestRow.getFeeType(), "费用类型");
String feeItem = requiredText(requestRow.getFeeItem(), "费用项");
if (!allowedManualFees.getOrDefault(feeType, Set.of()).contains(feeItem)) {
throw new ServiceException("费用类型与费用项不匹配或费用项已停用");
}
// 暂时注释费用类型与费用项匹配及停用校验
// if (!allowedManualFees.getOrDefault(feeType, Set.of()).contains(feeItem)) {
// throw new ServiceException("费用类型与费用项不匹配或费用项已停用");
// }
FormalSettlementSummaryFee row = requestRow.getId() == null ? null : existingMap.get(requestRow.getId());
if (row == null && requestRow.getId() == null) row = new FormalSettlementSummaryFee();
if (row == null || row.getId() != null && !Integer.valueOf(1).equals(row.getManualFlag())) {
@@ -30,6 +30,7 @@ import org.springblade.core.tool.jackson.JsonUtil;
import org.springblade.core.tool.utils.BeanUtil;
import org.springblade.core.tool.utils.Func;
import org.springblade.transport.mapper.FormalSettlementMapper;
import org.springblade.transport.mapper.FormalSettlementPaymentMapper;
import org.springblade.transport.mapper.BillLedgerMapper;
import org.springblade.transport.mapper.BillLedgerUsageMapper;
import org.springblade.transport.mapper.PreSettlementMapper;
@@ -46,6 +47,7 @@ import org.springblade.transport.pojo.dto.PaymentApplicationRecordRequest;
import org.springblade.transport.pojo.dto.PaymentApplicationSaveRequest;
import org.springblade.transport.pojo.dto.PaymentApplicationStatusRequest;
import org.springblade.transport.pojo.entity.FormalSettlement;
import org.springblade.transport.pojo.entity.FormalSettlementPayment;
import org.springblade.transport.pojo.entity.BillLedger;
import org.springblade.transport.pojo.entity.BillLedgerUsage;
import org.springblade.transport.pojo.entity.PreSettlement;
@@ -89,6 +91,7 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl<PaymentApplic
private final PaymentApplicationSettlementMapper settlementRelationMapper;
private final PreSettlementMapper preSettlementMapper;
private final FormalSettlementMapper formalSettlementMapper;
private final FormalSettlementPaymentMapper formalSettlementPaymentMapper;
private final ProjectApplyMapper projectApplyMapper;
private final ContractManageMapper contractManageMapper;
private final CustomerArchiveMapper customerArchiveMapper;
@@ -253,6 +256,7 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl<PaymentApplic
if (!List.of(DRAFT, RETURNED).contains(entity.getApprovalStatus())) {
throw new ServiceException("当前状态不允许提交");
}
validateNoTailPayment(entity);
validateProjectAdvanceContractFile(entity);
validateSelectedBill(entity, false);
validateQuota(entity);
@@ -618,6 +622,25 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl<PaymentApplic
formalSettlementService.refreshPaymentSummariesForPreSettlement(entity.getPreSettlementId());
}
}
private void validateNoTailPayment(PaymentApplication entity) {
if (!"settlement_payment".equals(entity.getPaymentType())) return;
List<Long> formalSettlementIds = new java.util.ArrayList<>();
if (entity.getSettlementId() != null) formalSettlementIds.add(entity.getSettlementId());
formalSettlementIds.addAll(settlementRelationMapper.selectList(Wrappers.<PaymentApplicationSettlement>lambdaQuery()
.select(PaymentApplicationSettlement::getFormalSettlementId)
.eq(PaymentApplicationSettlement::getPaymentApplicationId, entity.getId())
.eq(PaymentApplicationSettlement::getIsDeleted, 0))
.stream().map(PaymentApplicationSettlement::getFormalSettlementId).filter(Objects::nonNull).toList());
formalSettlementIds = formalSettlementIds.stream().distinct().toList();
if (formalSettlementIds.isEmpty()) return;
long activeTailPaymentCount = formalSettlementPaymentMapper.selectCount(Wrappers.<FormalSettlementPayment>lambdaQuery()
.eq(FormalSettlementPayment::getIsDeleted, 0)
.in(FormalSettlementPayment::getFormalSettlementId, formalSettlementIds)
.notIn(FormalSettlementPayment::getBillStatus, RETURNED, VOIDED));
if (activeTailPaymentCount > 0) {
throw new ServiceException("该结算单正在申请结算尾款,不允许再次提交申请付款");
}
}
private PaymentApplication existing(Long id) { PaymentApplication entity = getById(id); if (entity == null || Objects.equals(entity.getIsDeleted(), 1)) throw new ServiceException("付款申请不存在"); return entity; }
private PaymentApplication editable(Long id) { PaymentApplication entity = existing(id); if (!List.of(DRAFT, RETURNED).contains(entity.getApprovalStatus())) throw new ServiceException("当前状态不可编辑"); return entity; }
private String nextNo() { String prefix = "FKD" + LocalDate.now().format(DateTimeFormatter.BASIC_ISO_DATE); return prefix + String.format("%05d", count(Wrappers.<PaymentApplication>lambdaQuery().likeRight(PaymentApplication::getPaymentNo, prefix)) + 1); }
@@ -961,9 +961,10 @@ public class PreSettlementServiceImpl extends BaseServiceImpl<PreSettlementMappe
if (Integer.valueOf(1).equals(requestRow.getManualFlag())) {
String feeType = requiredText(requestRow.getFeeType(), "费用类型");
String feeItem = requiredText(requestRow.getFeeItem(), "费用项");
if (!allowedManualFees.getOrDefault(feeType, Set.of()).contains(feeItem)) {
throw new ServiceException("费用类型与费用项不匹配或费用项已停用");
}
// 暂时注释费用类型与费用项匹配及停用校验
// if (!allowedManualFees.getOrDefault(feeType, Set.of()).contains(feeItem)) {
// throw new ServiceException("费用类型与费用项不匹配或费用项已停用");
// }
PreSettlementSummaryFee row = requestRow.getId() == null ? null : existingMap.get(requestRow.getId());
if (row == null) {
row = existingMap.values().stream()
@@ -102,6 +102,7 @@ public class ReceivablePayableDetailServiceImpl
private static final String SOURCE_MASTER_ORDER = "总单系统生成";
private static final String SOURCE_LOADING_ORDER = "配载单系统生成";
private static final String SOURCE_MANUAL_GENERATION = "手动生成";
private static final String FEE_SOURCE_AUTO = "自动生成";
private static final String FEE_SOURCE_MANUAL = "手动录入";
private static final Set<String> FEE_SOURCE_MANUAL_LEGACY = Set.of("手工录入", "手动添加");
@@ -258,13 +259,16 @@ public class ReceivablePayableDetailServiceImpl
if (!"pending".equals(detail.getSettlementStatus())) {
continue;
}
BigDecimal before = money(detail.getTotalAmount());
List<ReceivablePayableCargoFee> beforeRows = activeCargoFees(detail.getId());
rebuildDetailFee(detail, request.getBillingPlanId());
if (request.getAdjustAmount() != null && request.getAdjustAmount().compareTo(BigDecimal.ZERO) != 0) {
applyManualAdjustment(detail, request.getAdjustAmount(), request.getAdjustFeeItem(), request.getAdjustReason());
}
saveChangeRecord(detail, "【费用合计】从[" + formatMoney(before) + "]调整为[" + formatMoney(detail.getTotalAmount()) + "]",
request.getAdjustReason());
List<ReceivablePayableCargoFee> afterRows = activeCargoFees(detail.getId());
List<String> changes = reconcileUpdatedFeeAmounts(beforeRows, afterRows);
for (int index = 0; index < changes.size(); index++) {
saveChangeRecord(detail, changes.get(index), request.getAdjustReason(), String.format("%04d", index + 1));
}
}
}
@@ -605,6 +609,7 @@ public class ReceivablePayableDetailServiceImpl
continue;
}
ReceivablePayableDetail detail = buildDetail(waybill, contract, request.getBillingPlanId(), settlementType(request.getSettlementType()));
detail.setSourceType(SOURCE_MANUAL_GENERATION);
save(detail);
calculatedFees(waybill, contract, request.getBillingPlanId()).forEach(fee -> {
fee.setDetailId(detail.getId());
@@ -1969,6 +1974,101 @@ public class ReceivablePayableDetailServiceImpl
updateById(detail);
}
private List<ReceivablePayableCargoFee> activeCargoFees(Long detailId) {
return cargoFeeMapper.selectList(Wrappers.<ReceivablePayableCargoFee>lambdaQuery()
.eq(ReceivablePayableCargoFee::getDetailId, detailId)
.eq(ReceivablePayableCargoFee::getIsDeleted, 0)
.orderByAsc(ReceivablePayableCargoFee::getLineNo));
}
private List<String> reconcileUpdatedFeeAmounts(List<ReceivablePayableCargoFee> beforeRows,
List<ReceivablePayableCargoFee> afterRows) {
Map<String, ReceivablePayableCargoFee> beforeByKey = beforeRows.stream()
.collect(Collectors.toMap(this::feeRowKey, row -> row, (left, right) -> left, LinkedHashMap::new));
Set<String> matchedKeys = new LinkedHashSet<>();
List<String> changes = new ArrayList<>();
for (ReceivablePayableCargoFee after : afterRows) {
String key = feeRowKey(after);
ReceivablePayableCargoFee before = beforeByKey.get(key);
if (before != null) matchedKeys.add(key);
BigDecimal beforeAmount = before == null ? BigDecimal.ZERO : money(before.getAfterAmount());
BigDecimal afterAmount = money(after.getAfterAmount());
boolean changed = before == null || feeChanged(before, after);
if (changed) {
after.setOriginalAmount(beforeAmount);
after.setAdjustAmount(afterAmount.subtract(beforeAmount));
} else {
after.setOriginalAmount(before.getOriginalAmount());
after.setAdjustAmount(before.getAdjustAmount());
}
after.setAfterAmount(afterAmount);
cargoFeeMapper.updateById(after);
if (changed) {
changes.add(updatedFeeChangeContent(before, after));
}
}
for (ReceivablePayableCargoFee before : beforeRows) {
if (!matchedKeys.contains(feeRowKey(before))) {
changes.add(updatedFeeChangeContent(before, null));
}
}
return changes;
}
private String updatedFeeChangeContent(ReceivablePayableCargoFee before,
ReceivablePayableCargoFee after) {
List<String> changes = new ArrayList<>();
BigDecimal beforeFreight = before == null ? BigDecimal.ZERO : money(before.getFreightAmount());
BigDecimal afterFreight = after == null ? BigDecimal.ZERO : money(after.getFreightAmount());
if (beforeFreight.compareTo(afterFreight) != 0) {
changes.add("【运输费】从[" + formatMoney(beforeFreight) + "]调整为["
+ formatMoney(afterFreight) + "]");
}
Map<String, Object> beforeItems = parseMap(before == null ? null : before.getFeeItemsJson());
Map<String, Object> afterItems = parseMap(after == null ? null : after.getFeeItemsJson());
Set<String> names = new LinkedHashSet<>(beforeItems.keySet());
names.addAll(afterItems.keySet());
for (String name : names) {
if (isFreightFeeItem(name)) continue;
BigDecimal beforeAmount = decimal(beforeItems.get(name));
BigDecimal afterAmount = decimal(afterItems.get(name));
if (beforeAmount.compareTo(afterAmount) != 0) {
changes.add("" + name + "】从[" + formatMoney(beforeAmount) + "]调整为["
+ formatMoney(afterAmount) + "]");
}
}
if (changes.isEmpty()) {
BigDecimal beforeAmount = before == null ? BigDecimal.ZERO : money(before.getAfterAmount());
BigDecimal afterAmount = after == null ? BigDecimal.ZERO : money(after.getAfterAmount());
changes.add("【费用合计】从[" + formatMoney(beforeAmount) + "]调整为["
+ formatMoney(afterAmount) + "]");
}
ReceivablePayableCargoFee cargoFee = after == null ? before : after;
return String.join("", changes) + "" + Objects.toString(cargoFee.getCargoName(), "") + "";
}
private String feeRowKey(ReceivablePayableCargoFee fee) {
if (Func.isNotEmpty(fee.getLineNo())) return fee.getLineNo();
return String.join("|", Objects.toString(fee.getCargoName(), ""),
Objects.toString(fee.getCargoType(), ""), Objects.toString(fee.getSpecification(), ""),
Objects.toString(fee.getModel(), ""));
}
private boolean feeChanged(ReceivablePayableCargoFee before, ReceivablePayableCargoFee after) {
return money(before.getAfterAmount()).compareTo(money(after.getAfterAmount())) != 0
|| money(before.getFreightAmount()).compareTo(money(after.getFreightAmount())) != 0
|| feeItemsChanged(before.getFeeItemsJson(), after.getFeeItemsJson());
}
private boolean feeItemsChanged(String beforeJson, String afterJson) {
Map<String, Object> beforeItems = parseMap(beforeJson);
Map<String, Object> afterItems = parseMap(afterJson);
Set<String> names = new LinkedHashSet<>(beforeItems.keySet());
names.addAll(afterItems.keySet());
return names.stream().anyMatch(name ->
decimal(beforeItems.get(name)).compareTo(decimal(afterItems.get(name))) != 0);
}
private String aggregateFeeItemsJson(List<ReceivablePayableCargoFee> fees) {
Map<String, BigDecimal> feeItems = new LinkedHashMap<>();
fees.forEach(fee -> parseMap(fee.getFeeItemsJson()).forEach((name, value) ->
@@ -13,7 +13,10 @@ import org.springblade.transport.mapper.FormalSettlementDetailFeeMapper;
import org.springblade.transport.mapper.FormalSettlementDetailMapper;
import org.springblade.transport.mapper.FormalSettlementChangeRecordMapper;
import org.springblade.transport.mapper.FormalSettlementMapper;
import org.springblade.transport.mapper.FormalSettlementPaymentMapper;
import org.springblade.transport.mapper.FormalSettlementSummaryFeeMapper;
import org.springblade.transport.mapper.PaymentApplicationMapper;
import org.springblade.transport.mapper.PaymentApplicationSettlementMapper;
import org.springblade.transport.mapper.SettlementAdjustmentDetailMapper;
import org.springblade.transport.mapper.SettlementAdjustmentMapper;
import org.springblade.transport.pojo.dto.SettlementAdjustmentSaveRequest;
@@ -22,7 +25,10 @@ import org.springblade.transport.pojo.entity.FormalSettlement;
import org.springblade.transport.pojo.entity.FormalSettlementChangeRecord;
import org.springblade.transport.pojo.entity.FormalSettlementDetail;
import org.springblade.transport.pojo.entity.FormalSettlementDetailFee;
import org.springblade.transport.pojo.entity.FormalSettlementPayment;
import org.springblade.transport.pojo.entity.FormalSettlementSummaryFee;
import org.springblade.transport.pojo.entity.PaymentApplication;
import org.springblade.transport.pojo.entity.PaymentApplicationSettlement;
import org.springblade.transport.pojo.entity.SettlementAdjustment;
import org.springblade.transport.pojo.entity.SettlementAdjustmentDetail;
import org.springblade.transport.pojo.vo.SettlementAdjustmentVO;
@@ -36,9 +42,11 @@ import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
@Service
@RequiredArgsConstructor
@@ -48,6 +56,7 @@ public class SettlementAdjustmentServiceImpl extends BaseServiceImpl<SettlementA
private static final String REVIEWING = "reviewing";
private static final String APPROVED = "approved";
private static final String RETURNED = "returned";
private static final String VOIDED = "voided";
private static final String PAID = "paid";
private final SettlementAdjustmentDetailMapper detailMapper;
private final FormalSettlementMapper formalMapper;
@@ -55,6 +64,9 @@ public class SettlementAdjustmentServiceImpl extends BaseServiceImpl<SettlementA
private final FormalSettlementDetailFeeMapper formalFeeMapper;
private final FormalSettlementSummaryFeeMapper formalSummaryFeeMapper;
private final FormalSettlementChangeRecordMapper formalChangeRecordMapper;
private final FormalSettlementPaymentMapper formalPaymentMapper;
private final PaymentApplicationMapper paymentApplicationMapper;
private final PaymentApplicationSettlementMapper paymentApplicationSettlementMapper;
@Override
public IPage<SettlementAdjustmentVO> selectPage(IPage<SettlementAdjustment> page, SettlementAdjustmentVO query) {
@@ -94,6 +106,11 @@ public class SettlementAdjustmentServiceImpl extends BaseServiceImpl<SettlementA
.or().ne(FormalSettlement::getPaymentStatus, PAID))
.like(Func.isNotEmpty(keyword), FormalSettlement::getFormalSettlementNo, keyword)
.orderByDesc(FormalSettlement::getCreateTime));
Set<Long> tailPaymentSettlementIds = tailPaymentSettlementIds(rows.stream()
.map(FormalSettlement::getId).filter(Objects::nonNull).toList());
rows = rows.stream().filter(row -> !isSettlementCompleted(row)
&& !isTailSettlement(row)
&& !tailPaymentSettlementIds.contains(row.getId())).toList();
List<Map<String, Object>> result = new ArrayList<>();
for (FormalSettlement row : rows) {
Map<String, Object> item = new HashMap<>();
@@ -133,7 +150,7 @@ public class SettlementAdjustmentServiceImpl extends BaseServiceImpl<SettlementA
public Long saveDraft(SettlementAdjustmentSaveRequest request) {
SettlementAdjustment adjustment = request.getId() == null ? new SettlementAdjustment() : editable(request.getId());
FormalSettlement formal = request.getFormalSettlementId() == null
? null : formalMapper.selectById(request.getFormalSettlementId());
? null : adjustableFormalSettlement(request.getFormalSettlementId());
if (adjustment.getId() == null) adjustment.setAdjustmentNo(nextNo());
adjustment.setApprovalStatus(DRAFT);
adjustment.setCurrentNode("草稿");
@@ -285,11 +302,68 @@ public class SettlementAdjustmentServiceImpl extends BaseServiceImpl<SettlementA
if (formal == null || !APPROVED.equals(formal.getApprovalStatus())) {
throw new ServiceException("仅审批通过的正式结算单可调整");
}
if (PAID.equals(formal.getPaymentStatus())) {
if (isSettlementCompleted(formal)) {
throw new ServiceException("该正式结算单已完成收/付款,不能进行结算调整");
}
if (isTailSettlement(formal)) {
throw new ServiceException("尾款结算正式结算单不能进行结算调整");
}
if (hasTailPayment(formal.getId())) {
throw new ServiceException("该正式结算单已发起尾款结算,不能进行结算调整");
}
return formal;
}
private boolean isSettlementCompleted(FormalSettlement formal) {
return PAID.equals(formal.getPaymentStatus())
|| (money(formal.getSettlementAmount()).signum() > 0
&& money(formal.getPaidAmount()).compareTo(money(formal.getSettlementAmount())) >= 0);
}
private boolean isTailSettlement(FormalSettlement formal) {
return formal != null && "预结算合并".equals(formal.getSourceType());
}
private boolean hasTailPayment(Long formalSettlementId) {
return formalSettlementId != null && tailPaymentSettlementIds(List.of(formalSettlementId))
.contains(formalSettlementId);
}
private Set<Long> tailPaymentSettlementIds(List<Long> formalSettlementIds) {
if (formalSettlementIds == null || formalSettlementIds.isEmpty()) return Set.of();
Set<Long> result = new HashSet<>(formalPaymentMapper.selectList(
Wrappers.<FormalSettlementPayment>lambdaQuery()
.eq(FormalSettlementPayment::getIsDeleted, 0)
.in(FormalSettlementPayment::getFormalSettlementId, formalSettlementIds)
.notIn(FormalSettlementPayment::getBillStatus, RETURNED, VOIDED)).stream()
.map(FormalSettlementPayment::getFormalSettlementId)
.filter(Objects::nonNull).toList());
result.addAll(paymentApplicationMapper.selectList(Wrappers.<PaymentApplication>lambdaQuery()
.select(PaymentApplication::getSettlementId)
.in(PaymentApplication::getSettlementId, formalSettlementIds)
.eq(PaymentApplication::getPaymentType, "settlement_payment")
.eq(PaymentApplication::getIsDeleted, 0)
.notIn(PaymentApplication::getApprovalStatus, RETURNED, VOIDED)).stream()
.map(PaymentApplication::getSettlementId).filter(Objects::nonNull).toList());
List<PaymentApplicationSettlement> relations = paymentApplicationSettlementMapper.selectList(
Wrappers.<PaymentApplicationSettlement>lambdaQuery()
.select(PaymentApplicationSettlement::getPaymentApplicationId,
PaymentApplicationSettlement::getFormalSettlementId)
.in(PaymentApplicationSettlement::getFormalSettlementId, formalSettlementIds)
.eq(PaymentApplicationSettlement::getIsDeleted, 0));
List<Long> applicationIds = relations.stream()
.map(PaymentApplicationSettlement::getPaymentApplicationId)
.filter(Objects::nonNull).distinct().toList();
if (applicationIds.isEmpty()) return result;
Set<Long> activeApplicationIds = new HashSet<>(paymentApplicationMapper.selectList(
Wrappers.<PaymentApplication>lambdaQuery()
.select(PaymentApplication::getId)
.in(PaymentApplication::getId, applicationIds)
.eq(PaymentApplication::getPaymentType, "settlement_payment")
.eq(PaymentApplication::getIsDeleted, 0)
.notIn(PaymentApplication::getApprovalStatus, RETURNED, VOIDED)).stream()
.map(PaymentApplication::getId).filter(Objects::nonNull).toList());
relations.stream().filter(relation -> activeApplicationIds.contains(relation.getPaymentApplicationId()))
.map(PaymentApplicationSettlement::getFormalSettlementId).filter(Objects::nonNull)
.forEach(result::add);
return result;
}
private boolean isManualFee(SettlementAdjustmentSaveRequest.Detail row) { return row.getFormalSettlementDetailId() == null && row.getFormalSettlementDetailFeeId() == null; }
private boolean isManualFee(SettlementAdjustmentDetail row) { return row.getFormalSettlementDetailId() == null && row.getFormalSettlementDetailFeeId() == null; }
private FormalSettlementDetailFee validateFee(Long formalId, Long detailId, Long feeId) { FormalSettlementDetail detail = formalDetailMapper.selectById(detailId); FormalSettlementDetailFee fee = formalFeeMapper.selectById(feeId); if (detail == null || fee == null || !Objects.equals(detail.getFormalSettlementId(), formalId) || !Objects.equals(fee.getFormalSettlementDetailId(), detailId)) throw new ServiceException("费用明细不存在或不属于关联正式结算单"); return fee; }