diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/support/KingdeeApBillSyncSupport.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/support/KingdeeApBillSyncSupport.java index 21be0c4f..c967fad6 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/support/KingdeeApBillSyncSupport.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/support/KingdeeApBillSyncSupport.java @@ -100,6 +100,10 @@ import java.util.stream.Collectors; * + 结算合计费用项(手动添加的每行一条); * 物料编码按装卸大类/项目业务模式/运单运输方式映射,编码维护在 Nacos 配置 thirdparty.bfi.material.*(见 {@link #resolveMaterialNumber}); * 运输类分录数量取运输量(按税率拆行时每行都取运输量)、装卸类默认 1;单价按结算金额/数量反算。

+ *

预付信息分录(preentry,见 {@link #resolvePreEntries}):存在预付款抵扣(进度预付/项目预付核销)时, + * 按金蝶付款处理单(cas_paybill)逐笔送出 y_billtype/y_billid/y_settleamt,同 y_billid 合并求和; + * 任一笔应抵扣预付无法定位金蝶付款处理单内码(未付款/内码未回写/实付不足)时抛异常拦截应付单推送, + * 失败落 kingdee_fail_reason,付款结果回写后由重推任务自动续推。

* * @author Chill */ @@ -283,7 +287,9 @@ public class KingdeeApBillSyncSupport { * 幂等跳过保存并返回已有单据编号(说明上次同步已保存成功、在后续环节中断,应续推而非拦截报错)。 * 明细分录取页面结算明细(每行按 货物→费用大类→税率 拆分)+ 结算合计费用项(每行一条),结算金额逐行直送; * 汇率日期/汇率取结算单页面维护值;付款计划 planentity 暂不传(启用方案见方法内注释), - * 当前由金蝶按明细自动汇总生成 + * 当前由金蝶按明细自动汇总生成; + * 预付信息分录 preentry 仅在真正新建应付单时解析(幂等跳过路径不做预付校验), + * 每条对应一张金蝶付款处理单(cas_paybill),取数口径见 {@link #resolvePreEntries} * * @param apBillSrcCode 财务应付单防重键(正式结算单号_结算明细单据号拼接) * @param settlement 正式结算单(合同/汇率日期/汇率取数来源) @@ -299,7 +305,7 @@ public class KingdeeApBillSyncSupport { String existBillNo = context.apBillNoCache().computeIfAbsent(apBillSrcCode, code -> queryApBillExistNos(List.of(code)).getOrDefault(code, "")); if (Func.isNotEmpty(existBillNo)) { - log.info("正式结算单[{}]金蝶财务应付单已生成(单据编号[{}]),幂等跳过 A102-2 保存", subject, existBillNo); + log.info("正式结算单[{}]金蝶财务应付单已生成(单据编号[{}]), 幂等跳过 A102-2 保存", subject, existBillNo); return new BfiApBillPushOutcome(existBillNo, null, true); } ContractManage contract = requireContract(settlement.getContractId(), context); @@ -410,14 +416,14 @@ public class KingdeeApBillSyncSupport { } // 推送入参摘要(联调排查:防重键、分录条数、页面维护的汇率口径) - log.info("推送金蝶财务应付单:正式结算单[{}],防重键[{}],明细分录{}条,预付信息{}条,汇率日期[{}],汇率[{}]", + log.info("推送金蝶财务应付单: 正式结算单[{}], 防重键[{}], 明细分录{}条, 预付信息{}条, 汇率日期[{}], 汇率[{}]", subject, apBillSrcCode, detailentry.size(), preEntries.size(), param.getExratedate(), param.getExchangerate()); BfiBillSaveResultVO saveResult = bfiService.saveApBill(param); // 应付单id不落库,后续如需可经 A102-4 按防重键(awrp_src_sys + awrp_src_code)查询 requireBfiSaveSuccess(saveResult, "推送金蝶财务应付单"); String billNo = saveResult.getResult().get(0).getNumber(); - log.info("金蝶财务应付单保存成功:正式结算单[{}],单据编号[{}],明细分录{}条", subject, billNo, detailentry.size()); + log.info("金蝶财务应付单保存成功: 正式结算单[{}], 单据编号[{}], 明细分录{}条", subject, billNo, detailentry.size()); // 星瀚应付单内码随结果返回(A902 附件上传定位用) BfiBillSaveResultItemVO savedItem = saveResult.getResult().get(0); return new BfiApBillPushOutcome(savedItem.getNumber(), savedItem.getId() == null ? null : String.valueOf(savedItem.getId()), false); @@ -487,8 +493,14 @@ public class KingdeeApBillSyncSupport { } } if (demandAmounts.isEmpty()) { + // 无预付抵扣(无核销行且预付记录已付金额均为0):不送 preentry,保持报文字段缺省 + log.info("应付单预付信息归集: 正式结算单[{}], 无预付抵扣, 不送预付信息分录", settlement.getFormalSettlementNo()); return List.of(); } + // 预付抵扣归集摘要(联调排查: 项目预付按核销行、进度预付按预付记录已付金额归集到底层付款申请) + log.info("应付单预付信息归集: 正式结算单[{}], 应抵扣付款申请{}笔, 合计金额{}", + settlement.getFormalSettlementNo(), demandAmounts.size(), + demandAmounts.values().stream().reduce(BigDecimal.ZERO, BigDecimal::add)); // 批量取付款申请与付款处理记录(仅保留带回金蝶付款处理单内码的记录),逐笔校验并分摊 Map applications = this.paymentApplicationMapper.selectBatchIds(demandAmounts.keySet()).stream() .collect(Collectors.toMap(PaymentApplication::getId, Function.identity(), (first, second) -> first)); @@ -542,6 +554,9 @@ public class KingdeeApBillSyncSupport { String summary = blockReasons.size() > PREPAY_BLOCK_REASON_LIMIT ? String.join(";", blockReasons.subList(0, PREPAY_BLOCK_REASON_LIMIT)) + ";等" + blockReasons.size() + "笔预付无法抵扣" : String.join(";", blockReasons); + // 拦截关键日志:原因落盘供排查(异常文案同步落 kingdee_fail_reason,付款结果回写后由重推任务自动续推) + log.warn("应付单预付信息校验未通过, 拦截应付单推送: 正式结算单[{}], 应抵扣付款申请{}笔, 原因: {}", + settlement.getFormalSettlementNo(), demandAmounts.size(), summary); throw new ServiceException("推送金蝶财务应付单失败:" + summary); } List preEntries = new ArrayList<>(); @@ -554,6 +569,11 @@ public class KingdeeApBillSyncSupport { log.info("应付单预付信息分录: 正式结算单[{}], 来源[{}], y_billid={}, y_settleamt={}", settlement.getFormalSettlementNo(), sourcesByBillId.get(billId), billId, settleAmount); }); + // 分录解析摘要:同一付款申请跨多预结算分摊后落在同一付款处理单的金额已按 y_billid 合并 + log.info("应付单预付信息解析完成: 正式结算单[{}], 应抵扣付款申请{}笔, 生成分录{}条, 合计金额{}", + settlement.getFormalSettlementNo(), demandAmounts.size(), preEntries.size(), + preEntries.stream().map(BfiApBillSavePreEntryDTO::getYSettleamt) + .reduce(BigDecimal.ZERO, BigDecimal::add)); return preEntries; } @@ -635,7 +655,7 @@ public class KingdeeApBillSyncSupport { + "]无结算明细与结算合计费用项,无法解析分录"); } // 分录解析摘要(联调排查:分录数=结算明细拆分行+手动添加的结算合计费用项,物料/分录标识逐条见下方分录生成日志) - log.info("金蝶应付单分录解析完成:正式结算单[{}],结算明细{}条 + 手工结算合计{}条 = 共{}条分录", + log.info("金蝶应付单分录解析完成: 正式结算单[{}], 结算明细{}条 + 手工结算合计{}条 = 共{}条分录", settlement.getFormalSettlementNo(), settlementDetails.size(), manualSummaryFees.size(), entryGroups.size()); return entryGroups; } @@ -680,7 +700,7 @@ public class KingdeeApBillSyncSupport { context.taxRateByRule(), rowAmount); if (splits.isEmpty()) { // 0元货物行(费用项与结算金额均为0)跳过不生成0元分录 - log.info("金蝶应付单分录生成:来源[{}],费用项与结算金额均为0,跳过该货物行", sourceDesc); + log.info("金蝶应付单分录生成: 来源[{}], 费用项与结算金额均为0, 跳过该货物行", sourceDesc); return; } // 运输类数量:取运单运输量(未维护兜底 1) @@ -707,7 +727,7 @@ public class KingdeeApBillSyncSupport { String entryCode = entryCodePrefix + "-" + split.getCategory().getLabel() + split.taxRateSuffix(); entryGroups.add(new BfiEntryGroup(materialNumber, entryCode, quantity, price, amount, amountNoTax, cargoName, vehicleNo, context.settlement().getId())); - log.info("金蝶应付单分录生成:来源[{}],分录标识[{}],物料编码[{}],数量[{}],单价[{}],含税金额[{}],不含税金额[{}]", + log.info("金蝶应付单分录生成: 来源[{}], 分录标识[{}], 物料编码[{}], 数量[{}], 单价[{}], 含税金额[{}], 不含税金额[{}]", sourceDesc, entryCode, materialNumber, quantity, price, amount, amountNoTax); } } @@ -741,7 +761,7 @@ public class KingdeeApBillSyncSupport { + (Func.isBlank(feeItem) ? feeType : feeType + "-" + feeItem); entryGroups.add(new BfiEntryGroup(materialNumber, entryCode, quantity, price, amount, null, null, null, settlement.getId())); - log.info("金蝶应付单分录生成:来源[{}],分录标识[{}],物料编码[{}],数量[{}],单价[{}],含税金额[{}]", + log.info("金蝶应付单分录生成: 来源[{}], 分录标识[{}], 物料编码[{}], 数量[{}], 单价[{}], 含税金额[{}]", sourceDesc, entryCode, materialNumber, quantity, price, amount); } @@ -840,7 +860,7 @@ public class KingdeeApBillSyncSupport { } catch (RuntimeException ignored) { // 计费方案 JSON 兼容历史数据,解析失败按未配置处理 } - log.info("合同[{}]计费方案解析:{}项费用类型/费用项配置税率{}", contract.getContractNo(), taxRateByRule.size(), taxRateByRule); + log.info("合同[{}]计费方案解析: {}项费用类型/费用项配置税率{}", contract.getContractNo(), taxRateByRule.size(), taxRateByRule); return taxRateByRule; } @@ -1090,7 +1110,7 @@ public class KingdeeApBillSyncSupport { } loaded.putIfAbsent(feeItem.getName().trim(), feeItem.getFeeCategory().trim()); } - log.info("费用项档案加载:{}项(费用项名称 -> 费用大类编码)", loaded.size()); + log.info("费用项档案加载: {}项(费用项名称 -> 费用大类编码)", loaded.size()); cache.putAll(loaded); return cache; }