From 17b8da0a6523ecf5e5b2f680dd2dfd6006af963f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=98=E6=B3=89=E4=BD=B3?= <1107186916@qq.com> Date: Fri, 18 Sep 2026 19:13:07 +0800 Subject: [PATCH 1/5] =?UTF-8?q?:zap:=20=E8=B0=83=E6=95=B4=E9=87=91?= =?UTF-8?q?=E8=9D=B6=E5=BA=94=E4=BB=98=E5=8D=95=E5=88=86=E5=BD=95=E5=8F=96?= =?UTF-8?q?=E6=95=B0=EF=BC=9A=E6=94=B9=E7=94=B1=E5=BA=94=E4=BB=98=E8=B4=A7?= =?UTF-8?q?=E7=89=A9=E8=B4=B9=E7=94=A8=E6=98=8E=E7=BB=86=E9=80=90=E8=A1=8C?= =?UTF-8?q?=E4=B8=8D=E8=81=9A=E5=90=88=E6=8E=A8=E9=80=81=EF=BC=8C=E5=88=86?= =?UTF-8?q?=E5=BD=95=E6=A0=87=E8=AF=86=E9=80=81=E8=B4=B9=E7=94=A8=E6=98=8E?= =?UTF-8?q?=E7=BB=86id?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../impl/PaymentApplicationServiceImpl.java | 214 +++++++++++------- 1 file changed, 130 insertions(+), 84 deletions(-) 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 7fe0dd99..34b22cb8 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 @@ -72,8 +72,10 @@ import org.springblade.transport.mapper.PaymentApplicationMapper; import org.springblade.transport.mapper.PaymentApplicationRecordMapper; import org.springblade.transport.mapper.PaymentApplicationSettlementMapper; import org.springblade.transport.mapper.PreSettlementAdvanceMapper; +import org.springblade.transport.mapper.PreSettlementDetailFeeMapper; import org.springblade.transport.mapper.PreSettlementMapper; import org.springblade.transport.mapper.ProjectApplyMapper; +import org.springblade.transport.mapper.ReceivablePayableCargoFeeMapper; import org.springblade.transport.mapper.TemporaryCreditLimitMapper; import org.springblade.transport.pojo.dto.PaymentApplicationInvoiceRequest; import org.springblade.transport.pojo.dto.PaymentApplicationRecordRequest; @@ -94,7 +96,9 @@ import org.springblade.transport.pojo.entity.PaymentApplicationRecord; import org.springblade.transport.pojo.entity.PaymentApplicationSettlement; import org.springblade.transport.pojo.entity.PreSettlement; import org.springblade.transport.pojo.entity.PreSettlementAdvance; +import org.springblade.transport.pojo.entity.PreSettlementDetailFee; import org.springblade.transport.pojo.entity.ProjectApply; +import org.springblade.transport.pojo.entity.ReceivablePayableCargoFee; import org.springblade.transport.pojo.entity.TemporaryCreditLimit; import org.springblade.transport.pojo.vo.PaymentApplicationReferenceAmountVO; import org.springblade.transport.pojo.vo.PaymentApplicationVO; @@ -148,6 +152,8 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl detailentry = new ArrayList<>(); for (int index = 0; index < entryGroups.size(); index++) { BfiEntryGroup entryGroup = entryGroups.get(index); @@ -1126,16 +1132,16 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl 0 ? entryGroup.quantity() : BigDecimal.ONE); - // 应付金额(含税):组内结算金额(含税)合计 + // 应付金额(含税):该行应付货物费用行调整后总金额(含税) entry.setEPricetaxtotal(entryGroup.amount()); - // 金额(不含税):组内结算金额(不含税)合计 + // 金额(不含税):该行结算金额(不含税)(应付费用行无不含税字段,经结算货物费用行回链) entry.setEAmount(entryGroup.amountNoTax()); // 【必填】项目.项目编码(联调期临时传金蝶测试档案值) entry.setProjectNumber(KingdeeBfiConstant.BFI_AP_BILL_PROJECT_NUMBER_TEST); - // 【必填】外部系统来源分录标识(候选键):结算明细单据号(多行并入同组时下划线拼接),A103-1 下推时以 awrp_finapbill_entryid 关联此行 - entry.setAwrpEntryCode(entryGroup.documentNo()); + // 【必填】外部系统来源分录标识(候选键):费用明细行id(唯一),A103-1 下推时 awrp_entry_code 与此对应 + entry.setAwrpEntryCode(entryGroup.entryCode()); // 调试 @@ -1186,12 +1192,12 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl entries = new ArrayList<>(); for (int index = 0; index < entryGroups.size(); index++) { BfiEntryGroup entryGroup = entryGroups.get(index); @@ -1240,8 +1246,8 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl entryGroups, List documentNos) { @@ -1299,14 +1305,18 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl物料编码:货物费用行/结算明细货物类型(支持“、”分隔多类型)匹配货物类型档案货物编码; - * 计量单位:快照运费计算单位(历史快照为空时兜底数量单位),档案暂无编码字段暂以单位名称作为编码; - * 分录唯一标识(awrp_entry_code)取所属结算明细单据号,A103-1 下推时以 awrp_finapbill_entryid 关联此行; - * 分录金额直接取结算金额(含税/不含税),不按申请付款金额分摊。

+ * 解析付款申请关联的正式结算明细,逐行生成应付单分录(不聚合,每条来源费用行一条分录)。 + *

取数来源:应付明细货物费用行(blade_receivable_payable_cargo_fee,经结算明细 sourceDetailId 关联应付明细, + * 每货物一条)——货物类型/计量单位/运输数量/含税金额直接取应付费用行(含税金额取调整后总金额 after_amount); + * 应付费用行无不含税字段,不含税金额经结算货物费用行 sourceFeeId 回链(直接选明细路径 sourceFeeId=应付费用行id, + * 预结算合并路径 sourceFeeId=预结算费用行id,经预结算费用行 sourceFeeId 回溯应付费用行id); + * 结算明细无应付费用行时兜底结算明细行(金额取结算金额)。物料编码:货物类型(支持“、”分隔多类型) + * 匹配货物类型档案货物编码;分录唯一标识(awrp_entry_code)取费用明细行id(雪花id天然唯一,满足 A103-1 + * 对方系统唯一行id不能重复;A103-1 与应付单的勾稽关联走 awrp_finapbill_entryid=付款计划 + * awrp_entry_code_p,与本标识无关);分录金额不按申请付款金额分摊。

