feat: 正式结算单整合项目预付核销并关联展示预付付款记录

核销金额计入结算单已申请/已付汇总(仅已付款预付可核销);保存转移来源预结算核销行、调整重推迁移核销行;付款申请作废/结算单作废/金蝶删单回调/预结算作废联动清理核销行;结算调整前校验核销合计;详情返回统一预付付款记录(advancePayments);列表页批量汇总避免N+1
This commit is contained in:
刘泉佳
2026-10-18 14:31:10 +08:00
parent 32f14b9299
commit a4b5f09160
10 changed files with 332 additions and 8 deletions
@@ -72,6 +72,7 @@ public class FormalSettlementVO extends FormalSettlement implements KingdeeUnloc
@TableField(exist = false) @Schema(description = "开票申请经金蝶回写的发票结果(含文件地址,只读展示)")
private List<InvoiceApplicationInvoice> issuedInvoices;
@TableField(exist = false) private List<PaymentApplicationVO> paymentApplications;
@TableField(exist = false) private List<SettlementAdvancePaymentVO> advancePayments;
@TableField(exist = false) private List<SettlementAdjustmentVO> adjustments;
@TableField(exist = false) private List<FormalSettlementChangeRecord> changeRecords;
}
@@ -0,0 +1,72 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.transport.pojo.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
import java.math.BigDecimal;
import java.util.Date;
/**
* 结算单统一预付付款记录(进度预付 / 项目预付核销,供付款记录合并展示)
*
* @author Chill
*/
@Data
@Schema(description = "结算单统一预付付款记录")
public class SettlementAdvancePaymentVO implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
@Schema(description = "记录类型:progress_advance 进度预付 / project_advance 项目预付核销")
private String recordType;
@Schema(description = "付款单号")
private String paymentNo;
@Schema(description = "申请金额(进度预付为申请预付金额,项目预付为本次核销金额)")
private BigDecimal appliedAmount;
@Schema(description = "已付款金额(底层付款申请实付)")
private BigDecimal paidAmount;
@Schema(description = "单据状态")
private String billStatus;
@Schema(description = "金蝶单据号")
private String kingdeeBillNo;
@Schema(description = "创建人姓名")
private String createUserName;
@Schema(description = "创建时间(统一取底层付款申请创建时间)")
private Date createTime;
}
@@ -15,6 +15,9 @@ import org.apache.ibatis.annotations.Param;
import org.springblade.transport.pojo.entity.SettlementProjectAdvanceWriteOff;
import java.math.BigDecimal;
import java.util.Collection;
import java.util.List;
import java.util.Map;
/**
* 结算单项目预付核销 Mapper
@@ -37,4 +40,12 @@ public interface SettlementProjectAdvanceWriteOffMapper extends BaseMapper<Settl
BigDecimal sumWriteOffBySettlement(@Param("settlementType") String settlementType,
@Param("settlementId") Long settlementId);
/**
* 按结算单集合分组统计核销合计(一次 group-by,供批量刷新与列表页使用,避免逐单 N+1)。
*
* @return settlementId / totalAmount 两列
*/
List<Map<String, Object>> sumWriteOffBySettlementIds(@Param("settlementType") String settlementType,
@Param("settlementIds") Collection<Long> settlementIds);
}
@@ -32,4 +32,16 @@
AND settlement_id = #{settlementId}
</select>
<select id="sumWriteOffBySettlementIds" resultType="java.util.HashMap">
SELECT settlement_id AS settlementId, COALESCE(SUM(write_off_amount), 0) AS totalAmount
FROM blade_settlement_project_advance_write_off
WHERE is_deleted = 0
AND settlement_type = #{settlementType}
AND settlement_id IN
<foreach collection="settlementIds" item="settlementId" open="(" separator="," close=")">
#{settlementId}
</foreach>
GROUP BY settlement_id
</select>
</mapper>
@@ -14,7 +14,9 @@ import org.springblade.transport.pojo.entity.SettlementProjectAdvanceWriteOff;
import org.springblade.transport.pojo.vo.ProjectAdvanceWriteOffOptionVO;
import java.math.BigDecimal;
import java.util.Collection;
import java.util.List;
import java.util.Map;
/**
* 结算单项目预付核销服务
@@ -24,7 +26,7 @@ import java.util.List;
public interface ISettlementProjectAdvanceWriteOffService {
/**
* 按合同查询可选项目预付(审批通过且仍有可核销余额)。
* 按合同查询可选项目预付(审批通过、金蝶已付款且仍有可核销余额)。
*/
List<ProjectAdvanceWriteOffOptionVO> listOptions(Long contractId, String excludeSettlementType,
Long excludeSettlementId);
@@ -39,6 +41,11 @@ public interface ISettlementProjectAdvanceWriteOffService {
*/
BigDecimal sumBySettlement(String settlementType, Long settlementId);
/**
* 按结算单集合分组统计核销合计(一次 group-by,供批量刷新与列表页使用)。
*/
Map<Long, BigDecimal> sumBySettlementIds(String settlementType, Collection<Long> settlementIds);
/**
* 覆盖保存核销明细,并校验不超过可核销金额与结算单金额。
*/
@@ -50,4 +57,14 @@ public interface ISettlementProjectAdvanceWriteOffService {
*/
void deleteBySettlement(String settlementType, Long settlementId);
/**
* 删除指定结算单集合上的核销明细(核销行转移/结算单作废联动),返回删除行数。
*/
int deleteBySettlementIds(String settlementType, Collection<Long> settlementIds);
/**
* 按付款申请删除其核销明细(付款申请作废联动),返回受影响的正式结算单id。
*/
List<Long> deleteByPaymentApplicationIds(Collection<Long> paymentApplicationIds);
}
@@ -33,12 +33,14 @@ import org.springblade.transport.mapper.InvoiceApplicationMapper;
import org.springblade.transport.mapper.InvoiceApplicationSettlementMapper;
import org.springblade.transport.mapper.PreSettlementDetailMapper;
import org.springblade.transport.mapper.PreSettlementDetailFeeMapper;
import org.springblade.transport.mapper.PreSettlementAdvanceMapper;
import org.springblade.transport.mapper.PreSettlementMapper;
import org.springblade.transport.mapper.PreSettlementSummaryFeeMapper;
import org.springblade.transport.mapper.PaymentApplicationMapper;
import org.springblade.transport.mapper.PaymentApplicationInvoiceMapper;
import org.springblade.transport.mapper.PaymentApplicationSettlementMapper;
import org.springblade.transport.mapper.ReceiptClaimSettlementMapper;
import org.springblade.transport.mapper.SettlementProjectAdvanceWriteOffMapper;
import org.springblade.transport.constant.KingdeeAttachmentConstant;
import org.springblade.transport.config.BfiProperties;
import org.springblade.transport.constant.KingdeeBfiConstant;
@@ -69,6 +71,7 @@ import org.springblade.transport.pojo.entity.InvoiceApplicationInvoice;
import org.springblade.transport.pojo.entity.InvoiceApplicationSettlement;
import org.springblade.transport.pojo.entity.ContractManage;
import org.springblade.transport.pojo.entity.PreSettlement;
import org.springblade.transport.pojo.entity.PreSettlementAdvance;
import org.springblade.transport.pojo.entity.PreSettlementDetail;
import org.springblade.transport.pojo.entity.PreSettlementDetailFee;
import org.springblade.transport.pojo.entity.PreSettlementSummaryFee;
@@ -79,9 +82,11 @@ import org.springblade.transport.pojo.entity.ReceiptClaimSettlement;
import org.springblade.transport.pojo.entity.ReceivablePayableDetail;
import org.springblade.transport.pojo.entity.ReceivablePayableCargoFee;
import org.springblade.transport.pojo.entity.SettlementAdjustment;
import org.springblade.transport.pojo.entity.SettlementProjectAdvanceWriteOff;
import org.springblade.transport.pojo.entity.Waybill;
import org.springblade.transport.pojo.vo.FormalSettlementVO;
import org.springblade.transport.pojo.vo.PreSettlementVO;
import org.springblade.transport.pojo.vo.SettlementAdvancePaymentVO;
import org.springblade.transport.pojo.vo.SettlementAdjustmentVO;
import org.springblade.transport.service.IFormalSettlementService;
import org.springblade.transport.service.IInvoiceReceiptService;
@@ -113,6 +118,8 @@ import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Comparator;
import java.util.Date;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
@@ -162,6 +169,8 @@ public class FormalSettlementServiceImpl extends BaseServiceImpl<FormalSettlemen
private final PreSettlementSummaryFeeMapper preSummaryFeeMapper;
private final PreSettlementDetailMapper preDetailMapper;
private final PreSettlementDetailFeeMapper preDetailFeeMapper;
private final PreSettlementAdvanceMapper preSettlementAdvanceMapper;
private final SettlementProjectAdvanceWriteOffMapper projectAdvanceWriteOffMapper;
private final ReceivablePayableDetailMapper receivablePayableMapper;
private final ReceivablePayableCargoFeeMapper receivablePayableCargoFeeMapper;
private final PaymentApplicationMapper paymentApplicationMapper;
@@ -207,7 +216,13 @@ public class FormalSettlementServiceImpl extends BaseServiceImpl<FormalSettlemen
if (ids.isEmpty()) wrapper.eq(FormalSettlement::getId, -1L); else wrapper.in(FormalSettlement::getId, ids);
}
IPage<FormalSettlement> result = page(page, wrapper.orderByDesc(FormalSettlement::getCreateTime));
IPage<FormalSettlementVO> voPage = result.convert(this::toVO);
// 本页应付结算单的项目预付核销合计一次查询,避免逐行 toVO 重算汇总时的 N+1
Map<Long, BigDecimal> writeOffAmounts = this.projectAdvanceWriteOffService.sumBySettlementIds(
TransportBusiConstant.SETTLEMENT_BILL_FORMAL,
result.getRecords().stream()
.filter(item -> "payable".equals(item.getSettlementType()))
.map(FormalSettlement::getId).toList());
IPage<FormalSettlementVO> voPage = result.convert(item -> this.toVO(item, writeOffAmounts));
fillAttachmentSyncStatus(voPage.getRecords());
fillKingdeeLockStatus(voPage.getRecords());
return voPage;
@@ -322,6 +337,7 @@ public class FormalSettlementServiceImpl extends BaseServiceImpl<FormalSettlemen
vo.setIssuedInvoices(listIssuedInvoices(id));
vo.setProjectAdvanceWriteOffs(projectAdvanceWriteOffService.listBySettlement(
TransportBusiConstant.SETTLEMENT_BILL_FORMAL, id));
vo.setAdvancePayments(this.listAdvancePayments(sources, vo.getProjectAdvanceWriteOffs()));
Set<Long> relationApplicationIds = paymentApplicationSettlementMapper.selectList(
Wrappers.<PaymentApplicationSettlement>lambdaQuery()
.select(PaymentApplicationSettlement::getPaymentApplicationId)
@@ -545,12 +561,17 @@ public class FormalSettlementServiceImpl extends BaseServiceImpl<FormalSettlemen
applySummaryRequest(settlement.getId(), request.getSummaryFees());
refreshSettlementAmount(settlement);
rebuildInvoices(settlement, request.getInvoices());
// 预结算转正式/调整来源:先转移来源预结算的项目预付核销行(释放可核销占用),replaceWriteOffs 的余额校验才能通过
this.projectAdvanceWriteOffService.deleteBySettlementIds(
TransportBusiConstant.SETTLEMENT_BILL_PRE, request.getSourcePreSettlementIds());
projectAdvanceWriteOffService.replaceWriteOffs(
TransportBusiConstant.SETTLEMENT_BILL_FORMAL,
settlement.getId(),
settlement.getContractId(),
settlement.getSettlementAmount(),
request.getProjectAdvanceWriteOffs());
// 核销行计入收付汇总(refreshSettlementAmount 内的刷新发生在核销落库之前,覆盖不到核销行)
this.refreshPaymentSummary(settlement.getId());
if (!creating && !restoring) {
saveChange(settlement.getId(), "结算单基本信息", null, "调整",
"保存正式结算单" + settlement.getFormalSettlementNo(), request.getRemark());
@@ -605,6 +626,9 @@ public class FormalSettlementServiceImpl extends BaseServiceImpl<FormalSettlemen
settlement.setCurrentNode("已作废");
settlement.setVoidReason(required(limit(request.getReason(), 200), "作废原因"));
updateById(settlement);
// 联动删除项目预付核销行,释放可核销占用并避免残留记录
this.projectAdvanceWriteOffService.deleteBySettlementIds(
TransportBusiConstant.SETTLEMENT_BILL_FORMAL, List.of(settlement.getId()));
saveChange(settlement.getId(), "结算单基本信息", null, "调整", "作废正式结算单", request.getReason());
}
@@ -624,6 +648,9 @@ public class FormalSettlementServiceImpl extends BaseServiceImpl<FormalSettlemen
.set(FormalSettlement::getKingdeeBillNo, null)
.set(FormalSettlement::getKingdeeBillId, null)
.set(FormalSettlement::getSyncedTime, null));
// 联动删除项目预付核销行,释放可核销占用并避免残留记录
this.projectAdvanceWriteOffService.deleteBySettlementIds(
TransportBusiConstant.SETTLEMENT_BILL_FORMAL, List.of(id));
// 变更流水:回调无登录态,操作人取金蝶报文(兜底 system,与 saveChange 同款)
FormalSettlementChangeRecord record = new FormalSettlementChangeRecord();
record.setFormalSettlementId(id);
@@ -702,8 +729,24 @@ public class FormalSettlementServiceImpl extends BaseServiceImpl<FormalSettlemen
cloneRelation.setFormalSettlementId(clone.getId());
sourceMapper.insert(cloneRelation);
}
log.info("结算调整重推克隆正式结算单:原单[{}] -> 新单[{}](明细{}条)",
source.getFormalSettlementNo(), clone.getFormalSettlementNo(), details.size());
// 核销行迁移:克隆到新单并删除原单行,避免预付可核销额度双占(原单为调整存档,收付口径以新单为准)
List<SettlementProjectAdvanceWriteOff> writeOffRows = this.projectAdvanceWriteOffMapper.selectList(
Wrappers.<SettlementProjectAdvanceWriteOff>lambdaQuery()
.eq(SettlementProjectAdvanceWriteOff::getSettlementType, TransportBusiConstant.SETTLEMENT_BILL_FORMAL)
.eq(SettlementProjectAdvanceWriteOff::getSettlementId, sourceSettlementId)
.eq(SettlementProjectAdvanceWriteOff::getIsDeleted, 0));
for (SettlementProjectAdvanceWriteOff writeOffRow : writeOffRows) {
SettlementProjectAdvanceWriteOff cloneWriteOff = Objects.requireNonNull(
BeanUtil.copyProperties(writeOffRow, SettlementProjectAdvanceWriteOff.class));
resetAuditFields(cloneWriteOff);
cloneWriteOff.setSettlementId(clone.getId());
this.projectAdvanceWriteOffMapper.insert(cloneWriteOff);
}
this.projectAdvanceWriteOffService.deleteBySettlementIds(
TransportBusiConstant.SETTLEMENT_BILL_FORMAL, List.of(sourceSettlementId));
this.refreshPaymentSummaries(List.of(sourceSettlementId, clone.getId()));
log.info("结算调整重推克隆正式结算单:原单[{}] -> 新单[{}](明细{}条,核销迁移{}条)",
source.getFormalSettlementNo(), clone.getFormalSettlementNo(), details.size(), writeOffRows.size());
return new CloneResult(clone.getId(), detailIdMap, feeIdMap);
}
@@ -1601,6 +1644,11 @@ public class FormalSettlementServiceImpl extends BaseServiceImpl<FormalSettlemen
Collectors.reducing(BigDecimal.ZERO,
item -> money(item.getAllocatedReceiptAmount()), BigDecimal::add)))
: Map.of();
// 项目预付核销合计(仅应付结算单需要,一次 group-by 避免逐单查询)
Map<Long, BigDecimal> writeOffAmountsBySettlement = hasPayable
? this.projectAdvanceWriteOffService.sumBySettlementIds(
TransportBusiConstant.SETTLEMENT_BILL_FORMAL, settledIds)
: Map.of();
for (FormalSettlement settlement : settlements) {
List<PaymentApplicationSettlement> relations = relationsBySettlement
@@ -1616,7 +1664,8 @@ public class FormalSettlementServiceImpl extends BaseServiceImpl<FormalSettlemen
PaymentSummary summary = calculatePaymentSummary(settlement, new PaymentSummaryData(
relations, directApplications, relationStatuses,
sourcesBySettlement.getOrDefault(settlement.getId(), List.of()),
receiptClaimPaidBySettlement.getOrDefault(settlement.getId(), BigDecimal.ZERO)));
receiptClaimPaidBySettlement.getOrDefault(settlement.getId(), BigDecimal.ZERO),
writeOffAmountsBySettlement));
settlement.setAppliedPaymentAmount(summary.appliedAmount());
settlement.setPaidAmount(summary.paidAmount());
settlement.setRemainingPayableAmount(money(settlement.getSettlementAmount())
@@ -1825,9 +1874,16 @@ public class FormalSettlementServiceImpl extends BaseServiceImpl<FormalSettlemen
}
private FormalSettlementVO toVO(FormalSettlement entity) {
return this.toVO(entity, null);
}
/**
* @param writeOffAmounts 结算单id -&gt; 项目预付核销合计(列表页批量传入避免 N+1;null 表示按需单笔查询)
*/
private FormalSettlementVO toVO(FormalSettlement entity, Map<Long, BigDecimal> writeOffAmounts) {
FormalSettlementVO vo = FormalSettlementWrapper.build().entityVO(entity);
vo.setInvoiceStatus(invoiceStatus(entity.getInvoiceAmount(), entity.getSettlementAmount()));
PaymentSummary summary = calculatePaymentSummary(entity);
PaymentSummary summary = this.calculatePaymentSummary(entity, writeOffAmounts);
vo.setAppliedPaymentAmount(summary.appliedAmount());
vo.setPaidAmount(summary.paidAmount());
vo.setRemainingPayableAmount(money(entity.getSettlementAmount()).subtract(summary.paidAmount())
@@ -1837,6 +1893,10 @@ public class FormalSettlementServiceImpl extends BaseServiceImpl<FormalSettlemen
}
private PaymentSummary calculatePaymentSummary(FormalSettlement settlement) {
return this.calculatePaymentSummary(settlement, (Map<Long, BigDecimal>) null);
}
private PaymentSummary calculatePaymentSummary(FormalSettlement settlement, Map<Long, BigDecimal> writeOffAmounts) {
List<PaymentApplicationSettlement> relations = paymentApplicationSettlementMapper.selectList(
Wrappers.<PaymentApplicationSettlement>lambdaQuery()
.eq(PaymentApplicationSettlement::getFormalSettlementId, settlement.getId())
@@ -1854,7 +1914,7 @@ public class FormalSettlementServiceImpl extends BaseServiceImpl<FormalSettlemen
.in(PaymentApplication::getId, relationApplicationIds).eq(PaymentApplication::getIsDeleted, 0))
.stream().collect(Collectors.toMap(PaymentApplication::getId, PaymentApplication::getApprovalStatus));
return calculatePaymentSummary(settlement, new PaymentSummaryData(relations, applications,
relationStatuses, null, null));
relationStatuses, null, null, writeOffAmounts));
}
/**
@@ -1887,6 +1947,13 @@ public class FormalSettlementServiceImpl extends BaseServiceImpl<FormalSettlemen
.map(this::money).reduce(BigDecimal.ZERO, BigDecimal::add));
paidAmount = paidAmount.add(sources.stream().map(FormalSettlementSource::getAdvancePaidAmount)
.map(this::money).reduce(BigDecimal.ZERO, BigDecimal::add));
// 项目预付核销计入已付/已申请(核销行底层预付必已付款,核销额即实付抵扣)
BigDecimal writeOffAmount = data.writeOffAmounts() != null
? data.writeOffAmounts().getOrDefault(settlement.getId(), BigDecimal.ZERO)
: this.projectAdvanceWriteOffService.sumBySettlement(
TransportBusiConstant.SETTLEMENT_BILL_FORMAL, settlement.getId());
appliedAmount = appliedAmount.add(this.money(writeOffAmount));
paidAmount = paidAmount.add(this.money(writeOffAmount));
}
if ("receivable".equals(settlement.getSettlementType())) {
paidAmount = data.receiptClaimPaid() != null ? data.receiptClaimPaid()
@@ -1922,10 +1989,12 @@ public class FormalSettlementServiceImpl extends BaseServiceImpl<FormalSettlemen
* @param relationStatuses 付款申请id → 审批状态
* @param sources 预收来源行(仅应付结算单;null 表示由纯计算按需查询)
* @param receiptClaimPaid 收款认领分摊聚合(仅应收结算单;null 表示由纯计算按需查询)
* @param writeOffAmounts 结算单id → 项目预付核销合计(仅应付结算单;null 表示由纯计算按需查询)
*/
private record PaymentSummaryData(List<PaymentApplicationSettlement> relations,
List<PaymentApplication> directApplications, Map<Long, String> relationStatuses,
List<FormalSettlementSource> sources, BigDecimal receiptClaimPaid) {
List<FormalSettlementSource> sources, BigDecimal receiptClaimPaid,
Map<Long, BigDecimal> writeOffAmounts) {
}
private SettlementAdjustmentVO toAdjustmentVO(SettlementAdjustment entity) {
@@ -1937,6 +2006,74 @@ public class FormalSettlementServiceImpl extends BaseServiceImpl<FormalSettlemen
return vo;
}
/**
* 统一预付付款记录:进度预付(来源预结算台账)+ 项目预付核销(回填底层付款申请实付)。
* createTime 统一取底层付款申请创建时间(项目预付核销行已回填 paymentCreateTime,进度预付按单号反查)。
*/
private List<SettlementAdvancePaymentVO> listAdvancePayments(List<FormalSettlementSource> sources,
List<SettlementProjectAdvanceWriteOff> writeOffs) {
List<SettlementAdvancePaymentVO> records = new ArrayList<>();
List<Long> preSettlementIds = sources.stream().map(FormalSettlementSource::getPreSettlementId)
.filter(Objects::nonNull).distinct().toList();
if (!preSettlementIds.isEmpty()) {
List<PreSettlementAdvance> advances = this.preSettlementAdvanceMapper.selectList(
Wrappers.<PreSettlementAdvance>lambdaQuery()
.eq(PreSettlementAdvance::getIsDeleted, 0)
.ne(PreSettlementAdvance::getBillStatus, TransportBusiConstant.VOIDED)
.in(PreSettlementAdvance::getPreSettlementId, preSettlementIds));
// 进度预付行的展示时间取底层付款申请创建时间(advanceNo 即付款申请单号)
List<String> advanceNos = advances.stream().map(PreSettlementAdvance::getAdvanceNo)
.filter(Func::isNotEmpty).distinct().toList();
Map<String, Date> applicationTimes = advanceNos.isEmpty() ? Map.of()
: this.paymentApplicationMapper.selectList(Wrappers.<PaymentApplication>lambdaQuery()
.select(PaymentApplication::getPaymentNo, PaymentApplication::getCreateTime)
.eq(PaymentApplication::getIsDeleted, 0)
.in(PaymentApplication::getPaymentNo, advanceNos))
.stream().filter(item -> item.getCreateTime() != null)
.collect(Collectors.toMap(PaymentApplication::getPaymentNo,
PaymentApplication::getCreateTime, (first, second) -> first));
for (PreSettlementAdvance advance : advances) {
SettlementAdvancePaymentVO record = new SettlementAdvancePaymentVO();
record.setRecordType(TransportBusiConstant.PAYMENT_TYPE_PROGRESS_ADVANCE);
record.setPaymentNo(advance.getAdvanceNo());
record.setAppliedAmount(advance.getAppliedAmount());
record.setPaidAmount(advance.getPaidAmount());
record.setBillStatus(advance.getBillStatus());
record.setKingdeeBillNo(advance.getKingdeeAdvanceNo());
record.setCreateUserName(UserCache.getUserRealName(advance.getCreateUser()));
record.setCreateTime(applicationTimes.getOrDefault(advance.getAdvanceNo(), advance.getCreateTime()));
records.add(record);
}
}
if (Func.isNotEmpty(writeOffs)) {
List<Long> applicationIds = writeOffs.stream()
.map(SettlementProjectAdvanceWriteOff::getPaymentApplicationId)
.filter(Objects::nonNull).distinct().toList();
Map<Long, PaymentApplication> applications = applicationIds.isEmpty() ? Map.of()
: this.paymentApplicationMapper.selectBatchIds(applicationIds).stream()
.collect(Collectors.toMap(PaymentApplication::getId, Function.identity(),
(first, second) -> first));
for (SettlementProjectAdvanceWriteOff writeOff : writeOffs) {
PaymentApplication application = applications.get(writeOff.getPaymentApplicationId());
SettlementAdvancePaymentVO record = new SettlementAdvancePaymentVO();
record.setRecordType(TransportBusiConstant.PAYMENT_TYPE_PROJECT_ADVANCE);
record.setPaymentNo(writeOff.getPaymentNo());
record.setAppliedAmount(writeOff.getWriteOffAmount());
record.setPaidAmount(application == null ? null : application.getPaidAmount());
record.setBillStatus(application == null ? null : application.getApprovalStatus());
record.setKingdeeBillNo(writeOff.getKingdeeBillNo());
record.setCreateUserName(UserCache.getUserRealName(application == null
? writeOff.getCreateUser() : application.getCreateUser()));
record.setCreateTime(writeOff.getPaymentCreateTime() != null
? writeOff.getPaymentCreateTime() : writeOff.getCreateTime());
records.add(record);
}
}
records.sort(Comparator.comparing(SettlementAdvancePaymentVO::getCreateTime,
Comparator.nullsLast(Comparator.reverseOrder())));
return records;
}
private String approvalStatusName(String value) {
if (value == null) {
return "";
@@ -456,6 +456,11 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl<PaymentApplic
updateById(entity);
// 联动作废提前建档的预结算预付记录,避免残留记录阻断预结算作废
voidPreSettlementAdvance(entity);
// 联动删除项目预付核销行,避免结算单已付汇总虚高
List<Long> writeOffSettlementIds = this.projectAdvanceWriteOffService.deleteByPaymentApplicationIds(List.of(entity.getId()));
if (!writeOffSettlementIds.isEmpty()) {
formalSettlementService.refreshPaymentSummaries(writeOffSettlementIds);
}
refreshFormalSettlement(entity);
}
@@ -461,6 +461,9 @@ public class PreSettlementServiceImpl extends BaseServiceImpl<PreSettlementMappe
settlement.setCurrentProcessor(AuthUtil.getUserName());
settlement.setVoidReason(reason);
updateById(settlement);
// 联动删除项目预付核销行,避免残留记录与可核销占用
this.projectAdvanceWriteOffService.deleteBySettlementIds(
TransportBusiConstant.SETTLEMENT_BILL_PRE, List.of(settlement.getId()));
releaseSourceDetails(settlement, listDetails(settlement.getId()));
saveChange(settlement.getId(), "结算单基本信息", null, "调整", "作废预结算单", reason);
}
@@ -41,6 +41,7 @@ import org.springblade.transport.pojo.entity.SettlementAdjustmentDetail;
import org.springblade.transport.pojo.vo.SettlementAdjustmentVO;
import org.springblade.transport.service.IFormalSettlementService;
import org.springblade.transport.service.ISettlementAdjustmentService;
import org.springblade.transport.service.ISettlementProjectAdvanceWriteOffService;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@@ -73,6 +74,7 @@ public class SettlementAdjustmentServiceImpl extends BaseServiceImpl<SettlementA
private final PaymentApplicationMapper paymentApplicationMapper;
private final PaymentApplicationSettlementMapper paymentApplicationSettlementMapper;
private final IFormalSettlementService formalSettlementService;
private final ISettlementProjectAdvanceWriteOffService projectAdvanceWriteOffService;
private final KingdeeApBillSyncSupport kingdeeApBillSyncSupport;
private final KingdeeArBillSyncSupport kingdeeArBillSyncSupport;
private final ApplicationEventPublisher eventPublisher;
@@ -382,10 +384,18 @@ public class SettlementAdjustmentServiceImpl extends BaseServiceImpl<SettlementA
detail.setAdjustAmount(detail.getSettlementAmountTax().subtract(money(detail.getOriginalAmount()))); formalDetailMapper.updateById(detail);
}
BigDecimal amount = money(formal.getSettlementAmount()).add(money(adjustment.getAdjustmentAmount()));
// 调整后结算金额不得低于项目预付核销合计,否则已付将超过结算金额且无法拦截
BigDecimal writeOffTotal = this.projectAdvanceWriteOffService.sumBySettlement(
TransportBusiConstant.SETTLEMENT_BILL_FORMAL, formal.getId());
if (money(writeOffTotal).compareTo(money(amount)) > 0) {
throw new ServiceException("调整后结算金额不能低于项目预付核销合计" + money(writeOffTotal) + ",请先调减项目预付核销");
}
formal.setSettlementAmount(amount);
formal.setLocalSettlementAmount(amount.multiply(formal.getExchangeRate() == null ? BigDecimal.ONE : formal.getExchangeRate()));
formal.setRemainingPayableAmount(amount.subtract(money(formal.getPaidAmount())).max(BigDecimal.ZERO));
formalMapper.updateById(formal);
// 重算收付汇总(含核销计入),保持付款状态与剩余可付准确
formalSettlementService.refreshPaymentSummary(formal.getId());
}
/** 涉事事务的自代理:跨方法调用确保 {@link #cloneAndApplyForRepush} 的事务注解生效 */
@@ -14,6 +14,7 @@ import lombok.RequiredArgsConstructor;
import org.springblade.core.log.exception.ServiceException;
import org.springblade.core.tool.utils.Func;
import org.springblade.system.cache.UserCache;
import org.springblade.transport.constant.KingdeeBfiConstant;
import org.springblade.transport.constant.TransportBusiConstant;
import org.springblade.transport.mapper.PaymentApplicationMapper;
import org.springblade.transport.mapper.SettlementProjectAdvanceWriteOffMapper;
@@ -28,8 +29,11 @@ import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
@@ -56,6 +60,7 @@ public class SettlementProjectAdvanceWriteOffServiceImpl implements ISettlementP
.eq(PaymentApplication::getContractId, contractId)
.eq(PaymentApplication::getPaymentType, TransportBusiConstant.PAYMENT_TYPE_PROJECT_ADVANCE)
.eq(PaymentApplication::getApprovalStatus, TransportBusiConstant.APPROVED)
.eq(PaymentApplication::getKingdeeStatus, KingdeeBfiConstant.KINGDEE_STATUS_PAID)
.eq(PaymentApplication::getIsDeleted, 0)
.orderByDesc(PaymentApplication::getCreateTime));
List<ProjectAdvanceWriteOffOptionVO> options = new ArrayList<>();
@@ -121,6 +126,20 @@ public class SettlementProjectAdvanceWriteOffServiceImpl implements ISettlementP
return money(writeOffMapper.sumWriteOffBySettlement(settlementType, settlementId));
}
@Override
public Map<Long, BigDecimal> sumBySettlementIds(String settlementType, Collection<Long> settlementIds) {
if (Func.isEmpty(settlementType) || Func.isEmpty(settlementIds)) {
return Map.of();
}
Map<Long, BigDecimal> result = new HashMap<>();
for (Map<String, Object> row : writeOffMapper.sumWriteOffBySettlementIds(settlementType, settlementIds)) {
if (row.get("settlementId") instanceof Number settlementId && row.get("totalAmount") instanceof BigDecimal totalAmount) {
result.put(settlementId.longValue(), money(totalAmount));
}
}
return result;
}
@Override
@Transactional(rollbackFor = Exception.class)
public void replaceWriteOffs(String settlementType, Long settlementId, Long contractId,
@@ -157,6 +176,9 @@ public class SettlementProjectAdvanceWriteOffServiceImpl implements ISettlementP
if (!TransportBusiConstant.APPROVED.equals(application.getApprovalStatus())) {
throw new ServiceException("仅可选择审核通过的项目预付单");
}
if (!KingdeeBfiConstant.KINGDEE_STATUS_PAID.equals(application.getKingdeeStatus())) {
throw new ServiceException("预付单" + Func.toStr(application.getPaymentNo(), "") + "尚未付款,项目预付须金蝶付款完成后方可核销");
}
if (!Objects.equals(contractId, application.getContractId())) {
throw new ServiceException("项目预付单与结算单合同不一致");
}
@@ -203,6 +225,40 @@ public class SettlementProjectAdvanceWriteOffServiceImpl implements ISettlementP
.eq(SettlementProjectAdvanceWriteOff::getSettlementId, settlementId));
}
@Override
@Transactional(rollbackFor = Exception.class)
public int deleteBySettlementIds(String settlementType, Collection<Long> settlementIds) {
if (Func.isEmpty(settlementType) || Func.isEmpty(settlementIds)) {
return 0;
}
return writeOffMapper.delete(Wrappers.<SettlementProjectAdvanceWriteOff>lambdaQuery()
.eq(SettlementProjectAdvanceWriteOff::getSettlementType, settlementType)
.in(SettlementProjectAdvanceWriteOff::getSettlementId, settlementIds));
}
@Override
@Transactional(rollbackFor = Exception.class)
public List<Long> deleteByPaymentApplicationIds(Collection<Long> paymentApplicationIds) {
if (Func.isEmpty(paymentApplicationIds)) {
return List.of();
}
List<SettlementProjectAdvanceWriteOff> rows = writeOffMapper.selectList(
Wrappers.<SettlementProjectAdvanceWriteOff>lambdaQuery()
.eq(SettlementProjectAdvanceWriteOff::getIsDeleted, 0)
.in(SettlementProjectAdvanceWriteOff::getPaymentApplicationId, paymentApplicationIds));
if (rows.isEmpty()) {
return List.of();
}
writeOffMapper.delete(Wrappers.<SettlementProjectAdvanceWriteOff>lambdaQuery()
.in(SettlementProjectAdvanceWriteOff::getPaymentApplicationId, paymentApplicationIds));
return rows.stream()
.filter(row -> TransportBusiConstant.SETTLEMENT_BILL_FORMAL.equals(row.getSettlementType()))
.map(SettlementProjectAdvanceWriteOff::getSettlementId)
.filter(Objects::nonNull)
.distinct()
.toList();
}
private BigDecimal money(BigDecimal value) {
return value == null ? BigDecimal.ZERO.setScale(2, RoundingMode.HALF_UP)
: value.setScale(2, RoundingMode.HALF_UP);