From 26dc598b58f5f7890b344b3ef518ef626927a3ba 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:28 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E8=B4=A2=E5=8A=A1=E5=BA=94=E4=BB=98?= =?UTF-8?q?=E5=8D=95=E6=8E=A8=E9=80=81=E6=90=BA=E5=B8=A6=E9=A2=84=E4=BB=98?= =?UTF-8?q?=E4=BF=A1=E6=81=AF=E5=88=86=E5=BD=95=E5=B9=B6=E6=8B=A6=E6=88=AA?= =?UTF-8?q?=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; + } + // ==================== 结算明细分录解析(结算明细行 + 结算合计费用项) ==================== /**