From 75f33803221c63a93668661d19358eb902911f64 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E5=88=98=E6=B3=89=E4=BD=B3?= <1107186916@qq.com>
Date: Wed, 4 Nov 2026 16:00:23 +0800
Subject: [PATCH 1/4] =?UTF-8?q?feat:=20=E4=BB=98=E6=AC=BE=E8=AE=B0?=
=?UTF-8?q?=E5=BD=95=E8=90=BD=E5=BA=93=E9=87=91=E8=9D=B6=E4=BB=98=E6=AC=BE?=
=?UTF-8?q?=E5=A4=84=E7=90=86=E5=8D=95=E5=86=85=E7=A0=81?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../pojo/entity/PaymentApplicationRecord.java | 2 ++
.../impl/PaymentApplicationServiceImpl.java | 2 ++
...cation_record_kingdee_bill_id_20261104.sql | 21 +++++++++++++++++++
3 files changed, 25 insertions(+)
create mode 100644 doc/sql/transport/lqj/temp/23_blade_payment_application_record_kingdee_bill_id_20261104.sql
diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/PaymentApplicationRecord.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/PaymentApplicationRecord.java
index 1c5c1afe..9d97e3d9 100644
--- a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/PaymentApplicationRecord.java
+++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/PaymentApplicationRecord.java
@@ -40,4 +40,6 @@ public class PaymentApplicationRecord extends TenantEntity {
private String paymentNo;
private String voucherJson;
private String kingdeeBillNo;
+ /** 金蝶付款处理单id(cas_paybill.id,A103-4 回写,应付单 preentry 预付抵扣取数用) */
+ private String kingdeeBillId;
}
diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/PaymentApplicationServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/PaymentApplicationServiceImpl.java
index e9d56acf..5fc32654 100644
--- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/PaymentApplicationServiceImpl.java
+++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/PaymentApplicationServiceImpl.java
@@ -1146,6 +1146,8 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl
Date: Wed, 4 Nov 2026 16:00:28 +0800
Subject: [PATCH 2/4] =?UTF-8?q?feat:=20=E8=B4=A2=E5=8A=A1=E5=BA=94?=
=?UTF-8?q?=E4=BB=98=E5=8D=95=E6=8E=A8=E9=80=81=E6=90=BA=E5=B8=A6=E9=A2=84?=
=?UTF-8?q?=E4=BB=98=E4=BF=A1=E6=81=AF=E5=88=86=E5=BD=95=E5=B9=B6=E6=8B=A6?=
=?UTF-8?q?=E6=88=AA=E6=9C=AA=E5=9B=9E=E5=86=99=E9=A2=84=E4=BB=98?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../support/KingdeeApBillSyncSupport.java | 166 +++++++++++++++++-
1 file changed, 163 insertions(+), 3 deletions(-)
diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/support/KingdeeApBillSyncSupport.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/support/KingdeeApBillSyncSupport.java
index 12a1060f..21be0c4f 100644
--- a/blade-service/blade-transport/src/main/java/org/springblade/transport/support/KingdeeApBillSyncSupport.java
+++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/support/KingdeeApBillSyncSupport.java
@@ -39,26 +39,39 @@ import org.springblade.system.pojo.entity.FeeItem;
import org.springblade.thirdparty.bfi.pojo.dto.BfiApBillQueryDTO;
import org.springblade.thirdparty.bfi.pojo.dto.BfiApBillSaveDTO;
import org.springblade.thirdparty.bfi.pojo.dto.BfiApBillSaveEntryDTO;
+import org.springblade.thirdparty.bfi.pojo.dto.BfiApBillSavePreEntryDTO;
import org.springblade.thirdparty.bfi.pojo.vo.BfiApBillVO;
import org.springblade.thirdparty.bfi.pojo.vo.BfiBillSaveResultItemVO;
import org.springblade.thirdparty.bfi.pojo.vo.BfiBillSaveResultVO;
import org.springblade.thirdparty.bfi.service.IBfiService;
import org.springblade.thirdparty.bfi.util.BfiUtil;
+import org.springblade.thirdparty.bfi.constant.BfiConstant;
import org.springblade.transport.config.BfiProperties;
import org.springblade.transport.constant.KingdeeBfiConstant;
+import org.springblade.transport.constant.TransportBusiConstant;
import org.springblade.transport.mapper.ContractManageMapper;
import org.springblade.transport.mapper.CustomerArchiveMapper;
import org.springblade.transport.mapper.FormalSettlementDetailFeeMapper;
import org.springblade.transport.mapper.FormalSettlementMapper;
+import org.springblade.transport.mapper.FormalSettlementSourceMapper;
import org.springblade.transport.mapper.FormalSettlementSummaryFeeMapper;
+import org.springblade.transport.mapper.PaymentApplicationMapper;
+import org.springblade.transport.mapper.PaymentApplicationRecordMapper;
+import org.springblade.transport.mapper.PreSettlementAdvanceMapper;
import org.springblade.transport.mapper.ProjectApplyMapper;
+import org.springblade.transport.mapper.SettlementProjectAdvanceWriteOffMapper;
import org.springblade.transport.pojo.entity.ContractManage;
import org.springblade.transport.pojo.entity.CustomerArchive;
import org.springblade.transport.pojo.entity.FormalSettlement;
import org.springblade.transport.pojo.entity.FormalSettlementDetail;
import org.springblade.transport.pojo.entity.FormalSettlementDetailFee;
+import org.springblade.transport.pojo.entity.FormalSettlementSource;
import org.springblade.transport.pojo.entity.FormalSettlementSummaryFee;
+import org.springblade.transport.pojo.entity.PaymentApplication;
+import org.springblade.transport.pojo.entity.PaymentApplicationRecord;
+import org.springblade.transport.pojo.entity.PreSettlementAdvance;
import org.springblade.transport.pojo.entity.ProjectApply;
+import org.springblade.transport.pojo.entity.SettlementProjectAdvanceWriteOff;
import org.springblade.transport.service.KingdeeSyncContext;
import org.springframework.stereotype.Component;
@@ -73,6 +86,7 @@ import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.function.Consumer;
+import java.util.function.Function;
import java.util.stream.Collectors;
/**
@@ -100,6 +114,8 @@ public class KingdeeApBillSyncSupport {
private static final int PAY_RESULT_PAGE_SIZE = 200;
/** 结算明细行(运费)计费方案匹配费用项(运输类无税率残差组标识用) */
private static final String FREIGHT_FEE_ITEM = "运输费";
+ /** 预付抵扣拦截提示最多列出的预付单笔数(超出部分汇总"等N笔",kingdee_fail_reason 列长 500 截断兜底) */
+ private static final int PREPAY_BLOCK_REASON_LIMIT = 3;
/** 费用大类编码(业务字典 fee_category 键值):运输 */
public static final String FEE_CATEGORY_FREIGHT = "A01";
/** 费用大类编码(业务字典 fee_category 键值):装卸 */
@@ -138,7 +154,12 @@ public class KingdeeApBillSyncSupport {
private final FormalSettlementDetailFeeMapper formalSettlementDetailFeeMapper;
private final FormalSettlementSummaryFeeMapper formalSettlementSummaryFeeMapper;
private final FormalSettlementMapper formalSettlementMapper;
+ private final FormalSettlementSourceMapper formalSettlementSourceMapper;
+ private final PaymentApplicationMapper paymentApplicationMapper;
+ private final PaymentApplicationRecordMapper paymentApplicationRecordMapper;
+ private final PreSettlementAdvanceMapper preSettlementAdvanceMapper;
private final ProjectApplyMapper projectApplyMapper;
+ private final SettlementProjectAdvanceWriteOffMapper projectAdvanceWriteOffMapper;
private final ISysClient sysClient;
private final BfiProperties bfiProperties;
@@ -381,11 +402,16 @@ public class KingdeeApBillSyncSupport {
// 付款计划 planentity 暂不传(联调待确认后启用):启用后送正式结算单下绑定、单据状态为审核通过(approved)
// 的付款信息,每条付款信息一行,awrp_entry_code_p 取付款单号(A103-1 下推 awrp_finapbill_entryid 勾稽此值);
// 当前不传由金蝶按明细自动汇总生成,A103-1 推送前经 A102-4 反查金蝶生成的 awrp_entry_code_p
- // TODO 待业务确认:存在预付款抵扣时是否需传预付信息 preentry(y_billtype/y_billid/y_settleamt),本期不传、后续迭代
+ // 预付信息分录 preentry:存在预付款抵扣(进度预付/项目预付核销)时按金蝶付款处理单逐笔送出,
+ // 任一笔无法定位金蝶付款处理单内码(未付款/内码未回写/实付不足)时拦截推送(见 resolvePreEntries)
+ List preEntries = this.resolvePreEntries(settlement);
+ if (!preEntries.isEmpty()) {
+ param.setPreentry(preEntries);
+ }
// 推送入参摘要(联调排查:防重键、分录条数、页面维护的汇率口径)
- log.info("推送金蝶财务应付单:正式结算单[{}],防重键[{}],明细分录{}条,汇率日期[{}],汇率[{}]",
- subject, apBillSrcCode, detailentry.size(), param.getExratedate(), param.getExchangerate());
+ log.info("推送金蝶财务应付单:正式结算单[{}],防重键[{}],明细分录{}条,预付信息{}条,汇率日期[{}],汇率[{}]",
+ subject, apBillSrcCode, detailentry.size(), preEntries.size(), param.getExratedate(), param.getExchangerate());
BfiBillSaveResultVO saveResult = bfiService.saveApBill(param);
// 应付单id不落库,后续如需可经 A102-4 按防重键(awrp_src_sys + awrp_src_code)查询
@@ -397,6 +423,140 @@ public class KingdeeApBillSyncSupport {
return new BfiApBillPushOutcome(savedItem.getNumber(), savedItem.getId() == null ? null : String.valueOf(savedItem.getId()), false);
}
+ // ==================== 预付信息分录(A102-2 preentry,预付抵扣) ====================
+
+ /**
+ * 解析正式结算单预付信息分录(A102-2 preentry):每条对应一张金蝶付款处理单(cas_paybill),
+ * 项目预付取核销行核销金额、进度预付取预付记录已付金额,按付款处理单实付占比分摊后同 y_billid 合并求和。
+ * 任一笔应抵扣预付无法定位金蝶付款处理单(付款申请不存在/未付款/内码未回写/实付不足)时抛异常拦截应付单推送,
+ * 失败落 kingdee_fail_reason,付款结果回写后由重推任务自动续推
+ *
+ * @param settlement 正式结算单(应付)
+ * @return 预付信息分录列表(按 y_billid 合并后),无预付抵扣返回空列表
+ */
+ private List resolvePreEntries(FormalSettlement settlement) {
+ // 应抵扣需求按底层付款申请归集:付款申请id -> (本次抵扣金额, 来源描述)
+ Map demandAmounts = new LinkedHashMap<>();
+ Map demandSources = new LinkedHashMap<>();
+ // 项目预付:核销行核销金额即本次抵扣金额(核销保存校验已要求预付金蝶已付款)
+ List writeOffs = this.projectAdvanceWriteOffMapper.selectList(
+ Wrappers.lambdaQuery()
+ .eq(SettlementProjectAdvanceWriteOff::getSettlementType, TransportBusiConstant.SETTLEMENT_BILL_FORMAL)
+ .eq(SettlementProjectAdvanceWriteOff::getSettlementId, settlement.getId())
+ .eq(SettlementProjectAdvanceWriteOff::getIsDeleted, 0));
+ for (SettlementProjectAdvanceWriteOff writeOff : writeOffs) {
+ if (writeOff.getPaymentApplicationId() == null || this.money(writeOff.getWriteOffAmount()).signum() <= 0) {
+ continue;
+ }
+ demandAmounts.merge(writeOff.getPaymentApplicationId(), this.money(writeOff.getWriteOffAmount()), BigDecimal::add);
+ demandSources.putIfAbsent(writeOff.getPaymentApplicationId(), "项目预付[" + Func.toStr(writeOff.getPaymentNo()) + "]");
+ }
+ // 进度预付:来源预结算的预付记录已付金额即本次抵扣金额(syncOnePreSettlementAdvance 已按预结算关联行分摊)
+ List sources = this.formalSettlementSourceMapper.selectList(
+ Wrappers.lambdaQuery()
+ .eq(FormalSettlementSource::getFormalSettlementId, settlement.getId())
+ .eq(FormalSettlementSource::getIsDeleted, 0));
+ List preSettlementIds = sources.stream().map(FormalSettlementSource::getPreSettlementId)
+ .filter(Objects::nonNull).distinct().toList();
+ if (!preSettlementIds.isEmpty()) {
+ List advances = this.preSettlementAdvanceMapper.selectList(
+ Wrappers.lambdaQuery()
+ .in(PreSettlementAdvance::getPreSettlementId, preSettlementIds)
+ .eq(PreSettlementAdvance::getIsDeleted, 0)
+ .ne(PreSettlementAdvance::getBillStatus, TransportBusiConstant.VOIDED));
+ // 预付记录 advanceNo 即付款申请单号,按单号批量定位底层付款申请(避免循环内逐单查询)
+ List advanceNos = advances.stream().map(PreSettlementAdvance::getAdvanceNo)
+ .filter(Func::isNotBlank).distinct().toList();
+ Map applicationsByNo = advanceNos.isEmpty() ? Map.of()
+ : this.paymentApplicationMapper.selectList(Wrappers.lambdaQuery()
+ .in(PaymentApplication::getPaymentNo, advanceNos)
+ .eq(PaymentApplication::getIsDeleted, 0))
+ .stream().collect(Collectors.toMap(PaymentApplication::getPaymentNo, Function.identity(),
+ (first, second) -> first));
+ for (PreSettlementAdvance advance : advances) {
+ if (this.money(advance.getPaidAmount()).signum() <= 0) {
+ continue;
+ }
+ PaymentApplication application = applicationsByNo.get(advance.getAdvanceNo());
+ if (application == null) {
+ throw new ServiceException("推送金蝶财务应付单失败:预付单[" + Func.toStr(advance.getAdvanceNo())
+ + "]不存在付款申请,无法解析预付信息");
+ }
+ demandAmounts.merge(application.getId(), this.money(advance.getPaidAmount()), BigDecimal::add);
+ demandSources.putIfAbsent(application.getId(), "进度预付[" + Func.toStr(advance.getAdvanceNo()) + "]");
+ }
+ }
+ if (demandAmounts.isEmpty()) {
+ return List.of();
+ }
+ // 批量取付款申请与付款处理记录(仅保留带回金蝶付款处理单内码的记录),逐笔校验并分摊
+ Map applications = this.paymentApplicationMapper.selectBatchIds(demandAmounts.keySet()).stream()
+ .collect(Collectors.toMap(PaymentApplication::getId, Function.identity(), (first, second) -> first));
+ Map> recordsByApplicationId = this.paymentApplicationRecordMapper.selectList(
+ Wrappers.lambdaQuery()
+ .in(PaymentApplicationRecord::getPaymentApplicationId, demandAmounts.keySet())
+ .eq(PaymentApplicationRecord::getIsDeleted, 0))
+ .stream().filter(record -> Func.isNotBlank(record.getKingdeeBillId()))
+ .collect(Collectors.groupingBy(PaymentApplicationRecord::getPaymentApplicationId));
+ Map settleAmountsByBillId = new LinkedHashMap<>();
+ Map sourcesByBillId = new LinkedHashMap<>();
+ List blockReasons = new ArrayList<>();
+ demandAmounts.forEach((applicationId, demandAmount) -> {
+ String source = demandSources.get(applicationId);
+ PaymentApplication application = applications.get(applicationId);
+ if (application == null) {
+ blockReasons.add(source + "不存在付款申请");
+ return;
+ }
+ List records = recordsByApplicationId.getOrDefault(applicationId, List.of());
+ if (records.isEmpty()) {
+ blockReasons.add(source + (KingdeeBfiConstant.KINGDEE_STATUS_PAID.equals(application.getKingdeeStatus())
+ ? "金蝶付款处理单内码未回写,请先在付款管理执行[同步付款结果]后再推送应付单"
+ : "尚未付款完成,预付须金蝶付款完成后方可抵扣"));
+ return;
+ }
+ BigDecimal recordTotal = records.stream().map(record -> this.money(record.getPaidAmount()))
+ .reduce(BigDecimal.ZERO, BigDecimal::add);
+ if (recordTotal.compareTo(demandAmount) < 0) {
+ blockReasons.add(source + "付款处理单实付金额不足");
+ return;
+ }
+ // 按付款处理单实付占比分摊(末条取剩余补差保证合计精确),同 y_billid 合并求和
+ BigDecimal remaining = demandAmount;
+ for (int index = 0; index < records.size(); index++) {
+ PaymentApplicationRecord record = records.get(index);
+ BigDecimal settleAmount = index == records.size() - 1 ? remaining
+ : demandAmount.multiply(this.money(record.getPaidAmount()))
+ .divide(recordTotal, 2, RoundingMode.HALF_UP)
+ .min(remaining);
+ settleAmount = settleAmount.setScale(2, RoundingMode.HALF_UP);
+ if (settleAmount.signum() <= 0) {
+ continue;
+ }
+ remaining = remaining.subtract(settleAmount);
+ settleAmountsByBillId.merge(record.getKingdeeBillId(), settleAmount, BigDecimal::add);
+ sourcesByBillId.putIfAbsent(record.getKingdeeBillId(), source);
+ }
+ });
+ if (!blockReasons.isEmpty()) {
+ String summary = blockReasons.size() > PREPAY_BLOCK_REASON_LIMIT
+ ? String.join(";", blockReasons.subList(0, PREPAY_BLOCK_REASON_LIMIT)) + ";等" + blockReasons.size() + "笔预付无法抵扣"
+ : String.join(";", blockReasons);
+ throw new ServiceException("推送金蝶财务应付单失败:" + summary);
+ }
+ List preEntries = new ArrayList<>();
+ settleAmountsByBillId.forEach((billId, settleAmount) -> {
+ BfiApBillSavePreEntryDTO preEntry = new BfiApBillSavePreEntryDTO();
+ preEntry.setYBilltype(BfiConstant.PREPAY_BILL_TYPE_PAY_BILL);
+ preEntry.setYBillid(billId);
+ preEntry.setYSettleamt(settleAmount);
+ preEntries.add(preEntry);
+ log.info("应付单预付信息分录: 正式结算单[{}], 来源[{}], y_billid={}, y_settleamt={}",
+ settlement.getFormalSettlementNo(), sourcesByBillId.get(billId), billId, settleAmount);
+ });
+ return preEntries;
+ }
+
// ==================== 结算明细分录解析(结算明细行 + 结算合计费用项) ====================
/**
From 01cc9f7f37c9a71a13f93be7ab9eeceee95cdd7f Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E5=88=98=E6=B3=89=E4=BD=B3?= <1107186916@qq.com>
Date: Wed, 4 Nov 2026 16:01:06 +0800
Subject: [PATCH 3/4] =?UTF-8?q?docs:=20=E6=96=B0=E5=A2=9E=E8=B4=A2?=
=?UTF-8?q?=E5=8A=A1=E5=BA=94=E4=BB=98=E5=8D=95preentry=E9=A2=84=E4=BB=98?=
=?UTF-8?q?=E4=BF=A1=E6=81=AF=E6=8E=A8=E9=80=81=E9=87=91=E8=9D=B6=E8=AE=BE?=
=?UTF-8?q?=E8=AE=A1=E6=96=87=E6=A1=A3?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
...金蝶设计(A102-2预付抵扣-y_billid落库-拦截规则与分摊).md | 195 ++++++++++++++++++
1 file changed, 195 insertions(+)
create mode 100644 doc/docs/业务流程/2026-11-04-财务应付单preentry预付信息推送金蝶设计(A102-2预付抵扣-y_billid落库-拦截规则与分摊).md
diff --git a/doc/docs/业务流程/2026-11-04-财务应付单preentry预付信息推送金蝶设计(A102-2预付抵扣-y_billid落库-拦截规则与分摊).md b/doc/docs/业务流程/2026-11-04-财务应付单preentry预付信息推送金蝶设计(A102-2预付抵扣-y_billid落库-拦截规则与分摊).md
new file mode 100644
index 00000000..8783f30b
--- /dev/null
+++ b/doc/docs/业务流程/2026-11-04-财务应付单preentry预付信息推送金蝶设计(A102-2预付抵扣-y_billid落库-拦截规则与分摊).md
@@ -0,0 +1,195 @@
+# 财务应付单预付信息 preentry 推送金蝶设计
+
+> 范围:正式结算单「同步金蝶」推送财务应付单(A102-2)时,携带预付信息 `preentry`(`y_billtype / y_billid / y_settleamt`)的取数口径、每笔计算规则、拦截规则与落地改动。
+> 背景:`KingdeeApBillSyncSupport.pushApBill` 现有 TODO"存在预付款抵扣时是否需传预付信息 preentry,本期不传、后续迭代",本文为该迭代的实现设计。
+> 关联文档:`2026-10-18-预付完整流程梳理(进度预付-项目预付-正式结算单-付款-金蝶回写).md`、`2026-09-16-付款管理正式付款同步金蝶(财务应付单+付款申请下推)技术文档.md`、`2026-09-17-付款管理金蝶付款结果定时回写(A103-2付款申请单查询+A103-4付款处理查询)技术文档.md`。
+> 设计日期:2026-11-04。
+
+---
+
+## 一、字段语义与结论
+
+`preentry` 是 A102-2 财务应付单的预付信息分录,**支持多条**,用于登记本张应付单抵扣了哪些已付款的预付单。每条 `preentry` 对应**一张金蝶付款处理单(`cas_paybill`)**:
+
+```json
+"preentry": [
+ { "y_billtype": "cas_paybill", "y_billid": "付款处理单1 id", "y_settleamt": 10000.00 },
+ { "y_billtype": "cas_paybill", "y_billid": "付款处理单2 id", "y_settleamt": 5000.00 }
+]
+```
+
+### 字段取值
+
+| 字段 | 必填 | 取值 | 说明 |
+| --- | --- | --- | --- |
+| `y_billtype` | 是 | 固定 `cas_paybill`(`BfiConstant.PREPAY_BILL_TYPE_PAY_BILL`) | TMS 预付均经 A103-3 付款申请 → 金蝶付款处理,不产生期初预付单(`ap_paidbill` 不使用) |
+| `y_billid` | 是 | 金蝶**付款处理单** `id`(A103-4 `BfiPayBillVO.id`) | **不能**用 `blade_payment_application.kingdee_bill_id`(那是 A103-2 返回的付款申请单 id,类型不对) |
+| `y_settleamt` | 否 | 本次应付单实际抵扣该预付的金额,2 位小数 | 进度预付取 `PreSettlementAdvance.paid_amount`,项目预付取核销行 `write_off_amount` |
+
+### 两个易混淆字段(不在本次改造范围)
+
+| 字段 | 取值 | 现状 |
+| --- | --- | --- |
+| `awrp_src_type`(A102-2 应付单头) | `TMS财务应付单` | `KingdeeBfiConstant.BFI_AP_BILL_SRC_TYPE`,保持不变,**不要**改成 `0KzXs` |
+| `awrp_src_type`(A103-1/A103-3 付款申请头/分录) | 固定 `0KzXs` | Nacos `thirdParty.bfi.payApply.srcType` 配置,已是正确值,无需改 |
+
+---
+
+## 二、现状核查
+
+### 1. preentry 目前未送
+
+`KingdeeApBillSyncSupport.pushApBill`(A102-2 保存)明细分录之后保留 TODO,`setPreentry` 全项目无调用;DTO `BfiApBillSavePreEntryDTO`(`y_billtype / y_billid / y_settleamt`)已就绪,`@JsonInclude(NON_NULL)`——**为空时保持 `null`,不要设置空数组**(避免报文送 `preentry: []`)。
+
+### 2. y_billid 需要的内码未落库
+
+| 数据 | 实际值 | 可否作 `y_billid` |
+| --- | --- | --- |
+| `blade_payment_application.kingdee_bill_id` | A103-2 返回的付款申请单 id(`ap_payapply`) | ❌ 类型不对 |
+| `blade_payment_application_record.kingdee_bill_no` | A103-4 返回的付款处理单号(`cas_paybill.billno`) | ❌ 只有单号 |
+| `blade_payment_application_record.voucher_json` | A103-4 付款处理单整单 JSON,含 `id` | ⚠️ 仅 JSON 内有,无独立字段 |
+| `blade_pre_settlement_advance.kingdee_advance_no` | 付款申请单号 | ❌ |
+| `blade_settlement_project_advance_write_off.kingdee_bill_no` | 付款申请单号 | ❌ |
+
+**结论:必须先补存 `cas_paybill.id`**,否则 preentry 无值可送。
+
+---
+
+## 三、每笔 preentry 的计算规则
+
+### 粒度与合并
+
+- 一条 `preentry` = 一张金蝶付款处理单(按 `y_billid` 唯一)。
+- 同一 `y_billid` 被多行业务数据引用(如一张付款申请跨多个预结算分摊后落到同一张付款处理单)时,**按 `y_billid` 汇总金额只出一条**,避免向金蝶送重复键。
+
+### 项目预付
+
+数据源 `blade_settlement_project_advance_write_off`(本身就是"本结算单核销了某张项目预付多少"):
+
+```
+遍历核销行(settlement_type='formal' AND settlement_id=本单id)
+ → payment_application_id → 付款申请
+ → 付款处理记录(blade_payment_application_record,kingdee_bill_id 非空)
+ → 应抵扣金额 = write_off_amount,按付款处理单实付分摊
+ → 每条记录生成一条 preentry
+```
+
+### 进度预付
+
+数据源 `blade_formal_settlement_source` + `blade_pre_settlement_advance`(`syncOnePreSettlementAdvance` 已把付款申请按预结算关联行拆分,`paid_amount` 即本预结算分摊额):
+
+```
+遍历正式结算来源(formal_settlement_id=本单id)
+ → pre_settlement_id
+ → 预付记录(pre_settlement_id= ? AND paid_amount>0 AND is_deleted=0)
+ → advance_no(=付款申请单号)→ 付款申请
+ → 付款处理记录(kingdee_bill_id 非空)
+ → 应抵扣金额 = advance.paid_amount,按付款处理单实付分摊
+ → 每条记录生成一条 preentry
+```
+
+### 多付款处理单分摊(部分付款兜底)
+
+一张付款申请可能对应多张付款处理单(A103-4 多条回写):
+
+1. 按各记录 `paid_amount` 占比分摊应抵扣金额;
+2. 金额 `setScale(2, HALF_UP)`;
+3. 最后一条取"总额 − 已分摊"补差,保证合计不差分;
+4. 单条 `y_settleamt` 不超过该记录 `paid_amount`;
+5. 应抵扣金额超过付款处理单实付合计时封顶为实付合计(正常业务不会出现,防御性处理)。
+
+---
+
+## 四、拦截规则(已确认:拦截应付单推送)
+
+拦截点在 **A102-4 幂等预检(确认金蝶无本单应付单)之后、`saveApBill` 之前**;预检命中"已存在"直接幂等返回,不做校验(已推单据无法补 preentry)。
+
+任一笔应抵扣预付不满足以下条件即抛 `ServiceException`,整张应付单不推:
+
+| # | 校验 | 不满足提示 |
+| --- | --- | --- |
+| 1 | 底层付款申请存在 | 预付单[XXX]不存在付款申请 |
+| 2 | 付款申请已付款(`kingdee_status='paid'` / 已有付款处理记录) | 预付单[XXX]尚未付款完成,预付须金蝶付款完成后方可抵扣 |
+| 3 | `kingdee_bill_id` 非空(至少一条付款处理记录带回内码) | 预付单[XXX]金蝶付款处理单内码未回写,请先在付款管理执行【同步付款结果】后再推送应付单 |
+| 4 | 付款处理单实付合计 ≥ 本次抵扣金额 | 预付单[XXX]付款处理单实付金额不足 |
+
+提示信息**汇总所有不满足的预付单**,最多列 3 笔 + "等 N 笔",控制在 `kingdee_fail_reason` 的 500 字符内。
+
+失败后的自动流转(复用现有机制,无需新增代码):
+
+```
+doSyncKingdee 抛 ServiceException
+ → syncKingdee 捕获 → markKingdeeSyncFailed(failed + 失败原因留档)
+ → retryKingdeeFailed 定时重推
+ → A103-4 回写 kingdee_bill_id 后,下一轮重推自动成功
+```
+
+---
+
+## 五、落地改动清单
+
+### 1. 数据表与实体
+
+- `blade_payment_application_record` 增加 `kingdee_bill_id varchar(64) DEFAULT NULL COMMENT '金蝶付款处理单id(cas_paybill.id,A103-4回写)'`;
+- `PaymentApplicationRecord` 增加 `kingdeeBillId` 字段;
+- SQL 放 `doc/sql/transport/lqj/temp/`,**可重复执行**,含存量回填:
+
+```sql
+ALTER TABLE `blade_payment_application_record`
+ ADD COLUMN `kingdee_bill_id` varchar(64) DEFAULT NULL COMMENT '金蝶付款处理单id(cas_paybill.id,A103-4回写)' AFTER `kingdee_bill_no`;
+
+UPDATE `blade_payment_application_record`
+SET `kingdee_bill_id` = JSON_UNQUOTE(JSON_EXTRACT(`voucher_json`, '$.id'))
+WHERE `kingdee_bill_id` IS NULL AND `voucher_json` IS NOT NULL;
+```
+
+### 2. A103-4 回写
+
+`PaymentApplicationServiceImpl.applyPayBillRecord` 补一行:
+
+```java
+record.setKingdeeBillId(payBill.getId() == null ? null : String.valueOf(payBill.getId()));
+```
+
+### 3. preentry 解析(KingdeeApBillSyncSupport)
+
+- `KingdeeApBillSyncSupport` 追加注入 5 个 Mapper:`FormalSettlementSourceMapper`、`PreSettlementAdvanceMapper`、`SettlementProjectAdvanceWriteOffMapper`、`PaymentApplicationMapper`、`PaymentApplicationRecordMapper`;
+- 新增私有方法 `resolvePreEntries(FormalSettlement settlement)`:按第三章两条链路取数 → 逐笔校验(第四章拦截规则)→ 分摊 → 按 `y_billid` 合并,返回 `List`;
+- `pushApBill` 中 A102-4 预检通过后、保存前调用:
+
+```java
+List preEntries = this.resolvePreEntries(settlement);
+if (!preEntries.isEmpty()) {
+ param.setPreentry(preEntries);
+}
+```
+
+同时删除原 TODO 注释。
+
+### 4. 日志
+
+每笔输出:来源(进度预付/项目预付)、`y_billid`、`y_settleamt`;跳过/封顶/拦截输出原因。日志禁用中文标点。
+
+---
+
+## 六、边界与风险
+
+| # | 边界 | 处理 |
+| --- | --- | --- |
+| 1 | 历史已推送的应付单 | A102-4 预检命中幂等返回,**不会补 preentry**(现有设计,不处理) |
+| 2 | 预付未付款(synced/paying)但有抵扣金额 | 按拦截规则 #2 阻断,等付款完成后重推自动成功 |
+| 3 | 进度预付部分付款 | 按已付金额(`paid_amount`)送,未付部分不抵扣;如业务后续要求"全额付清才可推应付单",在拦截规则追加 `paid_amount >= applied_amount` |
+| 4 | 存量付款记录无 `kingdee_bill_id` 且 `voucher_json` 缺失 | 拦截规则 #3 阻断,走付款管理【同步付款结果】重新回写 |
+| 5 | preentry 合计超应付单金额 | 正常业务不会出现(核销/预付均封顶于结算金额);防御性不额外校验,依赖金蝶侧报错 |
+| 6 | `ap_paidbill`(期初预付单) | TMS 不产生,不在本期范围;如有手工核销需求由金蝶侧人工处理 |
+
+---
+
+## 七、验证要点
+
+1. 无预付抵扣的应付单:报文无 `preentry` 字段(非空数组)。
+2. 单笔项目预付核销 + 已付款:一条 preentry,`y_settleamt` = 核销额。
+3. 进度预付跨多个预结算分摊:同 `y_billid` 合并为一条,金额为各行之和。
+4. 预付未付款时推应付单:拦截、落 `failed` 与原因;付款完成后定时重推成功且带 preentry。
+5. 部分付款(多条付款处理记录):分摊合计 = 应抵扣金额,末条补差无分差。
+6. 存量数据回填:SQL 重复执行幂等,回填后无需重新同步付款结果。
From d69b293dd3c712a22ed5025a3a5b86c519aafac5 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E5=88=98=E6=B3=89=E4=BD=B3?= <1107186916@qq.com>
Date: Wed, 4 Nov 2026 16:17:00 +0800
Subject: [PATCH 4/4] =?UTF-8?q?refactor:=20=E8=A7=84=E8=8C=83=E9=87=91?=
=?UTF-8?q?=E8=9D=B6=E5=BA=94=E4=BB=98=E5=8D=95=E5=90=8C=E6=AD=A5=E6=97=A5?=
=?UTF-8?q?=E5=BF=97=E8=8B=B1=E6=96=87=E6=A0=87=E7=82=B9=E5=B9=B6=E8=A1=A5?=
=?UTF-8?q?=E5=85=85=E9=A2=84=E4=BB=98=E4=BF=A1=E6=81=AF=E5=85=B3=E9=94=AE?=
=?UTF-8?q?=E6=97=A5=E5=BF=97?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../support/KingdeeApBillSyncSupport.java | 40 ++++++++++++++-----
1 file changed, 30 insertions(+), 10 deletions(-)
diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/support/KingdeeApBillSyncSupport.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/support/KingdeeApBillSyncSupport.java
index 21be0c4f..c967fad6 100644
--- a/blade-service/blade-transport/src/main/java/org/springblade/transport/support/KingdeeApBillSyncSupport.java
+++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/support/KingdeeApBillSyncSupport.java
@@ -100,6 +100,10 @@ import java.util.stream.Collectors;
* + 结算合计费用项(手动添加的每行一条);
* 物料编码按装卸大类/项目业务模式/运单运输方式映射,编码维护在 Nacos 配置 thirdparty.bfi.material.*(见 {@link #resolveMaterialNumber});
* 运输类分录数量取运输量(按税率拆行时每行都取运输量)、装卸类默认 1;单价按结算金额/数量反算。
+ * 预付信息分录(preentry,见 {@link #resolvePreEntries}):存在预付款抵扣(进度预付/项目预付核销)时,
+ * 按金蝶付款处理单(cas_paybill)逐笔送出 y_billtype/y_billid/y_settleamt,同 y_billid 合并求和;
+ * 任一笔应抵扣预付无法定位金蝶付款处理单内码(未付款/内码未回写/实付不足)时抛异常拦截应付单推送,
+ * 失败落 kingdee_fail_reason,付款结果回写后由重推任务自动续推。
*
* @author Chill
*/
@@ -283,7 +287,9 @@ public class KingdeeApBillSyncSupport {
* 幂等跳过保存并返回已有单据编号(说明上次同步已保存成功、在后续环节中断,应续推而非拦截报错)。
* 明细分录取页面结算明细(每行按 货物→费用大类→税率 拆分)+ 结算合计费用项(每行一条),结算金额逐行直送;
* 汇率日期/汇率取结算单页面维护值;付款计划 planentity 暂不传(启用方案见方法内注释),
- * 当前由金蝶按明细自动汇总生成
+ * 当前由金蝶按明细自动汇总生成;
+ * 预付信息分录 preentry 仅在真正新建应付单时解析(幂等跳过路径不做预付校验),
+ * 每条对应一张金蝶付款处理单(cas_paybill),取数口径见 {@link #resolvePreEntries}
*
* @param apBillSrcCode 财务应付单防重键(正式结算单号_结算明细单据号拼接)
* @param settlement 正式结算单(合同/汇率日期/汇率取数来源)
@@ -299,7 +305,7 @@ public class KingdeeApBillSyncSupport {
String existBillNo = context.apBillNoCache().computeIfAbsent(apBillSrcCode, code ->
queryApBillExistNos(List.of(code)).getOrDefault(code, ""));
if (Func.isNotEmpty(existBillNo)) {
- log.info("正式结算单[{}]金蝶财务应付单已生成(单据编号[{}]),幂等跳过 A102-2 保存", subject, existBillNo);
+ log.info("正式结算单[{}]金蝶财务应付单已生成(单据编号[{}]), 幂等跳过 A102-2 保存", subject, existBillNo);
return new BfiApBillPushOutcome(existBillNo, null, true);
}
ContractManage contract = requireContract(settlement.getContractId(), context);
@@ -410,14 +416,14 @@ public class KingdeeApBillSyncSupport {
}
// 推送入参摘要(联调排查:防重键、分录条数、页面维护的汇率口径)
- log.info("推送金蝶财务应付单:正式结算单[{}],防重键[{}],明细分录{}条,预付信息{}条,汇率日期[{}],汇率[{}]",
+ log.info("推送金蝶财务应付单: 正式结算单[{}], 防重键[{}], 明细分录{}条, 预付信息{}条, 汇率日期[{}], 汇率[{}]",
subject, apBillSrcCode, detailentry.size(), preEntries.size(), param.getExratedate(), param.getExchangerate());
BfiBillSaveResultVO saveResult = bfiService.saveApBill(param);
// 应付单id不落库,后续如需可经 A102-4 按防重键(awrp_src_sys + awrp_src_code)查询
requireBfiSaveSuccess(saveResult, "推送金蝶财务应付单");
String billNo = saveResult.getResult().get(0).getNumber();
- log.info("金蝶财务应付单保存成功:正式结算单[{}],单据编号[{}],明细分录{}条", subject, billNo, detailentry.size());
+ log.info("金蝶财务应付单保存成功: 正式结算单[{}], 单据编号[{}], 明细分录{}条", subject, billNo, detailentry.size());
// 星瀚应付单内码随结果返回(A902 附件上传定位用)
BfiBillSaveResultItemVO savedItem = saveResult.getResult().get(0);
return new BfiApBillPushOutcome(savedItem.getNumber(), savedItem.getId() == null ? null : String.valueOf(savedItem.getId()), false);
@@ -487,8 +493,14 @@ public class KingdeeApBillSyncSupport {
}
}
if (demandAmounts.isEmpty()) {
+ // 无预付抵扣(无核销行且预付记录已付金额均为0):不送 preentry,保持报文字段缺省
+ log.info("应付单预付信息归集: 正式结算单[{}], 无预付抵扣, 不送预付信息分录", settlement.getFormalSettlementNo());
return List.of();
}
+ // 预付抵扣归集摘要(联调排查: 项目预付按核销行、进度预付按预付记录已付金额归集到底层付款申请)
+ log.info("应付单预付信息归集: 正式结算单[{}], 应抵扣付款申请{}笔, 合计金额{}",
+ settlement.getFormalSettlementNo(), demandAmounts.size(),
+ demandAmounts.values().stream().reduce(BigDecimal.ZERO, BigDecimal::add));
// 批量取付款申请与付款处理记录(仅保留带回金蝶付款处理单内码的记录),逐笔校验并分摊
Map applications = this.paymentApplicationMapper.selectBatchIds(demandAmounts.keySet()).stream()
.collect(Collectors.toMap(PaymentApplication::getId, Function.identity(), (first, second) -> first));
@@ -542,6 +554,9 @@ public class KingdeeApBillSyncSupport {
String summary = blockReasons.size() > PREPAY_BLOCK_REASON_LIMIT
? String.join(";", blockReasons.subList(0, PREPAY_BLOCK_REASON_LIMIT)) + ";等" + blockReasons.size() + "笔预付无法抵扣"
: String.join(";", blockReasons);
+ // 拦截关键日志:原因落盘供排查(异常文案同步落 kingdee_fail_reason,付款结果回写后由重推任务自动续推)
+ log.warn("应付单预付信息校验未通过, 拦截应付单推送: 正式结算单[{}], 应抵扣付款申请{}笔, 原因: {}",
+ settlement.getFormalSettlementNo(), demandAmounts.size(), summary);
throw new ServiceException("推送金蝶财务应付单失败:" + summary);
}
List preEntries = new ArrayList<>();
@@ -554,6 +569,11 @@ public class KingdeeApBillSyncSupport {
log.info("应付单预付信息分录: 正式结算单[{}], 来源[{}], y_billid={}, y_settleamt={}",
settlement.getFormalSettlementNo(), sourcesByBillId.get(billId), billId, settleAmount);
});
+ // 分录解析摘要:同一付款申请跨多预结算分摊后落在同一付款处理单的金额已按 y_billid 合并
+ log.info("应付单预付信息解析完成: 正式结算单[{}], 应抵扣付款申请{}笔, 生成分录{}条, 合计金额{}",
+ settlement.getFormalSettlementNo(), demandAmounts.size(), preEntries.size(),
+ preEntries.stream().map(BfiApBillSavePreEntryDTO::getYSettleamt)
+ .reduce(BigDecimal.ZERO, BigDecimal::add));
return preEntries;
}
@@ -635,7 +655,7 @@ public class KingdeeApBillSyncSupport {
+ "]无结算明细与结算合计费用项,无法解析分录");
}
// 分录解析摘要(联调排查:分录数=结算明细拆分行+手动添加的结算合计费用项,物料/分录标识逐条见下方分录生成日志)
- log.info("金蝶应付单分录解析完成:正式结算单[{}],结算明细{}条 + 手工结算合计{}条 = 共{}条分录",
+ log.info("金蝶应付单分录解析完成: 正式结算单[{}], 结算明细{}条 + 手工结算合计{}条 = 共{}条分录",
settlement.getFormalSettlementNo(), settlementDetails.size(), manualSummaryFees.size(), entryGroups.size());
return entryGroups;
}
@@ -680,7 +700,7 @@ public class KingdeeApBillSyncSupport {
context.taxRateByRule(), rowAmount);
if (splits.isEmpty()) {
// 0元货物行(费用项与结算金额均为0)跳过不生成0元分录
- log.info("金蝶应付单分录生成:来源[{}],费用项与结算金额均为0,跳过该货物行", sourceDesc);
+ log.info("金蝶应付单分录生成: 来源[{}], 费用项与结算金额均为0, 跳过该货物行", sourceDesc);
return;
}
// 运输类数量:取运单运输量(未维护兜底 1)
@@ -707,7 +727,7 @@ public class KingdeeApBillSyncSupport {
String entryCode = entryCodePrefix + "-" + split.getCategory().getLabel() + split.taxRateSuffix();
entryGroups.add(new BfiEntryGroup(materialNumber, entryCode, quantity, price, amount,
amountNoTax, cargoName, vehicleNo, context.settlement().getId()));
- log.info("金蝶应付单分录生成:来源[{}],分录标识[{}],物料编码[{}],数量[{}],单价[{}],含税金额[{}],不含税金额[{}]",
+ log.info("金蝶应付单分录生成: 来源[{}], 分录标识[{}], 物料编码[{}], 数量[{}], 单价[{}], 含税金额[{}], 不含税金额[{}]",
sourceDesc, entryCode, materialNumber, quantity, price, amount, amountNoTax);
}
}
@@ -741,7 +761,7 @@ public class KingdeeApBillSyncSupport {
+ (Func.isBlank(feeItem) ? feeType : feeType + "-" + feeItem);
entryGroups.add(new BfiEntryGroup(materialNumber, entryCode, quantity, price, amount,
null, null, null, settlement.getId()));
- log.info("金蝶应付单分录生成:来源[{}],分录标识[{}],物料编码[{}],数量[{}],单价[{}],含税金额[{}]",
+ log.info("金蝶应付单分录生成: 来源[{}], 分录标识[{}], 物料编码[{}], 数量[{}], 单价[{}], 含税金额[{}]",
sourceDesc, entryCode, materialNumber, quantity, price, amount);
}
@@ -840,7 +860,7 @@ public class KingdeeApBillSyncSupport {
} catch (RuntimeException ignored) {
// 计费方案 JSON 兼容历史数据,解析失败按未配置处理
}
- log.info("合同[{}]计费方案解析:{}项费用类型/费用项配置税率{}", contract.getContractNo(), taxRateByRule.size(), taxRateByRule);
+ log.info("合同[{}]计费方案解析: {}项费用类型/费用项配置税率{}", contract.getContractNo(), taxRateByRule.size(), taxRateByRule);
return taxRateByRule;
}
@@ -1090,7 +1110,7 @@ public class KingdeeApBillSyncSupport {
}
loaded.putIfAbsent(feeItem.getName().trim(), feeItem.getFeeCategory().trim());
}
- log.info("费用项档案加载:{}项(费用项名称 -> 费用大类编码)", loaded.size());
+ log.info("费用项档案加载: {}项(费用项名称 -> 费用大类编码)", loaded.size());
cache.putAll(loaded);
return cache;
}