* * @param entity 付款申请实体 - * @return 应付单分录解析结果(分录组 + 结算明细单据号) + * @return 应付单分录解析结果(分录 + 结算明细单据号) */ private BfiEntryGroupResult resolveBfiEntryGroups(PaymentApplication entity, KingdeeSyncContext context) { List settlementDetails = settlementDetails(entity, context); @@ -1316,37 +1326,37 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl documentNos = settlementDocumentNos(settlementDetails); Map cargoCodeByName = cargoCodeByName(context); - LinkedHashMap groupMap = new LinkedHashMap<>(); - // 优先按货物费用行逐条取数(blade_formal_settlement_detail_fee 每货物一条,金额为结算调整后的精确值) - List detailFees = formalSettlementDetailFees(settlementDetails); - if (Func.isNotEmpty(detailFees)) { - // 结算明细id->单据号映射:货物费用行经此回溯所属结算明细单据号(作为分录唯一标识) - Map detailDocumentNoMap = new LinkedHashMap<>(); - for (FormalSettlementDetail settlementDetail : settlementDetails) { - detailDocumentNoMap.putIfAbsent(settlementDetail.getId(), settlementDetail.getDocumentNo()); - } - for (FormalSettlementDetailFee detailFee : detailFees) { - collectEntryGroups(groupMap, cargoCodeByName, detailFee.getCargoType(), - resolveMeasureunitNumber(null, detailFee.getQuantityUnit()), - detailDocumentNoMap.get(detailFee.getFormalSettlementDetailId()), - money(detailFee.getSettlementAmountTax()), money(detailFee.getSettlementAmountNoTax()), - money(detailFee.getTransportQuantity()), - "货物费用[" + Func.toStr(detailFee.getCargoName()) + " 第" + Func.toStr(detailFee.getLineNo()) + "行]"); - } - } else { - for (FormalSettlementDetail settlementDetail : settlementDetails) { - collectEntryGroups(groupMap, cargoCodeByName, settlementDetail.getCargoType(), + // 应付明细id->应付货物费用行(分录主取数来源) + Map> payableFeesByDetailId = payableCargoFeesByDetailId(settlementDetails); + // 应付费用行id->结算金额(不含税):应付费用行无不含税字段,经结算货物费用行 sourceFeeId 回链 + Map noTaxByPayableFeeId = settlementNoTaxByPayableFeeId(settlementDetails); + List entryGroups = new ArrayList<>(); + for (FormalSettlementDetail settlementDetail : settlementDetails) { + List payableFees = + payableFeesByDetailId.getOrDefault(settlementDetail.getSourceDetailId(), List.of()); + if (Func.isNotEmpty(payableFees)) { + for (ReceivablePayableCargoFee payableFee : payableFees) { + appendEntryGroups(entryGroups, cargoCodeByName, payableFee.getCargoType(), + resolveMeasureunitNumber(payableFee.getPriceUnit(), payableFee.getQuantityUnit()), + payableFee.getId(), + money(payableFee.getAfterAmount()), + noTaxByPayableFeeId.getOrDefault(payableFee.getId(), BigDecimal.ZERO), + money(payableFee.getTransportQuantity()), + "应付货物费用[" + Func.toStr(payableFee.getCargoName()) + " 第" + Func.toStr(payableFee.getLineNo()) + "行]"); + } + } else { + appendEntryGroups(entryGroups, cargoCodeByName, settlementDetail.getCargoType(), resolveMeasureunitNumber(null, settlementDetail.getQuantityUnit()), - settlementDetail.getDocumentNo(), + settlementDetail.getId(), money(settlementDetail.getSettlementAmountTax()), money(settlementDetail.getSettlementAmountNoTax()), money(settlementDetail.getTransportQuantity()), "结算明细[" + Func.toStr(settlementDetail.getDocumentNo()) + " 第" + Func.toStr(settlementDetail.getLineNo()) + "行]"); } } - if (groupMap.isEmpty()) { - throw new ServiceException("推送金蝶财务应付单失败:结算明细未解析出任何分录组,请检查货物类型维护"); + if (entryGroups.isEmpty()) { + throw new ServiceException("推送金蝶财务应付单失败:结算明细未解析出任何分录,请检查货物类型维护"); } - return new BfiEntryGroupResult(new ArrayList<>(groupMap.values()), documentNos); + return new BfiEntryGroupResult(entryGroups, documentNos); } /** 结算明细单据号去重收集(保持行序),用于拼接财务应付单外部系统来源单据号 */ @@ -1372,7 +1382,7 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl货物编码映射 * @param cargoType 货物类型(支持“、”分隔多类型) * @param measureunitNumber 计量单位编码(运费计算单位,档案暂无编码字段暂传单位名称) - * @param documentNo 结算明细单据号,作为分录唯一标识(awrp_entry_code) - * @param amount 结算金额(含税) - * @param amountNoTax 结算金额(不含税) - * @param quantity 运输数量 + * @param sourceRowId 来源行id(应付货物费用行id,兜底结算明细行id),雪花id天然唯一 + * @param amount 该行金额(含税) + * @param amountNoTax 该行金额(不含税) + * @param quantity 该行运输数量 * @param sourceDesc 来源行描述,用于异常提示 */ - private void collectEntryGroups(LinkedHashMap groupMap, Map cargoCodeByName, - String cargoType, String measureunitNumber, String documentNo, BigDecimal amount, BigDecimal amountNoTax, - BigDecimal quantity, String sourceDesc) { + private void appendEntryGroups(List entryGroups, Map cargoCodeByName, + String cargoType, String measureunitNumber, Long sourceRowId, + BigDecimal amount, BigDecimal amountNoTax, BigDecimal quantity, String sourceDesc) { List cargoNames = splitCargoTypes(cargoType); if (Func.isBlank(measureunitNumber)) { throw new ServiceException("推送金蝶财务应付单失败:" + sourceDesc + "计量单位为空,请先维护运费计算单位"); } - if (Func.isBlank(documentNo)) { - throw new ServiceException("推送金蝶财务应付单失败:" + sourceDesc + "单据号为空,无法生成金蝶分录标识(awrp_entry_code)"); - } + // 分录唯一标识(awrp_entry_code,A103-1 要求对方系统唯一行id不能重复):雪花id天然唯一,同行多类型拆分时追加序号 + String rowEntryCode = String.valueOf(sourceRowId); List amountShares = splitEqually(amount, cargoNames.size()); List amountNoTaxShares = splitEqually(amountNoTax, cargoNames.size()); List quantityShares = splitEqually(quantity, cargoNames.size()); @@ -1452,26 +1462,16 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl new BfiEntryGroup(left.materialNumber(), left.measureunitNumber(), - joinEntryDocumentNos(left.documentNo(), right.documentNo()), - left.quantity().add(right.quantity()), left.amount().add(right.amount()), left.amountNoTax().add(right.amountNoTax()))); + String entryCode = cargoNames.size() > 1 ? rowEntryCode + "-" + (index + 1) : rowEntryCode; + entryGroups.add(new BfiEntryGroup(materialNumber, measureunitNumber, entryCode, + quantityShares.get(index), amountShares.get(index), amountNoTaxShares.get(index))); + log.info("金蝶应付单分录生成:来源[{}],分录标识[{}],物料编码[{}],计量单位[{}],数量[{}],含税金额[{}],不含税金额[{}]", + sourceDesc, entryCode, materialNumber, measureunitNumber, + quantityShares.get(index), amountShares.get(index), amountNoTaxShares.get(index)); } } - /** 分录组合并时结算明细单据号去重下划线拼接(保持先后顺序),与外部系统来源单据号拼接规则一致 */ - private String joinEntryDocumentNos(String left, String right) { - if (Func.isBlank(left)) { - return right; - } - if (Func.isBlank(right) || left.equals(right)) { - return left; - } - return left.contains(right) ? left : right.contains(left) ? right : left + "_" + right; - } - - /** 付款申请关联的正式结算货物费用行(blade_formal_settlement_detail_fee,每货物一条,金额为结算调整后精确值) */ + /** 付款申请关联的正式结算货物费用行(blade_formal_settlement_detail_fee,用于不含税金额回链应付费用行) */ private List formalSettlementDetailFees(List settlementDetails) { List settlementDetailIds = settlementDetails.stream().map(FormalSettlementDetail::getId) .filter(Objects::nonNull).distinct().toList(); @@ -1484,6 +1484,52 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl应付货物费用行(blade_receivable_payable_cargo_fee,经结算明细 sourceDetailId 关联,每货物一条,分录主取数来源) */ + private Map> payableCargoFeesByDetailId(List settlementDetails) { + List sourceDetailIds = settlementDetails.stream() + .map(FormalSettlementDetail::getSourceDetailId).filter(Objects::nonNull).distinct().toList(); + if (Func.isEmpty(sourceDetailIds)) { + return Map.of(); + } + return receivablePayableCargoFeeMapper.selectList(Wrappers.lambdaQuery() + .in(ReceivablePayableCargoFee::getDetailId, sourceDetailIds) + .eq(ReceivablePayableCargoFee::getIsDeleted, 0) + .orderByAsc(ReceivablePayableCargoFee::getLineNo)) + .stream().collect(Collectors.groupingBy(ReceivablePayableCargoFee::getDetailId)); + } + + /** + * 应付货物费用行id->结算金额(不含税):应付费用行无不含税字段,经结算货物费用行 sourceFeeId 回链获取。 + * 直接选应收应付明细路径 sourceFeeId=应付费用行id;预结算合并路径 sourceFeeId=预结算费用行id, + * 经预结算费用行 sourceFeeId(指向应付费用行)回溯;无对应结算费用行时无映射,调用方兜底 0 + */ + private Map settlementNoTaxByPayableFeeId(List settlementDetails) { + List settlementFees = formalSettlementDetailFees(settlementDetails); + if (Func.isEmpty(settlementFees)) { + return Map.of(); + } + // 上游费用行id(应付费用行id或预结算费用行id)-> 不含税合计 + Map noTaxBySourceFeeId = new LinkedHashMap<>(); + for (FormalSettlementDetailFee settlementFee : settlementFees) { + if (settlementFee.getSourceFeeId() == null) { + continue; + } + noTaxBySourceFeeId.merge(settlementFee.getSourceFeeId(), money(settlementFee.getSettlementAmountNoTax()), BigDecimal::add); + } + if (noTaxBySourceFeeId.isEmpty()) { + return Map.of(); + } + // sourceFeeId 指向预结算费用行的:经预结算费用行回溯应付费用行id(直接路径无预结算费用行,保持原 id) + Map payableFeeIdByPreFeeId = preSettlementDetailFeeMapper.selectList( + Wrappers.lambdaQuery().in(PreSettlementDetailFee::getId, new ArrayList<>(noTaxBySourceFeeId.keySet()))) + .stream().filter(preSettlementFee -> preSettlementFee.getSourceFeeId() != null) + .collect(Collectors.toMap(PreSettlementDetailFee::getId, PreSettlementDetailFee::getSourceFeeId, (left, right) -> left)); + Map noTaxByPayableFeeId = new LinkedHashMap<>(); + noTaxBySourceFeeId.forEach((sourceFeeId, noTax) -> + noTaxByPayableFeeId.merge(payableFeeIdByPreFeeId.getOrDefault(sourceFeeId, sourceFeeId), noTax, BigDecimal::add)); + return noTaxByPayableFeeId; + } + /** 付款申请关联的正式结算明细(关联表优先,兼容仅有 settlementId 的存量数据) */ private List formalSettlementDetails(PaymentApplication entity) { List settlementIds = settlementRelationMapper.selectList(Wrappers.lambdaQuery() From 63b2e89cb60691f7646747e2f17a476769ea966e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=98=E6=B3=89=E4=BD=B3?= <1107186916@qq.com> Date: Fri, 18 Sep 2026 19:37:27 +0800 Subject: [PATCH 2/5] =?UTF-8?q?:zap:=20=E9=87=91=E8=9D=B6=E5=BA=94?= =?UTF-8?q?=E4=BB=98=E5=8D=95=E5=88=86=E5=BD=95=E6=96=B0=E5=A2=9E=E6=98=8E?= =?UTF-8?q?=E7=BB=86=E5=8D=95=E4=BB=B7=E6=8E=A8=E9=80=81=EF=BC=8C=E5=8F=96?= =?UTF-8?q?=E6=95=B0=E5=8E=BB=E6=8E=89=E7=BB=93=E7=AE=97=E6=98=8E=E7=BB=86?= =?UTF-8?q?=E8=A1=8C=E5=85=9C=E5=BA=95=E6=94=B9=E4=B8=BA=E7=BC=BA=E5=A4=B1?= =?UTF-8?q?=E5=8D=B3=E6=8A=A5=E9=94=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../impl/PaymentApplicationServiceImpl.java | 56 +++++++++---------- 1 file changed, 26 insertions(+), 30 deletions(-) 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 34b22cb8..d60ede21 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 @@ -1134,6 +1134,8 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl 0 ? entryGroup.quantity() : BigDecimal.ONE); + // 明细.单价:该行运输单价(费用明细 unit_price;单据头 ispricetotal=true 录入总价,单价仅作参考) + entry.setPrice(entryGroup.price()); // 应付金额(含税):该行应付货物费用行调整后总金额(含税) entry.setEPricetaxtotal(entryGroup.amount()); // 金额(不含税):该行结算金额(不含税)(应付费用行无不含税字段,经结算货物费用行回链) @@ -1275,13 +1277,14 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl @@ -1326,7 +1329,7 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl documentNos = settlementDocumentNos(settlementDetails); Map cargoCodeByName = cargoCodeByName(context); - // 应付明细id->应付货物费用行(分录主取数来源) + // 应付明细id->应付货物费用行(分录取数来源,无兜底) Map> payableFeesByDetailId = payableCargoFeesByDetailId(settlementDetails); // 应付费用行id->结算金额(不含税):应付费用行无不含税字段,经结算货物费用行 sourceFeeId 回链 Map noTaxByPayableFeeId = settlementNoTaxByPayableFeeId(settlementDetails); @@ -1334,27 +1337,19 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl payableFees = payableFeesByDetailId.getOrDefault(settlementDetail.getSourceDetailId(), List.of()); - if (Func.isNotEmpty(payableFees)) { - for (ReceivablePayableCargoFee payableFee : payableFees) { - appendEntryGroups(entryGroups, cargoCodeByName, payableFee.getCargoType(), - resolveMeasureunitNumber(payableFee.getPriceUnit(), payableFee.getQuantityUnit()), - payableFee.getId(), - money(payableFee.getAfterAmount()), - noTaxByPayableFeeId.getOrDefault(payableFee.getId(), BigDecimal.ZERO), - money(payableFee.getTransportQuantity()), - "应付货物费用[" + Func.toStr(payableFee.getCargoName()) + " 第" + Func.toStr(payableFee.getLineNo()) + "行]"); - } - } else { - appendEntryGroups(entryGroups, cargoCodeByName, settlementDetail.getCargoType(), - resolveMeasureunitNumber(null, settlementDetail.getQuantityUnit()), - settlementDetail.getId(), - money(settlementDetail.getSettlementAmountTax()), money(settlementDetail.getSettlementAmountNoTax()), - money(settlementDetail.getTransportQuantity()), - "结算明细[" + Func.toStr(settlementDetail.getDocumentNo()) + " 第" + Func.toStr(settlementDetail.getLineNo()) + "行]"); + if (Func.isEmpty(payableFees)) { + throw new ServiceException("推送金蝶财务应付单失败:结算明细[" + Func.toStr(settlementDetail.getDocumentNo()) + + "]对应应付明细无货物费用行,分录取数必须来自应付费用明细,请先维护费用数据"); + } + for (ReceivablePayableCargoFee payableFee : payableFees) { + appendEntryGroups(entryGroups, cargoCodeByName, payableFee.getCargoType(), + resolveMeasureunitNumber(payableFee.getPriceUnit(), payableFee.getQuantityUnit()), + payableFee.getId(), money(payableFee.getUnitPrice()), + money(payableFee.getAfterAmount()), + noTaxByPayableFeeId.getOrDefault(payableFee.getId(), BigDecimal.ZERO), + money(payableFee.getTransportQuantity()), + "应付货物费用[" + Func.toStr(payableFee.getCargoName()) + " 第" + Func.toStr(payableFee.getLineNo()) + "行]"); } - } - if (entryGroups.isEmpty()) { - throw new ServiceException("推送金蝶财务应付单失败:结算明细未解析出任何分录,请检查货物类型维护"); } return new BfiEntryGroupResult(entryGroups, documentNos); } @@ -1431,21 +1426,22 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl货物编码映射 * @param cargoType 货物类型(支持“、”分隔多类型) * @param measureunitNumber 计量单位编码(运费计算单位,档案暂无编码字段暂传单位名称) - * @param sourceRowId 来源行id(应付货物费用行id,兜底结算明细行id),雪花id天然唯一 + * @param sourceRowId 应付货物费用行id,雪花id天然唯一 + * @param price 该行运输单价(费用明细 unit_price) * @param amount 该行金额(含税) * @param amountNoTax 该行金额(不含税) * @param quantity 该行运输数量 * @param sourceDesc 来源行描述,用于异常提示 */ private void appendEntryGroups(List entryGroups, Map cargoCodeByName, - String cargoType, String measureunitNumber, Long sourceRowId, + String cargoType, String measureunitNumber, Long sourceRowId, BigDecimal price, BigDecimal amount, BigDecimal amountNoTax, BigDecimal quantity, String sourceDesc) { List cargoNames = splitCargoTypes(cargoType); if (Func.isBlank(measureunitNumber)) { @@ -1464,10 +1460,10 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl 1 ? rowEntryCode + "-" + (index + 1) : rowEntryCode; entryGroups.add(new BfiEntryGroup(materialNumber, measureunitNumber, entryCode, - quantityShares.get(index), amountShares.get(index), amountNoTaxShares.get(index))); - log.info("金蝶应付单分录生成:来源[{}],分录标识[{}],物料编码[{}],计量单位[{}],数量[{}],含税金额[{}],不含税金额[{}]", + quantityShares.get(index), price, amountShares.get(index), amountNoTaxShares.get(index))); + log.info("金蝶应付单分录生成:来源[{}],分录标识[{}],物料编码[{}],计量单位[{}],数量[{}],单价[{}],含税金额[{}],不含税金额[{}]", sourceDesc, entryCode, materialNumber, measureunitNumber, - quantityShares.get(index), amountShares.get(index), amountNoTaxShares.get(index)); + quantityShares.get(index), price, amountShares.get(index), amountNoTaxShares.get(index)); } } From e79c4ce888740c0358552e9029447ad1d5fc6b2b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=98=E6=B3=89=E4=BD=B3?= <1107186916@qq.com> Date: Fri, 18 Sep 2026 20:42:44 +0800 Subject: [PATCH 3/5] =?UTF-8?q?:zap:=20=E9=87=91=E8=9D=B6=E5=90=8C?= =?UTF-8?q?=E6=AD=A5=E6=B5=81=E7=A8=8B=E6=8B=86=E5=88=86=EF=BC=9A=E6=AD=A3?= =?UTF-8?q?=E5=BC=8F=E7=BB=93=E7=AE=97=E5=8D=95=E5=90=8C=E6=AD=A5=E6=8E=A8?= =?UTF-8?q?=E9=80=81=E8=B4=A2=E5=8A=A1=E5=BA=94=E4=BB=98=E5=8D=95A102-2?= =?UTF-8?q?=EF=BC=8C=E4=BB=98=E6=AC=BE=E7=94=B3=E8=AF=B7=E5=90=8C=E6=AD=A5?= =?UTF-8?q?=E4=BB=85=E6=8E=A8=E9=80=81=E4=BB=98=E6=AC=BE=E7=94=B3=E8=AF=B7?= =?UTF-8?q?=E5=8D=95A103-1/A103-3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../transport/service/KingdeeSyncContext.java | 7 +- .../impl/FormalSettlementServiceImpl.java | 37 +- .../impl/PaymentApplicationServiceImpl.java | 796 +++--------------- .../support/KingdeeApBillSyncSupport.java | 708 ++++++++++++++++ 4 files changed, 841 insertions(+), 707 deletions(-) create mode 100644 blade-service/blade-transport/src/main/java/org/springblade/transport/support/KingdeeApBillSyncSupport.java diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/KingdeeSyncContext.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/KingdeeSyncContext.java index f2cbde55..fcc05c4f 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/KingdeeSyncContext.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/KingdeeSyncContext.java @@ -25,6 +25,7 @@ package org.springblade.transport.service; import org.springblade.transport.pojo.entity.ContractManage; +import org.springblade.transport.pojo.entity.FormalSettlement; import org.springblade.transport.pojo.entity.FormalSettlementDetail; import org.springblade.transport.pojo.entity.ProjectApply; @@ -42,10 +43,12 @@ public record KingdeeSyncContext(Map cargoCodeByName, Map customerCodeCache, Map projectCache, Map apBillNoCache, Map> settlementDetailCache, - Map apBillPlanCodeCache) { + Map apBillPlanCodeCache, + Map settlementCache) { /** 空上下文:缓存可写但初始为空,首条记录命中后全批复用 */ public static KingdeeSyncContext empty() { - return new KingdeeSyncContext(new HashMap<>(), new HashMap<>(), new HashMap<>(), new HashMap<>(), new HashMap<>(), new HashMap<>(), new HashMap<>()); + return new KingdeeSyncContext(new HashMap<>(), new HashMap<>(), new HashMap<>(), new HashMap<>(), + new HashMap<>(), new HashMap<>(), new HashMap<>(), new HashMap<>()); } } diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/FormalSettlementServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/FormalSettlementServiceImpl.java index 349459b6..9a6cdc4c 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/FormalSettlementServiceImpl.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/FormalSettlementServiceImpl.java @@ -35,6 +35,7 @@ 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.constant.KingdeeBfiConstant; import org.springblade.transport.constant.TransportBusiConstant; import org.springblade.transport.mapper.ReceivablePayableDetailMapper; import org.springblade.transport.mapper.ReceivablePayableCargoFeeMapper; @@ -75,6 +76,8 @@ import org.springblade.transport.service.IFormalSettlementService; import org.springblade.transport.service.IContractManageService; import org.springblade.transport.service.IPreSettlementService; import org.springblade.transport.service.IWaybillService; +import org.springblade.transport.service.KingdeeSyncContext; +import org.springblade.transport.support.KingdeeApBillSyncSupport; import org.springblade.transport.wrapper.PreSettlementWrapper; import org.springblade.core.tool.jackson.JsonUtil; import org.springblade.transport.wrapper.FormalSettlementWrapper; @@ -136,6 +139,7 @@ public class FormalSettlementServiceImpl extends BaseServiceImpl selectPage(IPage page, FormalSettlementVO query) { @@ -472,17 +476,40 @@ public class FormalSettlementServiceImpl extends BaseServiceImpl details = detailMapper.selectList(Wrappers.lambdaQuery() + .eq(FormalSettlementDetail::getFormalSettlementId, id) + .eq(FormalSettlementDetail::getIsDeleted, 0) + .orderByAsc(FormalSettlementDetail::getLineNo)); + if (Func.isEmpty(details)) { + throw new ServiceException("推送金蝶财务应付单失败:正式结算单无结算明细,无法解析物料与计量单位"); + } + KingdeeSyncContext context = KingdeeSyncContext.empty(); + // 财务应付单防重键(awrp_src_code):正式结算单号_结算明细单据号下划线拼接, + // 付款申请 A103-1 下推前的前置校验与付款计划反查均按该键勾稽 + String apBillSrcCode = kingdeeApBillSyncSupport.apBillSrcCode(settlement, details); + List entryGroups = kingdeeApBillSyncSupport.resolveEntryGroups(details, context); + KingdeeApBillSyncSupport.BfiApBillPushOutcome outcome = kingdeeApBillSyncSupport.pushApBill( + apBillSrcCode, settlement.getContractId(), entryGroups, settlement.getFormalSettlementNo(), context); + // 回写财务应付单单据编号(A102-2 billNo)与同步状态;付款申请侧回填的付款申请单编号(A103-2)不是同一单据 + settlement.setKingdeeBillNo(outcome.billNo()); + settlement.setKingdeeSyncStatus(KingdeeBfiConstant.KINGDEE_STATUS_SYNCED); settlement.setSyncedTime(LocalDateTime.now()); updateById(settlement); - saveChange(settlement.getId(), "结算单基本信息", null, "调整", "同步金蝶单据" + kingdeeNo, ""); - return kingdeeNo; + saveChange(settlement.getId(), "结算单基本信息", null, "调整", "同步金蝶财务应付单" + outcome.billNo(), ""); + return outcome.billNo(); } @Override 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 d60ede21..cbc53e08 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 @@ -27,29 +27,18 @@ import lombok.extern.slf4j.Slf4j; import org.springblade.core.log.exception.ServiceException; import org.springblade.core.mp.base.BaseServiceImpl; import org.springblade.core.secure.utils.AuthUtil; -import org.springblade.core.tool.api.R; import org.springblade.core.tool.jackson.JsonUtil; import org.springblade.core.tool.utils.BeanUtil; import org.springblade.core.tool.utils.Func; import org.springblade.core.tool.utils.SpringUtil; import org.springblade.system.cache.DictBizCache; -import org.springblade.system.cache.SysCache; -import org.springblade.system.feign.ISysClient; -import org.springblade.system.pojo.entity.CargoType; -import org.springblade.system.pojo.entity.Dept; import org.springblade.system.pojo.entity.DictBiz; -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.BfiPayApplyPushDTO; import org.springblade.thirdparty.bfi.pojo.dto.BfiPayApplyPushEntryDTO; import org.springblade.thirdparty.bfi.pojo.dto.BfiPayApplyQueryDTO; import org.springblade.thirdparty.bfi.pojo.dto.BfiPayApplySaveDTO; import org.springblade.thirdparty.bfi.pojo.dto.BfiPayApplySaveEntryDTO; import org.springblade.thirdparty.bfi.pojo.dto.BfiPayBillQueryDTO; -import org.springblade.thirdparty.bfi.pojo.vo.BfiApBillPlanVO; -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.pojo.vo.BfiPayApplyEntryVO; import org.springblade.thirdparty.bfi.pojo.vo.BfiPayApplyVO; @@ -62,7 +51,6 @@ import org.springblade.transport.mapper.BillLedgerMapper; import org.springblade.transport.mapper.BillLedgerUsageMapper; import org.springblade.transport.mapper.ContractManageMapper; import org.springblade.transport.mapper.CustomerArchiveMapper; -import org.springblade.transport.mapper.FormalSettlementDetailFeeMapper; import org.springblade.transport.mapper.FormalSettlementDetailMapper; import org.springblade.transport.mapper.FormalSettlementMapper; import org.springblade.transport.mapper.FormalSettlementPaymentMapper; @@ -72,10 +60,8 @@ import org.springblade.transport.mapper.PaymentApplicationMapper; import org.springblade.transport.mapper.PaymentApplicationRecordMapper; import org.springblade.transport.mapper.PaymentApplicationSettlementMapper; import org.springblade.transport.mapper.PreSettlementAdvanceMapper; -import org.springblade.transport.mapper.PreSettlementDetailFeeMapper; import org.springblade.transport.mapper.PreSettlementMapper; import org.springblade.transport.mapper.ProjectApplyMapper; -import org.springblade.transport.mapper.ReceivablePayableCargoFeeMapper; import org.springblade.transport.mapper.TemporaryCreditLimitMapper; import org.springblade.transport.pojo.dto.PaymentApplicationInvoiceRequest; import org.springblade.transport.pojo.dto.PaymentApplicationRecordRequest; @@ -87,7 +73,6 @@ 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.FormalSettlementPayment; import org.springblade.transport.pojo.entity.FormalSettlementSource; import org.springblade.transport.pojo.entity.PaymentApplication; @@ -96,16 +81,14 @@ import org.springblade.transport.pojo.entity.PaymentApplicationRecord; import org.springblade.transport.pojo.entity.PaymentApplicationSettlement; import org.springblade.transport.pojo.entity.PreSettlement; import org.springblade.transport.pojo.entity.PreSettlementAdvance; -import org.springblade.transport.pojo.entity.PreSettlementDetailFee; import org.springblade.transport.pojo.entity.ProjectApply; -import org.springblade.transport.pojo.entity.ReceivablePayableCargoFee; import org.springblade.transport.pojo.entity.TemporaryCreditLimit; import org.springblade.transport.pojo.vo.PaymentApplicationReferenceAmountVO; import org.springblade.transport.pojo.vo.PaymentApplicationVO; import org.springblade.transport.service.IFormalSettlementService; import org.springblade.transport.service.IPaymentApplicationService; import org.springblade.transport.service.KingdeeSyncContext; -import org.springblade.transport.support.ExternalSrcCodeSupport; +import org.springblade.transport.support.KingdeeApBillSyncSupport; import org.springblade.transport.wrapper.PaymentApplicationWrapper; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; @@ -117,7 +100,6 @@ import java.time.LocalDate; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; import java.util.ArrayList; -import java.util.Arrays; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.List; @@ -151,9 +133,6 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl settlementSrcCodes = settlementApBillSrcCodes(entity, context); + for (Map.Entry srcCodeEntry : settlementSrcCodes.entrySet()) { + String existBillNo = context.apBillNoCache().computeIfAbsent(srcCodeEntry.getValue(), + code -> kingdeeApBillSyncSupport.queryApBillExistNos(List.of(code)).getOrDefault(code, "")); + if (Func.isEmpty(existBillNo)) { + FormalSettlement settlement = context.settlementCache().get(srcCodeEntry.getKey()); + throw new ServiceException("推送金蝶付款申请失败:正式结算单[" + + (settlement == null ? srcCodeEntry.getKey() : settlement.getFormalSettlementNo()) + + "]尚未同步金蝶财务应付单(A102-2),请先在正式付款单页面同步金蝶"); + } } + // A103-1 幂等预检:按付款申请号查询金蝶付款申请单是否已生成(上次下推成功但本地事务回滚未落库的场景), + // 已生成直接回填单据id与编号视为同步成功,未生成才继续 A103-1 下推,下推接口真正报错才算同步失败 + kingdeeBillId = reconcileExistPayApply(entity); if (Func.isEmpty(kingdeeBillId)) { - kingdeeBillId = pushBfiPayApplyPush(entity, entryGroupResult.entryGroups(), apBillOutcome.planEntryCodes(), context); + // A103-1 分录与财务应付单明细分录一一对应(awrp_entry_code 取费用明细行id), + // 多结算单付款申请各分录按所属结算单勾稽各自应付单的付款计划分录 awrp_entry_code_p + List entryGroups = + kingdeeApBillSyncSupport.resolveEntryGroups(settlementDetails(entity, context), context); + log.info("付款申请[{}]正式结算付款:金蝶财务应付单(A102-2)已生成,开始推送金蝶付款申请单(A103-1 应付下推),分录{}条", + entity.getPaymentNo(), entryGroups.size()); + kingdeeBillId = pushBfiPayApplyPush(entity, entryGroups, context); log.info("付款申请[{}]金蝶付款申请单(A103-1 应付下推)推送成功:kingdeeBillId={}", entity.getPaymentNo(), kingdeeBillId); } } else { @@ -449,7 +430,7 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl单据编号 映射 - */ - private Map queryApBillExistNos(List awrpSrcCodes) { - Map existBillNos = new LinkedHashMap<>(); - // 防重键批量分批查询,避免单次过滤条件过长 - batchForeach(awrpSrcCodes, PAY_RESULT_QUERY_BATCH, batch -> { - BfiApBillQueryDTO param = new BfiApBillQueryDTO(); - param.setAwrpSrcCode(batch); - param.setAwrpSrcSys(bfiPayApplySrcSys); - for (BfiApBillVO apBill : BfiUtil.queryAllPages(pageNo -> { - param.setPageNo(pageNo); - param.setPageSize(PAY_RESULT_PAGE_SIZE); - return bfiService.queryApBill(param); - })) { - if (Func.isNotEmpty(apBill.getAwrpSrcCode())) { - existBillNos.put(apBill.getAwrpSrcCode(), apBill.getBillno()); - } - } - }); - return existBillNos; - } - - /** - * A102-2 结果回写关联正式结算单:金蝶单据编号(财务应付单编号)、同步状态置已同步、同步时间。 - * 正式结算单列表据此回显金蝶单据号与单据状态;一个付款申请可能关联多张正式结算单, - * 按结算明细归属的结算单去重后批量回写,结算单已同步时重复回写为幂等更新。 - * 注意回写的是财务应付单编号(A102-2 billNo),付款申请表的 kingdeeBillNo 后续由 - * A103-2 定时回写任务覆盖为付款申请单编号,两者不是同一单据,禁止混用 - * - * @param entity 付款申请实体 - * @param apBillBillNo 金蝶财务应付单单据编号(A102-2 返回) - * @param context 金蝶同步上下文(缓存载体) - */ - private void syncSettlementKingdeeResult(PaymentApplication entity, String apBillBillNo, KingdeeSyncContext context) { - if (Func.isEmpty(apBillBillNo)) { - return; - } - List settlementIds = settlementDetails(entity, context).stream() - .map(FormalSettlementDetail::getFormalSettlementId) - .filter(Objects::nonNull).distinct().toList(); - if (Func.isEmpty(settlementIds)) { - return; - } - formalSettlementMapper.update(null, Wrappers.lambdaUpdate() - .in(FormalSettlement::getId, settlementIds) - .set(FormalSettlement::getKingdeeBillNo, apBillBillNo) - .set(FormalSettlement::getKingdeeSyncStatus, KingdeeBfiConstant.KINGDEE_STATUS_SYNCED) - .set(FormalSettlement::getSyncedTime, LocalDateTime.now())); - } - - /** - * 查询正式结算单下绑定、单据状态为审核通过(approved)的付款信息(blade_formal_settlement_payment)。 - * 付款申请可能关联多张正式结算单,按结算明细归属的结算单汇总去重后查询,创建时间升序。 - * 当前付款计划 planentity 暂不传、本方法未启用,保留待付款计划启用后供 A102-2 取数 - * - * @param entity 付款申请实体 - * @param context 金蝶同步上下文(缓存载体) - * @return 审核通过的付款信息列表 - */ - private List approvedSettlementPayments(PaymentApplication entity, KingdeeSyncContext context) { - List settlementIds = settlementDetails(entity, context).stream() - .map(FormalSettlementDetail::getFormalSettlementId) - .filter(Objects::nonNull).distinct().toList(); - if (Func.isEmpty(settlementIds)) { - return List.of(); - } - return formalSettlementPaymentMapper.selectList(Wrappers.lambdaQuery() - .in(FormalSettlementPayment::getFormalSettlementId, settlementIds) - .eq(FormalSettlementPayment::getIsDeleted, 0) - .eq(FormalSettlementPayment::getBillStatus, APPROVED) - .and(wrapper -> wrapper.isNull(FormalSettlementPayment::getKingdeeBillNo) - .or().eq(FormalSettlementPayment::getKingdeeBillNo, "")) - .orderByAsc(FormalSettlementPayment::getCreateTime)); - } - - /** - * A102-4 反查财务应付单付款计划分录的外部系统来源分录标识(awrp_entry_code_p)。 - * A102-2 保存未传 planentity,由金蝶按明细自动汇总生成(awrp_entry_code_p 不传时自动取外部系统单据号), - * A103-1 应付下推的 awrp_finapbill_entryid 需填该值勾稽应付单; - * 结果按防重键(awrp_src_code)缓存,恢复场景避免重复远程查询 - * - * @param entity 付款申请实体(异常提示用) - * @param apBillSrcCode 财务应付单防重键(外部系统来源单据号) - * @param context 金蝶同步上下文(缓存载体) - * @return 付款计划分录 awrp_entry_code_p - */ - private String apBillPlanEntryCode(PaymentApplication entity, String apBillSrcCode, KingdeeSyncContext context) { - return context.apBillPlanCodeCache().computeIfAbsent(apBillSrcCode, code -> { - BfiApBillQueryDTO param = new BfiApBillQueryDTO(); - param.setAwrpSrcCode(List.of(code)); - param.setAwrpSrcSys(bfiPayApplySrcSys); - List bills = BfiUtil.queryAllPages(pageNo -> { - param.setPageNo(pageNo); - param.setPageSize(PAY_RESULT_PAGE_SIZE); - return bfiService.queryApBill(param); - }); - for (BfiApBillVO apBill : bills) { - if (Func.isNotEmpty(apBill.getPlanentity())) { - for (BfiApBillPlanVO plan : apBill.getPlanentity()) { - if (Func.isNotBlank(plan.getAwrpEntryCodeP())) { - return plan.getAwrpEntryCodeP(); - } - } - } - } - throw new ServiceException("推送金蝶付款申请(应付下推)失败:财务应付单[" + entity.getPaymentNo() + "]付款计划分录 awrp_entry_code_p 未查询到,无法勾稽关联"); - }); - } - /** * A103-4 分页查询:按付款人(公司)编码 + 最近1个月业务日期窗口(接口限制不超过1个月),自动翻页 * @@ -955,14 +823,14 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl entryGroups, List documentNos, KingdeeSyncContext context) { - // A102-4 幂等预检:按防重键查询金蝶是否已生成财务应付单(批量同步时整批已预热缓存,此处仅懒加载未命中编码), - // 已存在则幂等跳过保存,由调用方经 A103-2 预检后续推付款申请(A103-1)或直接回填已有付款申请单 - String apBillSrcCode = ExternalSrcCodeSupport.buildSrcCode(entity.getPaymentNo(), documentNos); - String existBillNo = context.apBillNoCache().computeIfAbsent(apBillSrcCode, code -> - queryApBillExistNos(List.of(code)).getOrDefault(code, "")); - if (Func.isNotEmpty(existBillNo)) { - log.info("付款申请[{}]金蝶财务应付单已生成(单据编号[{}]),幂等跳过 A102-2 保存,继续后续下推流程", entity.getPaymentNo(), existBillNo); - return new BfiApBillPushOutcome(existBillNo, true, List.of()); - } - ContractManage contract = requireContract(entity, context); - // 单据类型按映射字典转换:默认服务采购应付,金蝶确认其他类型后修改 BFI_AP_BILL_BILLTYPE_NAME 即可 - String billtypeNumber = KingdeeBfiConstant.billtypeBy(KingdeeBfiConstant.AP_BILL_BILLTYPE_MAP, KingdeeBfiConstant.BFI_AP_BILL_BILLTYPE_NAME); - if (Func.isBlank(billtypeNumber)) { - throw new ServiceException("推送金蝶财务应付单失败:单据类型[" + KingdeeBfiConstant.BFI_AP_BILL_BILLTYPE_NAME + "]未在映射字典中维护"); - } - String orgNumber = bfiOrgNumber(contract); - // 币种取合同结算币种 - String currency = bfiCurrency(contract); - // 单据日期:yyyy-MM-dd HH:mm:ss(各接口统一格式) - String billDateText = bfiDateTimeText(entity); - BfiApBillSaveDTO param = new BfiApBillSaveDTO(); - // 【必填】单据类型.编码 - param.setBilltypeidNumber(billtypeNumber); - // 【必填】结算组织.编码 - param.setOrgNumber(orgNumber); - // 【必填】单据日期 - param.setBizdate(billDateText); - // 【必填】往来类型:合同类别为“客户合同”时为 bd_customer(客户),否则 bd_supplier(供应商) - param.setAsstacttype(bfiAsstactType(contract)); - // 【必填】往来户.编码:合同乙方关联客商档案客商编码 - param.setAsstactNumber(bfiAsstactNumber(contract, context)); - // 【必填】付款方式(CASH:现购, CREDIT:赊购) - param.setPurmode(KingdeeBfiConstant.BFI_AP_BILL_PUR_MODE_CASH); - // 【必填】结算币别.货币代码 - param.setCurrencyNumber(currency); - // 【必填】付款组织.编码 - param.setPayorgNumber(orgNumber); - // 采购组织.编码(非必传) - // param.setPurorgNumber(orgNumber); - // 【必填】款项性质.编码 - param.setPaypropertyNumber(KingdeeBfiConstant.BFI_AP_BILL_PAY_PROPERTY); - // TODO 待确认:结算方式编码(settlementtype_number)与税率编码(taxrateid_number)需金蝶侧确认后补充,当前不传 - // 录入含税价(非必传) - param.setIsincludetax(true); - // 录入总价,为 true 后可不录入单价(非必传) - param.setIspricetotal(true); - param.setIsintax(false); - // 汇率表.编码(非必传) - // param.setExratetableNumber(bfiPayApplyExratetable); - // 汇率日期(非必传) - // param.setExratedate(billDateText); - // 往来单位名称(非必传) - // param.setAsstactname(entity.getPayeeName()); - // 备注(非必传) - // param.setRemark(entity.getPaymentNo() + (Func.isEmpty(entity.getSettlementNo()) ? "" : " " + entity.getSettlementNo())); - // 【必填】外部系统编号(候选键) - param.setAwrpSrcSys(bfiPayApplySrcSys); - // 【必填】外部系统来源单据号(候选键):付款申请号_结算明细单据号1_单据号2...(复用预检已组装的防重键) - param.setAwrpSrcCode(apBillSrcCode); - // 来源单据(非必传,有上游合同时传 awrp_contract_info) - // param.setAwrpSrcBt(KingdeeBfiConstant.BFI_CONTRACT_SRC_BT); - // 上游外部系统单据号/上游合同号(非必传) - // if (Func.isNotEmpty(entity.getContractNo())) { - // param.setAwrpSrcConNo(entity.getContractNo()); - // } - // 【必填】外部单据类型 - param.setAwrpSrcType(KingdeeBfiConstant.BFI_AP_BILL_SRC_TYPE); - - // 明细分录:每条应付货物费用行一条(不聚合),无吨位时数量兜底 1 - List detailentry = new ArrayList<>(); - for (int index = 0; index < entryGroups.size(); index++) { - BfiEntryGroup entryGroup = entryGroups.get(index); - BfiApBillSaveEntryDTO entry = new BfiApBillSaveEntryDTO(); - // 【必填】物料编码.编码:结算明细货物类型匹配货物类型档案货物编码 - entry.setMaterialNumber(entryGroup.materialNumber()); - // 【必填】计量单位.编码:货物费用行运费计算单位(快照 price_unit,档案暂无编码字段暂传单位名称) - entry.setMeasureunitNumber(entryGroup.measureunitNumber()); - // 【必填】数量:该行运输数量,无吨位数据时兜底 1 - entry.setQuantity(entryGroup.quantity().compareTo(BigDecimal.ZERO) > 0 ? entryGroup.quantity() : BigDecimal.ONE); - // 明细.单价:该行运输单价(费用明细 unit_price;单据头 ispricetotal=true 录入总价,单价仅作参考) - entry.setPrice(entryGroup.price()); - // 应付金额(含税):该行应付货物费用行调整后总金额(含税) - entry.setEPricetaxtotal(entryGroup.amount()); - // 金额(不含税):该行结算金额(不含税)(应付费用行无不含税字段,经结算货物费用行回链) - entry.setEAmount(entryGroup.amountNoTax()); - // 【必填】项目.项目编码(联调期临时传金蝶测试档案值) - entry.setProjectNumber(KingdeeBfiConstant.BFI_AP_BILL_PROJECT_NUMBER_TEST); - // 【必填】外部系统来源分录标识(候选键):费用明细行id(唯一),A103-1 下推时 awrp_entry_code 与此对应 - entry.setAwrpEntryCode(entryGroup.entryCode()); - - // 调试 - - if (true) { - // 【必填】物料编码.编码:结算明细货物类型匹配货物类型档案货物编码 - entry.setMaterialNumber(KingdeeBfiConstant.BFI_AP_BILL_MATERIAL_NUMBER_TEST); - // 【必填】计量单位.编码:货物费用行运费计算单位(快照 price_unit,档案暂无编码字段暂传单位名称) - entry.setMeasureunitNumber(KingdeeBfiConstant.BFI_AP_BILL_MEASUREUNIT_NUMBER_TEST); - } - - // 分录备注(非必传) - // entry.setERemark(entity.getRemark()); - detailentry.add(entry); - } - param.setDetailentry(detailentry); - - // 付款计划 planentity 暂不传(联调待确认后启用):启用后送正式结算单下绑定、单据状态为审核通过(approved) - // 的付款信息,每条付款信息一行,awrp_entry_code_p 取付款单号(A103-1 下推 awrp_finapbill_entryid 勾稽此值, - // 取数见 approvedSettlementPayments);当前不传由金蝶按明细自动汇总生成,A103-1 推送前经 A102-4 反查 awrp_entry_code_p - // List approvedPayments = approvedSettlementPayments(entity, context); - // if (Func.isNotEmpty(approvedPayments)) { - // List planentity = new ArrayList<>(); - // for (FormalSettlementPayment payment : approvedPayments) { - // BfiApBillSavePlanDTO plan = new BfiApBillSavePlanDTO(); - // // 外部系统来源分录标识(候选键):付款单号 - // plan.setAwrpEntryCodeP(payment.getPaymentNo()); - // // 应付金额:付款信息申请付款金额 - // plan.setPlanpricetax(money(payment.getAppliedAmount())); - // // 【必填】项目.项目编码(联调期临时传金蝶测试档案值) - // plan.setPlanprojectNumber(KingdeeBfiConstant.BFI_AP_BILL_PROJECT_NUMBER_TEST); - // // 到期日:付款信息创建日期(yyyy-MM-dd) - // plan.setPlanduedate(payment.getCreateTime() == null ? null - // : payment.getCreateTime().toInstant().atZone(ZoneId.systemDefault()).toLocalDate().toString()); - // planentity.add(plan); - // planEntryCodes.add(payment.getPaymentNo()); - // } - // param.setPlanentity(planentity); - // } - // 付款计划未传时勾稽标识集合保持为空,A103-1 推送前经 A102-4 反查金蝶生成的 awrp_entry_code_p - List planEntryCodes = new ArrayList<>(); - // TODO 待业务确认:存在预付款抵扣时是否需传预付信息 preentry(y_billtype/y_billid/y_settleamt),本期不传、后续迭代 - - BfiBillSaveResultVO saveResult = bfiService.saveApBill(param); - // 应付单id不落库,后续如需可经 A102-4 按防重键(awrp_src_sys + awrp_src_code)查询 - requireBfiSaveSuccess(saveResult, "推送金蝶财务应付单"); - return new BfiApBillPushOutcome(saveResult.getResult().get(0).getNumber(), false, planEntryCodes); - } - - /** - * 推送金蝶付款申请(A103-1 应付下推付款申请,正式结算付款第二步),返回星瀚付款申请单id。 + * 推送金蝶付款申请(A103-1 应付下推付款申请,正式结算付款),返回星瀚付款申请单id。 * 付款申请分录与财务应付单明细分录一一对应,申请金额取各组含税金额; - * awrp_finapbill_entryid 勾稽 A102-2 付款计划分录 awrp_entry_code_p:付款计划暂不传,统一经 A102-4 反查 - * 金蝶自动生成的值;付款计划启用后优先取审核通过的付款信息单据号。awrp_entry_code 填费用明细行id + * awrp_finapbill_entryid 勾稽 A102-2 付款计划分录 awrp_entry_code_p:财务应付单(A102-2)由正式结算单 + * 「同步金蝶」推送且未传付款计划(planentity),统一经 A102-4 按各分录所属正式结算单的防重键反查 + * 金蝶自动生成的 awrp_entry_code_p(多结算单付款申请各分录勾稽各自应付单);awrp_entry_code 填费用明细行id * * @param entity 付款申请实体 - * @param entryGroups 结算明细解析出的应付单分录 - * @param planEntryCodes A102-2 付款计划分录 awrp_entry_code_p 集合(付款信息单据号),空表示未传付款计划 + * @param entryGroups 结算明细解析出的应付单分录(含分录所属正式结算单id) * @param context 金蝶同步上下文(缓存载体) * @return 星瀚付款申请单id */ - private String pushBfiPayApplyPush(PaymentApplication entity, List entryGroups, List planEntryCodes, KingdeeSyncContext context) { - ContractManage contract = requireContract(entity, context); - String orgNumber = bfiOrgNumber(contract); + private String pushBfiPayApplyPush(PaymentApplication entity, List entryGroups, KingdeeSyncContext context) { + ContractManage contract = kingdeeApBillSyncSupport.requireContract(entity.getContractId(), context); + String orgNumber = kingdeeApBillSyncSupport.bfiOrgNumber(contract); // 申请日期:yyyy-MM-dd HH:mm:ss(与 A102-2 单据日期格式统一) String applyDateText = bfiDateTimeText(entity); - // 【必填】结算单分录id勾稽值(awrp_finapbill_entryid):优先取审核通过的付款信息单据号(多条下划线拼接), - // 无审核通过付款信息时经 A102-4 反查金蝶自动生成的付款计划 awrp_entry_code_p - String finapbillEntryid; - if (Func.isNotEmpty(planEntryCodes)) { - finapbillEntryid = planEntryCodes.size() == 1 ? planEntryCodes.get(0) : String.join("_", planEntryCodes); - } else { - finapbillEntryid = apBillPlanEntryCode(entity, apBillSrcCode(entity, context), context); - } BfiPayApplyPushDTO param = new BfiPayApplyPushDTO(); // 【必填】外部系统来源单据号(候选键):付款申请号 param.setAwrpSrcCode(entity.getPaymentNo()); @@ -1241,8 +949,11 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl entries = new ArrayList<>(); - for (int index = 0; index < entryGroups.size(); index++) { - BfiEntryGroup entryGroup = entryGroups.get(index); + for (KingdeeApBillSyncSupport.BfiEntryGroup entryGroup : entryGroups) { + // 【必填】结算单分录id勾稽值(awrp_finapbill_entryid):按分录所属正式结算单的财务应付单, + // 经 A102-4 反查其付款计划分录 awrp_entry_code_p(按防重键缓存,同一结算单分录只反查一次) + String finapbillEntryid = kingdeeApBillSyncSupport.apBillPlanEntryCode( + settlementApBillSrcCode(entity, entryGroup.formalSettlementId(), context), entity.getPaymentNo(), context); BfiPayApplyPushEntryDTO entry = new BfiPayApplyPushEntryDTO(); // 【必填】付款类型.编码:正式付款固定 201 entry.setEPaymenttypeNumber(KingdeeBfiConstant.BFI_PAYMENT_TYPE_FORMAL); @@ -1269,99 +980,7 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl entryGroups, List documentNos) { - } - - /** - * 财务应付单(A102-2)推送结果 - * - * @param billNo 财务应付单单据编号(金蝶已存在时返回已有单据编号) - * @param alreadyExist 金蝶是否已存在该防重键的财务应付单,true 表示本次幂等跳过保存(上次同步已在后续环节中断) - * @param planEntryCodes 付款计划分录外部系统来源分录标识(awrp_entry_code_p,取审核通过的付款信息单据号), - * 空表示 A102-2 未传付款计划(A103-1 推送前需经 A102-4 反查) - */ - private record BfiApBillPushOutcome(String billNo, boolean alreadyExist, List planEntryCodes) { - } - - /** - * 解析付款申请关联的正式结算明细,逐行生成应付单分录(不聚合,每条来源费用行一条分录)。 - *

取数来源:应付明细货物费用行(blade_receivable_payable_cargo_fee,经结算明细 sourceDetailId 关联应付明细, - * 每货物一条)——货物类型/计量单位/运输数量/含税金额直接取应付费用行(含税金额取调整后总金额 after_amount); - * 应付费用行无不含税字段,不含税金额经结算货物费用行 sourceFeeId 回链(直接选明细路径 sourceFeeId=应付费用行id, - * 预结算合并路径 sourceFeeId=预结算费用行id,经预结算费用行 sourceFeeId 回溯应付费用行id); - * 结算明细对应应付明细无货物费用行时直接报错拦截(取数必须来自应付费用明细,无兜底)。物料编码:货物类型(支持“、”分隔多类型) - * 匹配货物类型档案货物编码;分录唯一标识(awrp_entry_code)取费用明细行id(雪花id天然唯一,满足 A103-1 - * 对方系统唯一行id不能重复;A103-1 与应付单的勾稽关联走 awrp_finapbill_entryid=付款计划 - * awrp_entry_code_p,与本标识无关);分录金额不按申请付款金额分摊。

- * - * @param entity 付款申请实体 - * @return 应付单分录解析结果(分录 + 结算明细单据号) - */ - private BfiEntryGroupResult resolveBfiEntryGroups(PaymentApplication entity, KingdeeSyncContext context) { - List settlementDetails = settlementDetails(entity, context); - if (Func.isEmpty(settlementDetails)) { - throw new ServiceException("推送金蝶财务应付单失败:正式结算单无结算明细,无法解析物料与计量单位"); - } - // 去重收集结算明细单据号(保持行序),用于拼接财务应付单外部系统来源单据号 - List documentNos = settlementDocumentNos(settlementDetails); - Map cargoCodeByName = cargoCodeByName(context); - // 应付明细id->应付货物费用行(分录取数来源,无兜底) - Map> payableFeesByDetailId = payableCargoFeesByDetailId(settlementDetails); - // 应付费用行id->结算金额(不含税):应付费用行无不含税字段,经结算货物费用行 sourceFeeId 回链 - Map noTaxByPayableFeeId = settlementNoTaxByPayableFeeId(settlementDetails); - List entryGroups = new ArrayList<>(); - for (FormalSettlementDetail settlementDetail : settlementDetails) { - List payableFees = - payableFeesByDetailId.getOrDefault(settlementDetail.getSourceDetailId(), List.of()); - if (Func.isEmpty(payableFees)) { - throw new ServiceException("推送金蝶财务应付单失败:结算明细[" + Func.toStr(settlementDetail.getDocumentNo()) - + "]对应应付明细无货物费用行,分录取数必须来自应付费用明细,请先维护费用数据"); - } - for (ReceivablePayableCargoFee payableFee : payableFees) { - appendEntryGroups(entryGroups, cargoCodeByName, payableFee.getCargoType(), - resolveMeasureunitNumber(payableFee.getPriceUnit(), payableFee.getQuantityUnit()), - payableFee.getId(), money(payableFee.getUnitPrice()), - money(payableFee.getAfterAmount()), - noTaxByPayableFeeId.getOrDefault(payableFee.getId(), BigDecimal.ZERO), - money(payableFee.getTransportQuantity()), - "应付货物费用[" + Func.toStr(payableFee.getCargoName()) + " 第" + Func.toStr(payableFee.getLineNo()) + "行]"); - } - } - return new BfiEntryGroupResult(entryGroups, documentNos); - } - - /** 结算明细单据号去重收集(保持行序),用于拼接财务应付单外部系统来源单据号 */ - private List settlementDocumentNos(List settlementDetails) { - return settlementDetails.stream() - .map(FormalSettlementDetail::getDocumentNo) - .filter(Func::isNotBlank) - .map(String::trim) - .distinct() - .toList(); + return kingdeeApBillSyncSupport.requireBfiBillId(saveResult, "推送金蝶付款申请(应付下推)"); } /** 结算明细(上下文缓存优先):批量预检加载过的明细推送时直接复用,消除逐单重复查询 */ @@ -1369,15 +988,48 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl formalSettlementDetails(entity)); } - /** 财务应付单防重键来源单据号:付款申请号 + 结算明细单据号下划线拼接(与 A102-2 保存传参一致) */ - private String apBillSrcCode(PaymentApplication entity, KingdeeSyncContext context) { - return ExternalSrcCodeSupport.buildSrcCode(entity.getPaymentNo(), settlementDocumentNos(settlementDetails(entity, context))); + /** + * 付款申请关联各正式结算单的财务应付单防重键集合(结算单id -> awrp_src_code), + * 防重键规则:正式结算单号_结算明细单据号下划线拼接(与正式结算单「同步金蝶」推送 A102-2 传参一致) + * + * @param entity 付款申请实体 + * @param context 金蝶同步上下文(缓存载体) + * @return 结算单id -> 财务应付单防重键 + */ + private Map settlementApBillSrcCodes(PaymentApplication entity, KingdeeSyncContext context) { + List settlementIds = settlementDetails(entity, context).stream() + .map(FormalSettlementDetail::getFormalSettlementId).filter(Objects::nonNull).distinct().toList(); + Map srcCodes = new LinkedHashMap<>(); + for (Long settlementId : settlementIds) { + srcCodes.put(settlementId, settlementApBillSrcCode(entity, settlementId, context)); + } + return srcCodes; + } + + /** + * 单张正式结算单的财务应付单防重键:正式结算单号_结算明细单据号下划线拼接, + * 结算单号经上下文缓存复用,结算明细取付款申请关联明细按结算单过滤 + * + * @param entity 付款申请实体 + * @param formalSettlementId 正式结算单id + * @param context 金蝶同步上下文(缓存载体) + * @return 财务应付单防重键 + */ + private String settlementApBillSrcCode(PaymentApplication entity, Long formalSettlementId, KingdeeSyncContext context) { + FormalSettlement settlement = context.settlementCache().computeIfAbsent(formalSettlementId, id -> { + FormalSettlement found = formalSettlementMapper.selectById(id); + if (found == null) throw new ServiceException("推送金蝶付款申请失败:正式结算单[" + id + "]不存在"); + return found; + }); + List ownDetails = settlementDetails(entity, context).stream() + .filter(detail -> Objects.equals(formalSettlementId, detail.getFormalSettlementId())).toList(); + return kingdeeApBillSyncSupport.apBillSrcCode(settlement, ownDetails); } /** * 金蝶分录标识:付款申请号-分录序号(1 起)。 * 现仅 A103-3 预付款直接保存路径使用(序号固定 1);正式结算链路 A102-2 明细 awrp_entry_code 与 - * A103-1 awrp_entry_code 已改为费用明细行id(resolveBfiEntryGroups 统一解析), + * A103-1 awrp_entry_code 已改为费用明细行id(KingdeeApBillSyncSupport.resolveEntryGroups 统一解析), * A103-1 awrp_finapbill_entryid 改为财务应付单付款计划 awrp_entry_code_p(A102-4 反查),禁止各接口自行拼接 * * @param paymentNo 付款申请号 @@ -1389,9 +1041,9 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl pendingCodes = srcCodes.stream() @@ -1420,112 +1072,11 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl context.apBillNoCache().putIfAbsent(code, "")); } - /** - * 单条应付货物费用行生成不聚合分录(每行一条); - * 一行含多种货物类型时按类型拆为多条,金额与数量按份数均分(尾差计入最后一份) - * - * @param entryGroups 分录集合(保持行序) - * @param cargoCodeByName 货物类型名称->货物编码映射 - * @param cargoType 货物类型(支持“、”分隔多类型) - * @param measureunitNumber 计量单位编码(运费计算单位,档案暂无编码字段暂传单位名称) - * @param sourceRowId 应付货物费用行id,雪花id天然唯一 - * @param price 该行运输单价(费用明细 unit_price) - * @param amount 该行金额(含税) - * @param amountNoTax 该行金额(不含税) - * @param quantity 该行运输数量 - * @param sourceDesc 来源行描述,用于异常提示 - */ - private void appendEntryGroups(List entryGroups, Map cargoCodeByName, - String cargoType, String measureunitNumber, Long sourceRowId, BigDecimal price, - BigDecimal amount, BigDecimal amountNoTax, BigDecimal quantity, String sourceDesc) { - List cargoNames = splitCargoTypes(cargoType); - if (Func.isBlank(measureunitNumber)) { - throw new ServiceException("推送金蝶财务应付单失败:" + sourceDesc + "计量单位为空,请先维护运费计算单位"); - } - // 分录唯一标识(awrp_entry_code,A103-1 要求对方系统唯一行id不能重复):雪花id天然唯一,同行多类型拆分时追加序号 - String rowEntryCode = String.valueOf(sourceRowId); - List amountShares = splitEqually(amount, cargoNames.size()); - List amountNoTaxShares = splitEqually(amountNoTax, cargoNames.size()); - List quantityShares = splitEqually(quantity, cargoNames.size()); - for (int index = 0; index < cargoNames.size(); index++) { - String cargoName = cargoNames.get(index); - String materialNumber = cargoCodeByName.get(cargoName); - if (Func.isBlank(materialNumber)) { - throw new ServiceException("推送金蝶财务应付单失败:" + sourceDesc + "货物类型[" + cargoName + "]未在货物类型档案中维护编码,请补充后重试"); - } - String entryCode = cargoNames.size() > 1 ? rowEntryCode + "-" + (index + 1) : rowEntryCode; - entryGroups.add(new BfiEntryGroup(materialNumber, measureunitNumber, entryCode, - quantityShares.get(index), price, amountShares.get(index), amountNoTaxShares.get(index))); - log.info("金蝶应付单分录生成:来源[{}],分录标识[{}],物料编码[{}],计量单位[{}],数量[{}],单价[{}],含税金额[{}],不含税金额[{}]", - sourceDesc, entryCode, materialNumber, measureunitNumber, - quantityShares.get(index), price, amountShares.get(index), amountNoTaxShares.get(index)); - } - } - - /** 付款申请关联的正式结算货物费用行(blade_formal_settlement_detail_fee,用于不含税金额回链应付费用行) */ - private List formalSettlementDetailFees(List settlementDetails) { - List settlementDetailIds = settlementDetails.stream().map(FormalSettlementDetail::getId) - .filter(Objects::nonNull).distinct().toList(); - if (Func.isEmpty(settlementDetailIds)) { - return List.of(); - } - return formalSettlementDetailFeeMapper.selectList(Wrappers.lambdaQuery() - .in(FormalSettlementDetailFee::getFormalSettlementDetailId, settlementDetailIds) - .eq(FormalSettlementDetailFee::getIsDeleted, 0) - .orderByAsc(FormalSettlementDetailFee::getLineNo)); - } - - /** 应付明细id->应付货物费用行(blade_receivable_payable_cargo_fee,经结算明细 sourceDetailId 关联,每货物一条,分录主取数来源) */ - private Map> payableCargoFeesByDetailId(List settlementDetails) { - List sourceDetailIds = settlementDetails.stream() - .map(FormalSettlementDetail::getSourceDetailId).filter(Objects::nonNull).distinct().toList(); - if (Func.isEmpty(sourceDetailIds)) { - return Map.of(); - } - return receivablePayableCargoFeeMapper.selectList(Wrappers.lambdaQuery() - .in(ReceivablePayableCargoFee::getDetailId, sourceDetailIds) - .eq(ReceivablePayableCargoFee::getIsDeleted, 0) - .orderByAsc(ReceivablePayableCargoFee::getLineNo)) - .stream().collect(Collectors.groupingBy(ReceivablePayableCargoFee::getDetailId)); - } - - /** - * 应付货物费用行id->结算金额(不含税):应付费用行无不含税字段,经结算货物费用行 sourceFeeId 回链获取。 - * 直接选应收应付明细路径 sourceFeeId=应付费用行id;预结算合并路径 sourceFeeId=预结算费用行id, - * 经预结算费用行 sourceFeeId(指向应付费用行)回溯;无对应结算费用行时无映射,调用方兜底 0 - */ - private Map settlementNoTaxByPayableFeeId(List settlementDetails) { - List settlementFees = formalSettlementDetailFees(settlementDetails); - if (Func.isEmpty(settlementFees)) { - return Map.of(); - } - // 上游费用行id(应付费用行id或预结算费用行id)-> 不含税合计 - Map noTaxBySourceFeeId = new LinkedHashMap<>(); - for (FormalSettlementDetailFee settlementFee : settlementFees) { - if (settlementFee.getSourceFeeId() == null) { - continue; - } - noTaxBySourceFeeId.merge(settlementFee.getSourceFeeId(), money(settlementFee.getSettlementAmountNoTax()), BigDecimal::add); - } - if (noTaxBySourceFeeId.isEmpty()) { - return Map.of(); - } - // sourceFeeId 指向预结算费用行的:经预结算费用行回溯应付费用行id(直接路径无预结算费用行,保持原 id) - Map payableFeeIdByPreFeeId = preSettlementDetailFeeMapper.selectList( - Wrappers.lambdaQuery().in(PreSettlementDetailFee::getId, new ArrayList<>(noTaxBySourceFeeId.keySet()))) - .stream().filter(preSettlementFee -> preSettlementFee.getSourceFeeId() != null) - .collect(Collectors.toMap(PreSettlementDetailFee::getId, PreSettlementDetailFee::getSourceFeeId, (left, right) -> left)); - Map noTaxByPayableFeeId = new LinkedHashMap<>(); - noTaxBySourceFeeId.forEach((sourceFeeId, noTax) -> - noTaxByPayableFeeId.merge(payableFeeIdByPreFeeId.getOrDefault(sourceFeeId, sourceFeeId), noTax, BigDecimal::add)); - return noTaxByPayableFeeId; - } - /** 付款申请关联的正式结算明细(关联表优先,兼容仅有 settlementId 的存量数据) */ private List formalSettlementDetails(PaymentApplication entity) { List settlementIds = settlementRelationMapper.selectList(Wrappers.lambdaQuery() @@ -1544,134 +1095,6 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl货物编码映射(取自货物类型档案;名称重复且编码不一致时拦截;批量同步时全批共享一次加载结果) */ - private Map cargoCodeByName(KingdeeSyncContext context) { - Map cache = context.cargoCodeByName(); - if (!cache.isEmpty()) { - return cache; - } - R> response = sysClient.getCargoTypes(); - if (response == null || !response.isSuccess() || Func.isEmpty(response.getData())) { - throw new ServiceException("推送金蝶财务应付单失败:货物类型档案读取失败,请稍后重试"); - } - Map loaded = new HashMap<>(); - for (CargoType cargoType : response.getData()) { - if (cargoType == null || Func.isBlank(cargoType.getCargoName()) || Func.isBlank(cargoType.getCargoCode())) { - continue; - } - String cargoName = cargoType.getCargoName().trim(); - String cargoCode = cargoType.getCargoCode().trim(); - String existingCode = loaded.putIfAbsent(cargoName, cargoCode); - if (existingCode != null && !existingCode.equals(cargoCode)) { - throw new ServiceException("推送金蝶财务应付单失败:货物类型[" + cargoName + "]在货物类型档案中存在多个编码,请检查档案数据"); - } - } - cache.putAll(loaded); - return cache; - } - - /** - * 解析计量单位编码:优先取快照运费计算单位(快照表 price_unit,随结算自动带出), - * 为空(历史快照)时兜底数量单位。计量单位档案暂无编码字段,暂以单位名称作为编码; - * 档案补充编码字段后在此处增加档案映射。 - * - * @param priceUnit 快照运费计算单位(可能为空) - * @param fallbackUnit 兜底数量单位 - * @return 计量单位编码,无法解析时返回 null - */ - private String resolveMeasureunitNumber(String priceUnit, String fallbackUnit) { - String unitName = Func.isNotBlank(priceUnit) ? priceUnit : fallbackUnit; - return Func.isBlank(unitName) ? null : unitName.trim(); - } - - /** 货物类型支持“、/,”分隔的多类型写法,拆分为单类型名称 */ - private List splitCargoTypes(String cargoType) { - if (Func.isBlank(cargoType)) { - throw new ServiceException("推送金蝶财务应付单失败:结算明细货物类型为空,无法解析物料编码"); - } - return Arrays.stream(cargoType.split("[、,,]")) - .map(String::trim).filter(Func::isNotBlank).distinct().toList(); - } - - /** 数值按份数均分(保留 2 位小数、向下取整,尾差计入最后一份) */ - private List splitEqually(BigDecimal total, int parts) { - if (parts <= 1) { - return List.of(total); - } - List shares = new ArrayList<>(parts); - BigDecimal remaining = total; - for (int index = 0; index < parts; index++) { - if (index == parts - 1) { - shares.add(remaining); - } else { - BigDecimal share = total.divide(BigDecimal.valueOf(parts), 2, RoundingMode.DOWN); - shares.add(share); - remaining = remaining.subtract(share); - } - } - return shares; - } - - /** - * 校验金蝶保存/下推返回结果(单据级失败以 errors 明细抛出) - * - * @param saveResult 金蝶返回结果 - * @param action 业务动作描述,用于异常提示 - */ - private void requireBfiSaveSuccess(BfiBillSaveResultVO saveResult, String action) { - if (saveResult == null || Func.isEmpty(saveResult.getResult())) { - throw new ServiceException(action + "失败,金蝶未返回单据结果"); - } - BfiBillSaveResultItemVO item = saveResult.getResult().get(0); - if (!Boolean.TRUE.equals(item.getBillStatus())) { - String errorText = Func.isBlank(item.getErrorText()) ? "未知错误" : item.getErrorText(); - throw new ServiceException(action + "失败:" + errorText); - } - } - - /** - * 校验金蝶保存/下推返回结果并取星瀚单据id(单据级失败以 errors 明细抛出) - * - * @param saveResult 金蝶返回结果 - * @param action 业务动作描述,用于异常提示 - * @return 星瀚单据id - */ - private String requireBfiBillId(BfiBillSaveResultVO saveResult, String action) { - requireBfiSaveSuccess(saveResult, action); - Object billId = saveResult.getResult().get(0).getId(); - if (billId == null) throw new ServiceException(action + "失败,金蝶未返回单据id"); - return String.valueOf(billId); - } - - private ContractManage requireContract(PaymentApplication entity, KingdeeSyncContext context) { - if (entity.getContractId() == null) throw new ServiceException("推送金蝶付款申请失败,付款申请未关联合同"); - // 批量同步时按合同id缓存复用(同一合同只查一次),单笔同步退化为直接查询 - ContractManage contract = context.contractCache().computeIfAbsent(entity.getContractId(), contractManageMapper::selectById); - if (contract == null || Objects.equals(contract.getIsDeleted(), 1)) throw new ServiceException("推送金蝶付款申请失败,关联合同不存在"); - return contract; - } - - /** - * 合同所属组织编码 - * @param contract - * @return - */ - private String bfiOrgNumber(ContractManage contract) { - Dept dept = contract.getOrganizationId() == null ? null : SysCache.getDept(contract.getOrganizationId()); - // dept为空 或者 deptCode为空,直接报错 - if (dept == null || Func.isBlank(dept.getDeptCode())) { - throw new ServiceException("推送金蝶付款申请失败,合同所属组织编码不能为空,请检查部门ID【"+contract.getOrganizationId()+"】对应的部门是否维护部门编码"); - } - return dept.getDeptCode(); - } - - private String bfiCurrency(ContractManage contract) { - if (Func.isBlank(contract.getSettlementCurrency())) throw new ServiceException("推送金蝶付款申请失败,合同结算币种不能为空"); - // 金蝶币别代码为 CNY,合同结算币种维护为 RMB 时统一映射 - String currency = contract.getSettlementCurrency(); - return "RMB".equalsIgnoreCase(currency) ? "CNY" : currency; - } - private String bfiApplyCause(String paymentType) { return switch (paymentType == null ? "" : paymentType) { case "project_advance" -> "项目预付款申请"; @@ -1688,33 +1111,6 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpllambdaQuery() - .and(wrapper -> wrapper.eq(CustomerArchive::getFullName, partyB).or().eq(CustomerArchive::getShortName, partyB)) - .eq(CustomerArchive::getIsDeleted, 0).last("limit 1")); - return customer == null ? null : customer.getCustomerCode(); - } - private String bfiProjectNumber(PaymentApplication entity, KingdeeSyncContext context) { if (entity.getProjectId() == null) return KingdeeBfiConstant.BFI_PROJECT_NUMBER_NONE; // 批量同步时按项目id缓存复用(同一项目只查一次),单笔同步退化为直接查询 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 new file mode 100644 index 00000000..caa2a279 --- /dev/null +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/support/KingdeeApBillSyncSupport.java @@ -0,0 +1,708 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.transport.support; + +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springblade.core.log.exception.ServiceException; +import org.springblade.core.tool.api.R; +import org.springblade.core.tool.utils.Func; +import org.springblade.system.cache.SysCache; +import org.springblade.system.feign.ISysClient; +import org.springblade.system.pojo.entity.CargoType; +import org.springblade.system.pojo.entity.Dept; +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.vo.BfiApBillPlanVO; +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.transport.constant.KingdeeBfiConstant; +import org.springblade.transport.mapper.ContractManageMapper; +import org.springblade.transport.mapper.CustomerArchiveMapper; +import org.springblade.transport.mapper.FormalSettlementDetailFeeMapper; +import org.springblade.transport.mapper.PreSettlementDetailFeeMapper; +import org.springblade.transport.mapper.ReceivablePayableCargoFeeMapper; +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.PreSettlementDetailFee; +import org.springblade.transport.pojo.entity.ReceivablePayableCargoFee; +import org.springblade.transport.service.KingdeeSyncContext; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.function.Consumer; +import java.util.stream.Collectors; + +/** + * 金蝶财务应付单(A102-2)同步支撑组件。 + *

流程拆分后:财务应付单(A102-2)由正式结算单「同步金蝶」推送并回写结算单; + * 付款申请「同步金蝶」仅负责付款申请单(A103-1 应付下推 / A103-3 直接保存), + * 推送前经 A102-4 预检应付单已生成、下推时经 A102-4 反查付款计划分录 awrp_entry_code_p。 + * 两侧共用的防重键组装、A102-4 查询/反查、A102-2 保存、结算明细分录解析与档案取数统一收口在本组件。

+ * + * @author Chill + */ +@Slf4j +@Component +@RequiredArgsConstructor +public class KingdeeApBillSyncSupport { + + /** A102-4 防重键批量分批查询单次条数上限(与 A103-2 批量查询一致) */ + private static final int PAY_RESULT_QUERY_BATCH = 50; + /** A102-4 分页查询页大小 */ + private static final int PAY_RESULT_PAGE_SIZE = 200; + + private final IBfiService bfiService; + private final ContractManageMapper contractManageMapper; + private final CustomerArchiveMapper customerArchiveMapper; + private final FormalSettlementDetailFeeMapper formalSettlementDetailFeeMapper; + private final PreSettlementDetailFeeMapper preSettlementDetailFeeMapper; + private final ReceivablePayableCargoFeeMapper receivablePayableCargoFeeMapper; + private final ISysClient sysClient; + @Value("${thirdParty.bfi.payApply.srcSys:WLTMS}") + private String bfiPayApplySrcSys; + + // ==================== 财务应付单防重键(awrp_src_code) ==================== + + /** + * 结算明细单据号去重收集(保持行序),用于拼接财务应付单外部系统来源单据号 + * + * @param settlementDetails 结算明细列表 + * @return 单据号列表(去空白、去重、保持行序) + */ + public List settlementDocumentNos(List settlementDetails) { + return settlementDetails.stream() + .map(FormalSettlementDetail::getDocumentNo) + .filter(Func::isNotBlank) + .map(String::trim) + .distinct() + .toList(); + } + + /** + * 财务应付单防重键(awrp_src_code):正式结算单号 + 结算明细单据号下划线拼接(与 A102-2 保存传参一致)。 + * 流程拆分后 A102-2 由正式结算单推送,防重键以正式结算单号为主,一张结算单只生成一张应付单, + * 同一结算单下多笔付款申请(A103-1)共享该应付单 + * + * @param settlement 正式结算单 + * @param settlementDetails 该结算单下的结算明细 + * @return 防重键 + */ + public String apBillSrcCode(FormalSettlement settlement, List settlementDetails) { + return ExternalSrcCodeSupport.buildSrcCode(settlement.getFormalSettlementNo(), settlementDocumentNos(settlementDetails)); + } + + // ==================== A102-4 财务应付单查询 ==================== + + /** + * A102-4 批量查询已生成财务应付单:来源单据号批量 + 外部系统编号(防重键),自动翻页, + * 用于推送前幂等预检与应付单是否已生成前置校验(awrp_src_code 支持多值,整批一次查询消除逐单远程查询 N+1) + * + * @param awrpSrcCodes 来源单据号(正式结算单号_结算明细单据号拼接)批次 + * @return 已生成财务应付单的 来源单据号->单据编号 映射 + */ + public Map queryApBillExistNos(List awrpSrcCodes) { + Map existBillNos = new LinkedHashMap<>(); + // 防重键批量分批查询,避免单次过滤条件过长 + batchForeach(awrpSrcCodes, PAY_RESULT_QUERY_BATCH, batch -> { + BfiApBillQueryDTO param = new BfiApBillQueryDTO(); + param.setAwrpSrcCode(batch); + param.setAwrpSrcSys(bfiPayApplySrcSys); + for (BfiApBillVO apBill : BfiUtil.queryAllPages(pageNo -> { + param.setPageNo(pageNo); + param.setPageSize(PAY_RESULT_PAGE_SIZE); + return bfiService.queryApBill(param); + })) { + if (Func.isNotEmpty(apBill.getAwrpSrcCode())) { + existBillNos.put(apBill.getAwrpSrcCode(), apBill.getBillno()); + } + } + }); + return existBillNos; + } + + /** + * A102-4 反查财务应付单付款计划分录的外部系统来源分录标识(awrp_entry_code_p)。 + * A102-2 保存未传 planentity,由金蝶按明细自动汇总生成(awrp_entry_code_p 不传时自动取外部系统单据号), + * A103-1 应付下推的 awrp_finapbill_entryid 需填该值勾稽应付单; + * 结果按防重键(awrp_src_code)缓存,恢复场景避免重复远程查询 + * + * @param apBillSrcCode 财务应付单防重键(外部系统来源单据号) + * @param subject 业务单据号(付款申请号,异常提示用) + * @param context 金蝶同步上下文(缓存载体) + * @return 付款计划分录 awrp_entry_code_p + */ + public String apBillPlanEntryCode(String apBillSrcCode, String subject, KingdeeSyncContext context) { + return context.apBillPlanCodeCache().computeIfAbsent(apBillSrcCode, code -> { + BfiApBillQueryDTO param = new BfiApBillQueryDTO(); + param.setAwrpSrcCode(List.of(code)); + param.setAwrpSrcSys(bfiPayApplySrcSys); + List bills = BfiUtil.queryAllPages(pageNo -> { + param.setPageNo(pageNo); + param.setPageSize(PAY_RESULT_PAGE_SIZE); + return bfiService.queryApBill(param); + }); + for (BfiApBillVO apBill : bills) { + if (Func.isNotEmpty(apBill.getPlanentity())) { + for (BfiApBillPlanVO plan : apBill.getPlanentity()) { + if (Func.isNotBlank(plan.getAwrpEntryCodeP())) { + return plan.getAwrpEntryCodeP(); + } + } + } + } + throw new ServiceException("推送金蝶付款申请(应付下推)失败:财务应付单[" + subject + "]付款计划分录 awrp_entry_code_p 未查询到,无法勾稽关联"); + }); + } + + // ==================== A102-2 财务应付单保存 ==================== + + /** + * 推送金蝶财务应付单(A102-2 财务应付单保存,由正式结算单「同步金蝶」触发)。 + * 推送前先经 A102-4 按防重键(awrp_src_sys + awrp_src_code)查询:金蝶已存在财务应付单时 + * 幂等跳过保存并返回已有单据编号(说明上次同步已保存成功、在后续环节中断,应续推而非拦截报错)。 + * 明细分录逐费用行生成(不聚合),含税金额取应付货物费用行调整后总金额、不含税取结算货物费用行不含税; + * 付款计划 planentity 暂不传(启用方案见方法内注释),当前由金蝶按明细自动汇总生成 + * + * @param apBillSrcCode 财务应付单防重键(正式结算单号_结算明细单据号拼接) + * @param contractId 正式结算单关联合同id + * @param entryGroups 结算明细解析出的应付单分录 + * @param subject 业务单据号(正式结算单号,日志与异常提示用) + * @param context 金蝶同步上下文(缓存载体) + * @return 财务应付单推送结果(单据编号 + 是否金蝶已存在幂等跳过) + */ + public BfiApBillPushOutcome pushApBill(String apBillSrcCode, Long contractId, List entryGroups, + String subject, KingdeeSyncContext context) { + // A102-4 幂等预检:按防重键查询金蝶是否已生成财务应付单(批量同步时整批已预热缓存,此处仅懒加载未命中编码), + // 已存在则幂等跳过保存,直接返回已有单据编号 + String existBillNo = context.apBillNoCache().computeIfAbsent(apBillSrcCode, code -> + queryApBillExistNos(List.of(code)).getOrDefault(code, "")); + if (Func.isNotEmpty(existBillNo)) { + log.info("正式结算单[{}]金蝶财务应付单已生成(单据编号[{}]),幂等跳过 A102-2 保存", subject, existBillNo); + return new BfiApBillPushOutcome(existBillNo, true); + } + ContractManage contract = requireContract(contractId, context); + // 单据类型按映射字典转换:默认服务采购应付,金蝶确认其他类型后修改 BFI_AP_BILL_BILLTYPE_NAME 即可 + String billtypeNumber = KingdeeBfiConstant.billtypeBy(KingdeeBfiConstant.AP_BILL_BILLTYPE_MAP, KingdeeBfiConstant.BFI_AP_BILL_BILLTYPE_NAME); + if (Func.isBlank(billtypeNumber)) { + throw new ServiceException("推送金蝶财务应付单失败:单据类型[" + KingdeeBfiConstant.BFI_AP_BILL_BILLTYPE_NAME + "]未在映射字典中维护"); + } + String orgNumber = bfiOrgNumber(contract); + // 币种取合同结算币种 + String currency = bfiCurrency(contract); + // 单据日期:yyyy-MM-dd HH:mm:ss(各接口统一格式),流程拆分后以同步当日作为应付单据日期 + String billDateText = bfiDateTimeText(LocalDate.now()); + BfiApBillSaveDTO param = new BfiApBillSaveDTO(); + // 【必填】单据类型.编码 + param.setBilltypeidNumber(billtypeNumber); + // 【必填】结算组织.编码 + param.setOrgNumber(orgNumber); + // 【必填】单据日期 + param.setBizdate(billDateText); + // 【必填】往来类型:合同类别为“客户合同”时为 bd_customer(客户),否则 bd_supplier(供应商) + param.setAsstacttype(bfiAsstactType(contract)); + // 【必填】往来户.编码:合同乙方关联客商档案客商编码 + param.setAsstactNumber(bfiAsstactNumber(contract, context)); + // 【必填】付款方式(CASH:现购, CREDIT:赊购) + param.setPurmode(KingdeeBfiConstant.BFI_AP_BILL_PUR_MODE_CASH); + // 【必填】结算币别.货币代码 + param.setCurrencyNumber(currency); + // 【必填】付款组织.编码 + param.setPayorgNumber(orgNumber); + // 采购组织.编码(非必传) + // param.setPurorgNumber(orgNumber); + // 【必填】款项性质.编码 + param.setPaypropertyNumber(KingdeeBfiConstant.BFI_AP_BILL_PAY_PROPERTY); + // TODO 待确认:结算方式编码(settlementtype_number)与税率编码(taxrateid_number)需金蝶侧确认后补充,当前不传 + // 录入含税价(非必传) + param.setIsincludetax(true); + // 录入总价,为 true 后可不录入单价(非必传) + param.setIspricetotal(true); + param.setIsintax(false); + // 汇率表.编码(非必传) + // param.setExratetableNumber(bfiPayApplyExratetable); + // 汇率日期(非必传) + // param.setExratedate(billDateText); + // 往来单位名称(非必传) + // param.setAsstactname(entity.getPayeeName()); + // 备注(非必传) + // param.setRemark(subject); + // 【必填】外部系统编号(候选键) + param.setAwrpSrcSys(bfiPayApplySrcSys); + // 【必填】外部系统来源单据号(候选键):正式结算单号_结算明细单据号1_单据号2...(复用预检已组装的防重键) + param.setAwrpSrcCode(apBillSrcCode); + // 来源单据(非必传,有上游合同时传 awrp_contract_info) + // param.setAwrpSrcBt(KingdeeBfiConstant.BFI_CONTRACT_SRC_BT); + // 上游外部系统单据号/上游合同号(非必传) + // if (Func.isNotEmpty(contract.getContractNo())) { + // param.setAwrpSrcConNo(contract.getContractNo()); + // } + // 【必填】外部单据类型 + param.setAwrpSrcType(KingdeeBfiConstant.BFI_AP_BILL_SRC_TYPE); + + // 明细分录:每条应付货物费用行一条(不聚合),无吨位时数量兜底 1 + List detailentry = new ArrayList<>(); + for (BfiEntryGroup entryGroup : entryGroups) { + BfiApBillSaveEntryDTO entry = new BfiApBillSaveEntryDTO(); + // 【必填】物料编码.编码:结算明细货物类型匹配货物类型档案货物编码 + entry.setMaterialNumber(entryGroup.materialNumber()); + // 【必填】计量单位.编码:货物费用行运费计算单位(快照 price_unit,档案暂无编码字段暂传单位名称) + entry.setMeasureunitNumber(entryGroup.measureunitNumber()); + // 【必填】数量:该行运输数量,无吨位数据时兜底 1 + entry.setQuantity(entryGroup.quantity().compareTo(BigDecimal.ZERO) > 0 ? entryGroup.quantity() : BigDecimal.ONE); + // 明细.单价:该行运输单价(费用明细 unit_price;单据头 ispricetotal=true 录入总价,单价仅作参考) + entry.setPrice(entryGroup.price()); + // 应付金额(含税):该行应付货物费用行调整后总金额(含税) + entry.setEPricetaxtotal(entryGroup.amount()); + // 金额(不含税):该行结算金额(不含税)(应付费用行无不含税字段,经结算货物费用行回链) + entry.setEAmount(entryGroup.amountNoTax()); + // 【必填】项目.项目编码(联调期临时传金蝶测试档案值) + entry.setProjectNumber(KingdeeBfiConstant.BFI_AP_BILL_PROJECT_NUMBER_TEST); + // 【必填】外部系统来源分录标识(候选键):费用明细行id(唯一),A103-1 下推时 awrp_entry_code 与此对应 + entry.setAwrpEntryCode(entryGroup.entryCode()); + + // 调试 + + if (true) { + // 【必填】物料编码.编码:结算明细货物类型匹配货物类型档案货物编码 + entry.setMaterialNumber(KingdeeBfiConstant.BFI_AP_BILL_MATERIAL_NUMBER_TEST); + // 【必填】计量单位.编码:货物费用行运费计算单位(快照 price_unit,档案暂无编码字段暂传单位名称) + entry.setMeasureunitNumber(KingdeeBfiConstant.BFI_AP_BILL_MEASUREUNIT_NUMBER_TEST); + } + + // 分录备注(非必传) + // entry.setERemark(subject); + detailentry.add(entry); + } + param.setDetailentry(detailentry); + + // 付款计划 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),本期不传、后续迭代 + + BfiBillSaveResultVO saveResult = bfiService.saveApBill(param); + // 应付单id不落库,后续如需可经 A102-4 按防重键(awrp_src_sys + awrp_src_code)查询 + requireBfiSaveSuccess(saveResult, "推送金蝶财务应付单"); + return new BfiApBillPushOutcome(saveResult.getResult().get(0).getNumber(), false); + } + + // ==================== 结算明细分录解析 ==================== + + /** + * 解析正式结算明细,逐行生成应付单分录(不聚合,每条来源费用行一条分录),分录携带所属正式结算单id + * (多结算单付款申请 A103-1 下推时按结算单分组勾稽各自应付单付款计划分录)。 + *

取数来源:应付明细货物费用行(blade_receivable_payable_cargo_fee,经结算明细 sourceDetailId 关联应付明细, + * 每货物一条)——货物类型/计量单位/运输数量/含税金额直接取应付费用行(含税金额取调整后总金额 after_amount); + * 应付费用行无不含税字段,不含税金额经结算货物费用行 sourceFeeId 回链(直接选明细路径 sourceFeeId=应付费用行id, + * 预结算合并路径 sourceFeeId=预结算费用行id,经预结算费用行 sourceFeeId 回溯应付费用行id); + * 结算明细对应应付明细无货物费用行时直接报错拦截(取数必须来自应付费用明细,无兜底)。物料编码:货物类型(支持“、”分隔多类型) + * 匹配货物类型档案货物编码;分录唯一标识(awrp_entry_code)取费用明细行id(雪花id天然唯一,满足 A103-1 + * 对方系统唯一行id不能重复;A103-1 与应付单的勾稽关联走 awrp_finapbill_entryid=付款计划 + * awrp_entry_code_p,与本标识无关);分录金额不按申请付款金额分摊。

+ * + * @param settlementDetails 正式结算明细列表 + * @param context 金蝶同步上下文(缓存载体) + * @return 应付单分录列表(保持行序) + */ + public List resolveEntryGroups(List settlementDetails, KingdeeSyncContext context) { + if (Func.isEmpty(settlementDetails)) { + throw new ServiceException("推送金蝶财务应付单失败:正式结算单无结算明细,无法解析物料与计量单位"); + } + Map cargoCodeByName = cargoCodeByName(context); + // 应付明细id->应付货物费用行(分录主取数来源) + Map> payableFeesByDetailId = payableCargoFeesByDetailId(settlementDetails); + // 应付费用行id->结算金额(不含税):应付费用行无不含税字段,经结算货物费用行 sourceFeeId 回链 + Map noTaxByPayableFeeId = settlementNoTaxByPayableFeeId(settlementDetails); + List entryGroups = new ArrayList<>(); + for (FormalSettlementDetail settlementDetail : settlementDetails) { + List payableFees = + payableFeesByDetailId.getOrDefault(settlementDetail.getSourceDetailId(), List.of()); + if (Func.isEmpty(payableFees)) { + throw new ServiceException("推送金蝶财务应付单失败:结算明细[" + Func.toStr(settlementDetail.getDocumentNo()) + + "]对应应付明细无货物费用行,分录取数必须来自应付费用明细,请先维护费用数据"); + } + for (ReceivablePayableCargoFee payableFee : payableFees) { + appendEntryGroups(entryGroups, cargoCodeByName, settlementDetail.getFormalSettlementId(), + payableFee.getCargoType(), + resolveMeasureunitNumber(payableFee.getPriceUnit(), payableFee.getQuantityUnit()), + payableFee.getId(), money(payableFee.getUnitPrice()), + money(payableFee.getAfterAmount()), + noTaxByPayableFeeId.getOrDefault(payableFee.getId(), BigDecimal.ZERO), + money(payableFee.getTransportQuantity()), + "应付货物费用[" + Func.toStr(payableFee.getCargoName()) + " 第" + Func.toStr(payableFee.getLineNo()) + "行]"); + } + } + return entryGroups; + } + + /** + * 单条应付货物费用行生成不聚合分录(每行一条); + * 一行含多种货物类型时按类型拆为多条,金额与数量按份数均分(尾差计入最后一份) + * + * @param entryGroups 分录集合(保持行序) + * @param cargoCodeByName 货物类型名称->货物编码映射 + * @param formalSettlementId 分录所属正式结算单id + * @param cargoType 货物类型(支持“、”分隔多类型) + * @param measureunitNumber 计量单位编码(运费计算单位,档案暂无编码字段暂传单位名称) + * @param sourceRowId 应付货物费用行id,雪花id天然唯一 + * @param price 该行运输单价(费用明细 unit_price) + * @param amount 该行金额(含税) + * @param amountNoTax 该行金额(不含税) + * @param quantity 该行运输数量 + * @param sourceDesc 来源行描述,用于异常提示 + */ + private void appendEntryGroups(List entryGroups, Map cargoCodeByName, + Long formalSettlementId, String cargoType, String measureunitNumber, Long sourceRowId, + BigDecimal price, BigDecimal amount, BigDecimal amountNoTax, BigDecimal quantity, + String sourceDesc) { + List cargoNames = splitCargoTypes(cargoType); + if (Func.isBlank(measureunitNumber)) { + throw new ServiceException("推送金蝶财务应付单失败:" + sourceDesc + "计量单位为空,请先维护运费计算单位"); + } + // 分录唯一标识(awrp_entry_code,A103-1 要求对方系统唯一行id不能重复):雪花id天然唯一,同行多类型拆分时追加序号 + String rowEntryCode = String.valueOf(sourceRowId); + List amountShares = splitEqually(amount, cargoNames.size()); + List amountNoTaxShares = splitEqually(amountNoTax, cargoNames.size()); + List quantityShares = splitEqually(quantity, cargoNames.size()); + for (int index = 0; index < cargoNames.size(); index++) { + String cargoName = cargoNames.get(index); + String materialNumber = cargoCodeByName.get(cargoName); + if (Func.isBlank(materialNumber)) { + throw new ServiceException("推送金蝶财务应付单失败:" + sourceDesc + "货物类型[" + cargoName + "]未在货物类型档案中维护编码,请补充后重试"); + } + String entryCode = cargoNames.size() > 1 ? rowEntryCode + "-" + (index + 1) : rowEntryCode; + entryGroups.add(new BfiEntryGroup(materialNumber, measureunitNumber, entryCode, + quantityShares.get(index), price, amountShares.get(index), amountNoTaxShares.get(index), formalSettlementId)); + log.info("金蝶应付单分录生成:来源[{}],分录标识[{}],物料编码[{}],计量单位[{}],数量[{}],单价[{}],含税金额[{}],不含税金额[{}]", + sourceDesc, entryCode, materialNumber, measureunitNumber, + quantityShares.get(index), price, amountShares.get(index), amountNoTaxShares.get(index)); + } + } + + /** 付款申请关联的正式结算货物费用行(blade_formal_settlement_detail_fee,用于不含税金额回链应付费用行) */ + private List formalSettlementDetailFees(List settlementDetails) { + List settlementDetailIds = settlementDetails.stream().map(FormalSettlementDetail::getId) + .filter(Objects::nonNull).distinct().toList(); + if (Func.isEmpty(settlementDetailIds)) { + return List.of(); + } + return formalSettlementDetailFeeMapper.selectList(Wrappers.lambdaQuery() + .in(FormalSettlementDetailFee::getFormalSettlementDetailId, settlementDetailIds) + .eq(FormalSettlementDetailFee::getIsDeleted, 0) + .orderByAsc(FormalSettlementDetailFee::getLineNo)); + } + + /** 应付明细id->应付货物费用行(blade_receivable_payable_cargo_fee,经结算明细 sourceDetailId 关联,每货物一条,分录主取数来源) */ + private Map> payableCargoFeesByDetailId(List settlementDetails) { + List sourceDetailIds = settlementDetails.stream() + .map(FormalSettlementDetail::getSourceDetailId).filter(Objects::nonNull).distinct().toList(); + if (Func.isEmpty(sourceDetailIds)) { + return Map.of(); + } + return receivablePayableCargoFeeMapper.selectList(Wrappers.lambdaQuery() + .in(ReceivablePayableCargoFee::getDetailId, sourceDetailIds) + .eq(ReceivablePayableCargoFee::getIsDeleted, 0) + .orderByAsc(ReceivablePayableCargoFee::getLineNo)) + .stream().collect(Collectors.groupingBy(ReceivablePayableCargoFee::getDetailId)); + } + + /** + * 应付货物费用行id->结算金额(不含税):应付费用行无不含税字段,经结算货物费用行 sourceFeeId 回链获取。 + * 直接选应收应付明细路径 sourceFeeId=应付费用行id;预结算合并路径 sourceFeeId=预结算费用行id, + * 经预结算费用行 sourceFeeId(指向应付费用行)回溯;无对应结算费用行时无映射,调用方兜底 0 + */ + private Map settlementNoTaxByPayableFeeId(List settlementDetails) { + List settlementFees = formalSettlementDetailFees(settlementDetails); + if (Func.isEmpty(settlementFees)) { + return Map.of(); + } + // 上游费用行id(应付费用行id或预结算费用行id)-> 不含税合计 + Map noTaxBySourceFeeId = new LinkedHashMap<>(); + for (FormalSettlementDetailFee settlementFee : settlementFees) { + if (settlementFee.getSourceFeeId() == null) { + continue; + } + noTaxBySourceFeeId.merge(settlementFee.getSourceFeeId(), money(settlementFee.getSettlementAmountNoTax()), BigDecimal::add); + } + if (noTaxBySourceFeeId.isEmpty()) { + return Map.of(); + } + // sourceFeeId 指向预结算费用行的:经预结算费用行回溯应付费用行id(直接路径无预结算费用行,保持原 id) + Map payableFeeIdByPreFeeId = preSettlementDetailFeeMapper.selectList( + Wrappers.lambdaQuery().in(PreSettlementDetailFee::getId, new ArrayList<>(noTaxBySourceFeeId.keySet()))) + .stream().filter(preSettlementFee -> preSettlementFee.getSourceFeeId() != null) + .collect(Collectors.toMap(PreSettlementDetailFee::getId, PreSettlementDetailFee::getSourceFeeId, (left, right) -> left)); + Map noTaxByPayableFeeId = new LinkedHashMap<>(); + noTaxBySourceFeeId.forEach((sourceFeeId, noTax) -> + noTaxByPayableFeeId.merge(payableFeeIdByPreFeeId.getOrDefault(sourceFeeId, sourceFeeId), noTax, BigDecimal::add)); + return noTaxByPayableFeeId; + } + + /** 货物类型名称->货物编码映射(取自货物类型档案;名称重复且编码不一致时拦截;批量同步时全批共享一次加载结果) */ + private Map cargoCodeByName(KingdeeSyncContext context) { + Map cache = context.cargoCodeByName(); + if (!cache.isEmpty()) { + return cache; + } + R> response = sysClient.getCargoTypes(); + if (response == null || !response.isSuccess() || Func.isEmpty(response.getData())) { + throw new ServiceException("推送金蝶财务应付单失败:货物类型档案读取失败,请稍后重试"); + } + Map loaded = new HashMap<>(); + for (CargoType cargoType : response.getData()) { + if (cargoType == null || Func.isBlank(cargoType.getCargoName()) || Func.isBlank(cargoType.getCargoCode())) { + continue; + } + String cargoName = cargoType.getCargoName().trim(); + String cargoCode = cargoType.getCargoCode().trim(); + String existingCode = loaded.putIfAbsent(cargoName, cargoCode); + if (existingCode != null && !existingCode.equals(cargoCode)) { + throw new ServiceException("推送金蝶财务应付单失败:货物类型[" + cargoName + "]在货物类型档案中存在多个编码,请检查档案数据"); + } + } + cache.putAll(loaded); + return cache; + } + + /** + * 解析计量单位编码:优先取快照运费计算单位(快照表 price_unit,随结算自动带出), + * 为空(历史快照)时兜底数量单位。计量单位档案暂无编码字段,暂以单位名称作为编码; + * 档案补充编码字段后在此处增加档案映射。 + * + * @param priceUnit 快照运费计算单位(可能为空) + * @param fallbackUnit 兜底数量单位 + * @return 计量单位编码,无法解析时返回 null + */ + private String resolveMeasureunitNumber(String priceUnit, String fallbackUnit) { + String unitName = Func.isNotBlank(priceUnit) ? priceUnit : fallbackUnit; + return Func.isBlank(unitName) ? null : unitName.trim(); + } + + /** 货物类型支持“、/,”分隔的多类型写法,拆分为单类型名称 */ + private List splitCargoTypes(String cargoType) { + if (Func.isBlank(cargoType)) { + throw new ServiceException("推送金蝶财务应付单失败:结算明细货物类型为空,无法解析物料编码"); + } + return Arrays.stream(cargoType.split("[、,,]")) + .map(String::trim).filter(Func::isNotBlank).distinct().toList(); + } + + /** 数值按份数均分(保留 2 位小数、向下取整,尾差计入最后一份) */ + private List splitEqually(BigDecimal total, int parts) { + if (parts <= 1) { + return List.of(total); + } + List shares = new ArrayList<>(parts); + BigDecimal remaining = total; + for (int index = 0; index < parts; index++) { + if (index == parts - 1) { + shares.add(remaining); + } else { + BigDecimal share = total.divide(BigDecimal.valueOf(parts), 2, RoundingMode.DOWN); + shares.add(share); + remaining = remaining.subtract(share); + } + } + return shares; + } + + // ==================== 金蝶公共校验与档案取数 ==================== + + /** + * 校验金蝶保存/下推返回结果(单据级失败以 errors 明细抛出) + * + * @param saveResult 金蝶返回结果 + * @param action 业务动作描述,用于异常提示 + */ + public void requireBfiSaveSuccess(BfiBillSaveResultVO saveResult, String action) { + if (saveResult == null || Func.isEmpty(saveResult.getResult())) { + throw new ServiceException(action + "失败,金蝶未返回单据结果"); + } + BfiBillSaveResultItemVO item = saveResult.getResult().get(0); + if (!Boolean.TRUE.equals(item.getBillStatus())) { + String errorText = Func.isBlank(item.getErrorText()) ? "未知错误" : item.getErrorText(); + throw new ServiceException(action + "失败:" + errorText); + } + } + + /** + * 校验金蝶保存/下推返回结果并取星瀚单据id(单据级失败以 errors 明细抛出) + * + * @param saveResult 金蝶返回结果 + * @param action 业务动作描述,用于异常提示 + * @return 星瀚单据id + */ + public String requireBfiBillId(BfiBillSaveResultVO saveResult, String action) { + requireBfiSaveSuccess(saveResult, action); + Object billId = saveResult.getResult().get(0).getId(); + if (billId == null) throw new ServiceException(action + "失败,金蝶未返回单据id"); + return String.valueOf(billId); + } + + /** + * 校验并获取单据关联合同(批量同步时按合同id缓存复用,单笔同步退化为直接查询) + * + * @param contractId 合同id + * @param context 金蝶同步上下文(缓存载体) + * @return 合同实体 + */ + public ContractManage requireContract(Long contractId, KingdeeSyncContext context) { + if (contractId == null) throw new ServiceException("推送金蝶失败,单据未关联合同"); + ContractManage contract = context.contractCache().computeIfAbsent(contractId, contractManageMapper::selectById); + if (contract == null || Objects.equals(contract.getIsDeleted(), 1)) throw new ServiceException("推送金蝶失败,关联合同不存在"); + return contract; + } + + /** + * 金蝶接口日期文本:yyyy-MM-dd HH:mm:ss(A102-2/A103-1/A103-3 各接口统一格式) + * + * @param date 业务日期 + * @return 日期文本 + */ + public String bfiDateTimeText(LocalDate date) { + LocalDate effectiveDate = date == null ? LocalDate.now() : date; + return effectiveDate.atStartOfDay().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")); + } + + /** + * 合同所属组织编码 + * + * @param contract 合同实体 + * @return 组织编码 + */ + public String bfiOrgNumber(ContractManage contract) { + Dept dept = contract.getOrganizationId() == null ? null : SysCache.getDept(contract.getOrganizationId()); + // dept为空 或者 deptCode为空,直接报错 + if (dept == null || Func.isBlank(dept.getDeptCode())) { + throw new ServiceException("推送金蝶失败,合同所属组织编码不能为空,请检查部门ID【" + contract.getOrganizationId() + "】对应的部门是否维护部门编码"); + } + return dept.getDeptCode(); + } + + /** + * 合同结算币种转金蝶币别代码(RMB 统一映射 CNY) + * + * @param contract 合同实体 + * @return 币别代码 + */ + public String bfiCurrency(ContractManage contract) { + if (Func.isBlank(contract.getSettlementCurrency())) throw new ServiceException("推送金蝶失败,合同结算币种不能为空"); + String currency = contract.getSettlementCurrency(); + return "RMB".equalsIgnoreCase(currency) ? "CNY" : currency; + } + + /** 财务应付单往来类型:合同类别为“客户合同”时为 bd_customer(客户),否则 bd_supplier(供应商) */ + public String bfiAsstactType(ContractManage contract) { + return "客户合同".equals(contract.getContractCategory()) + ? KingdeeBfiConstant.BFI_CUSTOMER_ASSTACT_TYPE : KingdeeBfiConstant.BFI_SUPPLIER_ASSTACT_TYPE; + } + + /** 财务应付单往来户.编码:合同乙方关联客商档案取客商编码,未维护时拦截 */ + public String bfiAsstactNumber(ContractManage contract, KingdeeSyncContext context) { + if (Func.isBlank(contract.getPartyB())) { + throw new ServiceException("推送金蝶财务应付单失败,合同乙方不能为空,请检查合同[" + Func.toStr(contract.getContractNo()) + "]乙方信息"); + } + // 批量同步时按乙方名称缓存客商编码(同批同一乙方只查一次),单笔同步退化为直接查询 + String customerCode = context.customerCodeCache().computeIfAbsent(contract.getPartyB(), this::queryCustomerCode); + if (Func.isBlank(customerCode)) { + throw new ServiceException("推送金蝶财务应付单失败,合同乙方[" + contract.getPartyB() + "]未在客商档案中维护客商编码,请补充后重试"); + } + return customerCode; + } + + /** 按乙方名称(全称或简称)查询客商编码,未维护返回 null */ + private String queryCustomerCode(String partyB) { + CustomerArchive customer = customerArchiveMapper.selectOne(Wrappers.lambdaQuery() + .and(wrapper -> wrapper.eq(CustomerArchive::getFullName, partyB).or().eq(CustomerArchive::getShortName, partyB)) + .eq(CustomerArchive::getIsDeleted, 0).last("limit 1")); + return customer == null ? null : customer.getCustomerCode(); + } + + /** + * 列表按批大小切分逐批处理(末批可能不足批大小),远程接口批量入参分批用,避免单次过滤条件过长 + * + * @param items 待处理列表 + * @param batchSize 批大小 + * @param batchConsumer 批处理逻辑 + * @param 元素类型 + */ + private void batchForeach(List items, int batchSize, Consumer> batchConsumer) { + for (int from = 0; from < items.size(); from += batchSize) { + batchConsumer.accept(items.subList(from, Math.min(from + batchSize, items.size()))); + } + } + + /** 金额空值兜底并保留 2 位小数 */ + private BigDecimal money(BigDecimal value) { + return (value == null ? BigDecimal.ZERO : value).setScale(2, RoundingMode.HALF_UP); + } + + /** + * 财务应付单/付款申请分录:每条来源费用行一条(不聚合) + * + * @param materialNumber 物料编码(货物类型档案货物编码) + * @param measureunitNumber 计量单位编码(运费计算单位,档案暂无编码字段暂传单位名称) + * @param entryCode 分录唯一标识(awrp_entry_code):费用明细行id,同行多货物类型拆分时追加序号 + * @param quantity 该行运输数量 + * @param price 该行运输单价(费用明细 unit_price) + * @param amount 该行金额(含税),对应明细应付金额 e_pricetaxtotal + * @param amountNoTax 该行金额(不含税),对应明细金额 e_amount + * @param formalSettlementId 分录所属正式结算单id(多结算单付款申请 A103-1 按结算单勾稽各自应付单) + */ + public record BfiEntryGroup(String materialNumber, String measureunitNumber, String entryCode, + BigDecimal quantity, BigDecimal price, BigDecimal amount, BigDecimal amountNoTax, + Long formalSettlementId) { + } + + /** + * 财务应付单(A102-2)推送结果 + * + * @param billNo 财务应付单单据编号(金蝶已存在时返回已有单据编号) + * @param alreadyExist 金蝶是否已存在该防重键的财务应付单,true 表示本次幂等跳过保存(上次同步已在后续环节中断) + */ + public record BfiApBillPushOutcome(String billNo, boolean alreadyExist) { + } +} From 0e1334be23b15c4b01f9393092a6af4623596d0c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=98=E6=B3=89=E4=BD=B3?= <1107186916@qq.com> Date: Fri, 18 Sep 2026 21:07:12 +0800 Subject: [PATCH 4/5] =?UTF-8?q?:zap:=20=E9=87=91=E8=9D=B6=E5=BA=94?= =?UTF-8?q?=E4=BB=98=E5=8D=95=E5=88=86=E5=BD=95=E4=B8=8D=E5=86=8D=E6=8C=89?= =?UTF-8?q?=E5=A4=9A=E8=B4=A7=E7=89=A9=E7=B1=BB=E5=9E=8B=E6=8B=86=E8=A1=8C?= =?UTF-8?q?=EF=BC=9A=E6=95=B4=E8=A1=8C=E4=B8=80=E6=9D=A1=E5=88=86=E5=BD=95?= =?UTF-8?q?=EF=BC=8C=E7=89=A9=E6=96=99=E7=BC=96=E7=A0=81=E5=8F=96=E7=AC=AC?= =?UTF-8?q?=E4=B8=80=E7=A7=8D=E8=B4=A7=E7=89=A9=E7=B1=BB=E5=9E=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../support/KingdeeApBillSyncSupport.java | 59 ++++++------------- 1 file changed, 18 insertions(+), 41 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 caa2a279..f5ab7264 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 @@ -379,8 +379,8 @@ public class KingdeeApBillSyncSupport { } /** - * 单条应付货物费用行生成不聚合分录(每行一条); - * 一行含多种货物类型时按类型拆为多条,金额与数量按份数均分(尾差计入最后一份) + * 单条应付货物费用行生成一条分录; + * 货物类型含多种(“、/,”分隔)时不拆行,物料编码取第一种货物类型,金额数量整行直送 * * @param entryGroups 分录集合(保持行序) * @param cargoCodeByName 货物类型名称->货物编码映射 @@ -398,28 +398,21 @@ public class KingdeeApBillSyncSupport { Long formalSettlementId, String cargoType, String measureunitNumber, Long sourceRowId, BigDecimal price, BigDecimal amount, BigDecimal amountNoTax, BigDecimal quantity, String sourceDesc) { - List cargoNames = splitCargoTypes(cargoType); if (Func.isBlank(measureunitNumber)) { throw new ServiceException("推送金蝶财务应付单失败:" + sourceDesc + "计量单位为空,请先维护运费计算单位"); } - // 分录唯一标识(awrp_entry_code,A103-1 要求对方系统唯一行id不能重复):雪花id天然唯一,同行多类型拆分时追加序号 - String rowEntryCode = String.valueOf(sourceRowId); - List amountShares = splitEqually(amount, cargoNames.size()); - List amountNoTaxShares = splitEqually(amountNoTax, cargoNames.size()); - List quantityShares = splitEqually(quantity, cargoNames.size()); - for (int index = 0; index < cargoNames.size(); index++) { - String cargoName = cargoNames.get(index); - String materialNumber = cargoCodeByName.get(cargoName); - if (Func.isBlank(materialNumber)) { - throw new ServiceException("推送金蝶财务应付单失败:" + sourceDesc + "货物类型[" + cargoName + "]未在货物类型档案中维护编码,请补充后重试"); - } - String entryCode = cargoNames.size() > 1 ? rowEntryCode + "-" + (index + 1) : rowEntryCode; - entryGroups.add(new BfiEntryGroup(materialNumber, measureunitNumber, entryCode, - quantityShares.get(index), price, amountShares.get(index), amountNoTaxShares.get(index), formalSettlementId)); - log.info("金蝶应付单分录生成:来源[{}],分录标识[{}],物料编码[{}],计量单位[{}],数量[{}],单价[{}],含税金额[{}],不含税金额[{}]", - sourceDesc, entryCode, materialNumber, measureunitNumber, - quantityShares.get(index), price, amountShares.get(index), amountNoTaxShares.get(index)); + // 货物类型含多种时不拆行(业务约定整行一条分录),物料编码取第一种货物类型 + String cargoName = splitCargoTypes(cargoType).get(0); + String materialNumber = cargoCodeByName.get(cargoName); + if (Func.isBlank(materialNumber)) { + throw new ServiceException("推送金蝶财务应付单失败:" + sourceDesc + "货物类型[" + cargoName + "]未在货物类型档案中维护编码,请补充后重试"); } + // 分录唯一标识(awrp_entry_code,A103-1 要求对方系统唯一行id不能重复):应付货物费用行id,雪花id天然唯一 + String entryCode = String.valueOf(sourceRowId); + entryGroups.add(new BfiEntryGroup(materialNumber, measureunitNumber, entryCode, + quantity, price, amount, amountNoTax, formalSettlementId)); + log.info("金蝶应付单分录生成:来源[{}],分录标识[{}],物料编码[{}],计量单位[{}],数量[{}],单价[{}],含税金额[{}],不含税金额[{}]", + sourceDesc, entryCode, materialNumber, measureunitNumber, quantity, price, amount, amountNoTax); } /** 付款申请关联的正式结算货物费用行(blade_formal_settlement_detail_fee,用于不含税金额回链应付费用行) */ @@ -521,7 +514,10 @@ public class KingdeeApBillSyncSupport { return Func.isBlank(unitName) ? null : unitName.trim(); } - /** 货物类型支持“、/,”分隔的多类型写法,拆分为单类型名称 */ + /** + * 货物类型支持“、/,”分隔的多类型写法,解析为类型名称列表; + * 分录不按类型拆行(业务约定整行一条),物料编码仅取列表第一种类型 + */ private List splitCargoTypes(String cargoType) { if (Func.isBlank(cargoType)) { throw new ServiceException("推送金蝶财务应付单失败:结算明细货物类型为空,无法解析物料编码"); @@ -530,25 +526,6 @@ public class KingdeeApBillSyncSupport { .map(String::trim).filter(Func::isNotBlank).distinct().toList(); } - /** 数值按份数均分(保留 2 位小数、向下取整,尾差计入最后一份) */ - private List splitEqually(BigDecimal total, int parts) { - if (parts <= 1) { - return List.of(total); - } - List shares = new ArrayList<>(parts); - BigDecimal remaining = total; - for (int index = 0; index < parts; index++) { - if (index == parts - 1) { - shares.add(remaining); - } else { - BigDecimal share = total.divide(BigDecimal.valueOf(parts), 2, RoundingMode.DOWN); - shares.add(share); - remaining = remaining.subtract(share); - } - } - return shares; - } - // ==================== 金蝶公共校验与档案取数 ==================== /** @@ -685,7 +662,7 @@ public class KingdeeApBillSyncSupport { * * @param materialNumber 物料编码(货物类型档案货物编码) * @param measureunitNumber 计量单位编码(运费计算单位,档案暂无编码字段暂传单位名称) - * @param entryCode 分录唯一标识(awrp_entry_code):费用明细行id,同行多货物类型拆分时追加序号 + * @param entryCode 分录唯一标识(awrp_entry_code):费用明细行id,雪花id天然唯一 * @param quantity 该行运输数量 * @param price 该行运输单价(费用明细 unit_price) * @param amount 该行金额(含税),对应明细应付金额 e_pricetaxtotal From ba93a9b0ca4f6112e0490bb206302362ef1d2bf8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=98=E6=B3=89=E4=BD=B3?= <1107186916@qq.com> Date: Fri, 18 Sep 2026 21:35:35 +0800 Subject: [PATCH 5/5] =?UTF-8?q?:zap:=20=E4=BB=98=E6=AC=BE=E7=94=B3?= =?UTF-8?q?=E8=AF=B7=E5=90=8C=E6=AD=A5=E9=87=91=E8=9D=B6=E6=8E=A8=E9=80=81?= =?UTF-8?q?=E6=88=90=E5=8A=9F=E5=8D=B3=E6=97=B6=E5=9B=9E=E5=A1=AB=E4=BB=98?= =?UTF-8?q?=E6=AC=BE=E7=94=B3=E8=AF=B7=E5=8D=95=E7=BC=96=E5=8F=B7=EF=BC=8C?= =?UTF-8?q?=E6=97=A0=E9=9C=80=E7=AD=89=E5=BE=85=E5=AE=9A=E6=97=B6=E5=9B=9E?= =?UTF-8?q?=E5=86=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../impl/PaymentApplicationServiceImpl.java | 25 +++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) 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 cbc53e08..6a70d8bc 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 @@ -39,6 +39,7 @@ import org.springblade.thirdparty.bfi.pojo.dto.BfiPayApplyQueryDTO; import org.springblade.thirdparty.bfi.pojo.dto.BfiPayApplySaveDTO; import org.springblade.thirdparty.bfi.pojo.dto.BfiPayApplySaveEntryDTO; import org.springblade.thirdparty.bfi.pojo.dto.BfiPayBillQueryDTO; +import org.springblade.thirdparty.bfi.pojo.vo.BfiBillSaveResultItemVO; import org.springblade.thirdparty.bfi.pojo.vo.BfiBillSaveResultVO; import org.springblade.thirdparty.bfi.pojo.vo.BfiPayApplyEntryVO; import org.springblade.thirdparty.bfi.pojo.vo.BfiPayApplyVO; @@ -888,7 +889,7 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl