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()