diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/config/BfiProperties.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/config/BfiProperties.java index 46748a99..e61d785d 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/config/BfiProperties.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/config/BfiProperties.java @@ -230,6 +230,11 @@ public class BfiProperties { * 装卸大类、手动添加非运费杂费物料编码(装卸) */ private String loading = "XDWL11002004"; + + /** + * 仓储大类、手动添加仓储费用物料编码(仓储) + */ + private String warehouse = "XDWL11002001"; } /** 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 653180b7..12a1060f 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 @@ -104,6 +104,33 @@ public class KingdeeApBillSyncSupport { public static final String FEE_CATEGORY_FREIGHT = "A01"; /** 费用大类编码(业务字典 fee_category 键值):装卸 */ public static final String FEE_CATEGORY_LOADING = "A10"; + /** 费用大类编码(业务字典 fee_category 键值):仓储 */ + public static final String FEE_CATEGORY_WAREHOUSE = "A05"; + + /** + * 分录费用大类(三分类):label 用于分组键与分录标识拼接 + */ + public enum FeeCategory { + + /** 运输 */ + FREIGHT("运输费"), + + /** 仓储 */ + WAREHOUSE("仓储"), + + /** 装卸 */ + LOADING("装卸"); + + private final String label; + + FeeCategory(String label) { + this.label = label; + } + + public String getLabel() { + return label; + } + } private final IBfiService bfiService; private final ContractManageMapper contractManageMapper; @@ -403,10 +430,10 @@ public class KingdeeApBillSyncSupport { * 解析单张正式结算单分录:页面结算明细每行(一个运单)按 货物 → 费用大类 → 税率 拆分多行 * + 手动添加的结算合计费用项每行一条(系统自动汇总行是明细金额的再汇总不送)。 *

取数口径: - * 结算明细拆行——一车多货(存在货物费用行)先按货物拆;费用项名称含“装卸”归装卸大类、其余归运输大类, - * 各大类内按合同计费方案规则税率分组、不同税率分行,运输类每行数量取运单运输量、装卸类每行数量默认 1, + * 结算明细拆行——一车多货(存在货物费用行)先按货物拆;费用项按档案分类判定 装卸(A10)/仓储(A05)/运输 三大类, + * 各大类内按合同计费方案规则税率分组、不同税率分行,运输类每行数量取运单运输量、装卸/仓储类每行数量默认 1, * 费用项金额合计与结算金额的残差并入运输类无税率组保证勾稽; - * 物料编码按装卸大类/项目业务模式/运单运输方式映射(thirdparty.bfi.material.* 配置); + * 物料编码按费用大类/项目业务模式/运单运输方式映射(thirdparty.bfi.material.* 配置); * 单价按结算金额/数量反算; * 结算金额:结算明细行取结算金额(含税)、结算合计费用项行取合计行结算金额; * 运输货物/车号仅结算明细拆分行传(货物名称/车号);分录标识:结算明细拆分行取单据号-货物序号-大类-税率、 @@ -479,8 +506,8 @@ public class KingdeeApBillSyncSupport { /** * 单个货物费用行按费用大类/税率拆分生成分录: - * 费用项名称含“装卸”归装卸大类、其余归运输大类,各大类内按合同计费方案税率分组、不同税率分行; - * 运输类每行数量取运单运输量、装卸类每行数量默认 1; + * 费用项按档案分类判定 装卸(A10)/仓储(A05)/运输 三大类,各大类内按合同计费方案税率分组、不同税率分行; + * 运输类每行数量取运单运输量、装卸/仓储类每行数量默认 1; * 运输类首行携带行不含税金额(拆分行无分类不含税金额,仅保证不含税合计勾稽); * 运输货物取货物名称、车号取结算明细车号(同一车牌号就是一个车) */ @@ -504,21 +531,20 @@ public class KingdeeApBillSyncSupport { String entryCodePrefix = documentNo + "-" + cargoSeq; boolean amountNoTaxSent = false; for (FeeLineSplit split : splits) { - String category = split.isLoading() ? "装卸" : "运输费"; - // 物料:装卸大类映射装卸物料;运输类按项目业务模式/运单运输方式映射 - String materialNumber = resolveMaterialNumber(split.isLoading(), context.project(), detail.getTransportType(), sourceDesc); - // 数量:运输类取运输量,装卸类默认 1 - BigDecimal quantity = split.isLoading() ? BigDecimal.ONE : freightQuantity; + // 物料:装卸大类映射装卸物料、仓储大类映射仓储物料;运输类按项目业务模式/运单运输方式映射 + String materialNumber = resolveMaterialNumber(split.getCategory(), context.project(), detail.getTransportType(), sourceDesc); + // 数量:运输类取运输量,装卸/仓储类默认 1 + BigDecimal quantity = split.getCategory() == FeeCategory.FREIGHT ? freightQuantity : BigDecimal.ONE; BigDecimal amount = money(split.getAmount()); BigDecimal price = entryPrice(amount, quantity); // 不含税金额:仅运输类首行携带,保证不含税合计与结算明细勾稽 BigDecimal amountNoTax = null; - if (!split.isLoading() && !amountNoTaxSent && rowAmountNoTax != null) { + if (split.getCategory() == FeeCategory.FREIGHT && !amountNoTaxSent && rowAmountNoTax != null) { amountNoTax = money(rowAmountNoTax); amountNoTaxSent = true; } // 分录唯一标识:单据号-货物序号-费用大类-税率(税率未配置不拼) - String entryCode = entryCodePrefix + "-" + category + split.taxRateSuffix(); + 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("金蝶应付单分录生成:来源[{}],分录标识[{}],物料编码[{}],数量[{}],单价[{}],含税金额[{}],不含税金额[{}]", @@ -532,8 +558,8 @@ public class KingdeeApBillSyncSupport { } /** - * 单条手动添加的结算合计费用项生成一条分录:非运费类(装卸/仓储等杂费)映射装卸物料, - * 运费类按项目业务模式/运输方式映射(运输方式取结算明细首行); + * 单条手动添加的结算合计费用项生成一条分录:运费类按项目业务模式/运输方式映射(运输方式取结算明细首行), + * 仓储大类(档案编码 A05)映射仓储物料,其余非运费类映射装卸物料; * 非运费类数量默认 1,金额取合计行结算金额,运输货物/车号不传,分录标识取结算单号-费用类型-费用项拼接 */ private void appendSummaryEntry(List entryGroups, EntryResolveContext context, @@ -542,9 +568,10 @@ public class KingdeeApBillSyncSupport { String feeType = Func.toStr(summaryFee.getFeeType()).trim(); String feeItem = Func.toStr(summaryFee.getFeeItem()).trim(); String sourceDesc = "结算合计[" + feeType + "-" + feeItem + "]"; - // 非运费类(费用项档案分类编码非 A01 且名称无运费字样)统一映射装卸物料 - boolean loadingFee = !isFreightCategory(feeType, feeItem, context.feeCategoryByName()); - String materialNumber = resolveMaterialNumber(loadingFee, context.project(), fallbackTransportType, sourceDesc); + // 物料:运费类按项目业务模式/运输方式映射,仓储大类(档案编码 A05)映射仓储物料,其余非运费类映射装卸物料 + FeeCategory feeCategory = isFreightCategory(feeType, feeItem, context.feeCategoryByName()) ? FeeCategory.FREIGHT + : isWarehouseCategory(feeItem, context.feeCategoryByName()) ? FeeCategory.WAREHOUSE : FeeCategory.LOADING; + String materialNumber = resolveMaterialNumber(feeCategory, context.project(), fallbackTransportType, sourceDesc); // 结算合计费用项行无运输量,数量默认 1 BigDecimal quantity = BigDecimal.ONE; BigDecimal amount = money(summaryFee.getSettlementAmount()); @@ -560,20 +587,22 @@ public class KingdeeApBillSyncSupport { /** * 物料编码映射(Nacos 配置 thirdparty.bfi.material.*): - * 装卸大类(及手动添加的非运费杂费)映射装卸物料 loading;业务模式/项目业务类型含“国际”(国际运输、 + * 装卸大类映射装卸物料 loading;仓储大类映射仓储物料 warehouse;业务模式/项目业务类型含“国际”(国际运输、 * 国际货物运输代理服务)映射国际货代服务 international;国内运输按运单运输方式查 domestic 映射 * (公路运输/铁路运输/水路运输/航空运输),运输方式未配置映射时直接拦截报错 * - * @param loadingFee 该行是否属于装卸大类(或非运费类杂费) + * @param feeCategory 该行费用大类(运输/仓储/装卸) * @param project 结算单关联项目(业务类型/业务模式取数来源) * @param transportType 运单运输方式(结算合计费用项行取结算明细首行运输方式) * @param sourceDesc 来源行描述,用于异常提示 * @return 金蝶物料编码 */ - private String resolveMaterialNumber(boolean loadingFee, ProjectApply project, String transportType, String sourceDesc) { + private String resolveMaterialNumber(FeeCategory feeCategory, ProjectApply project, String transportType, String sourceDesc) { BfiProperties.Material material = bfiProperties.getMaterial(); String materialNumber; - if (loadingFee) { + if (feeCategory == FeeCategory.WAREHOUSE) { + materialNumber = material.getWarehouse(); + } else if (feeCategory == FeeCategory.LOADING) { materialNumber = material.getLoading(); } else if (isInternationalBusiness(project)) { materialNumber = material.getInternational(); @@ -590,9 +619,9 @@ public class KingdeeApBillSyncSupport { if (Func.isBlank(materialNumber)) { throw new ServiceException("推送金蝶失败:" + sourceDesc + "未在配置 thirdparty.bfi.material 中维护对应物料编码,请补充后重试"); } - // 物料映射判定依据(联调排查:装卸大类/业务模式/项目业务类型/运输方式 -> 物料) - log.info("金蝶物料映射:来源[{}],装卸大类[{}],业务模式[{}],项目业务类型[{}],运单运输方式[{}] -> 物料编码[{}]", - sourceDesc, loadingFee ? "是" : "否", + // 物料映射判定依据(联调排查: 费用大类/业务模式/项目业务类型/运输方式 -> 物料) + log.info("金蝶物料映射: 来源[{}], 费用大类[{}], 业务模式[{}], 项目业务类型[{}], 运单运输方式[{}] -> 物料编码[{}]", + sourceDesc, feeCategory.getLabel(), project == null ? null : project.getBusinessMode(), project == null ? null : project.getBusinessType(), transportType, materialNumber); return materialNumber; @@ -669,7 +698,7 @@ public class KingdeeApBillSyncSupport { /** * 费用项按费用大类/税率拆分(纯函数): - * 费用项经费用项档案判为装卸大类(费用大类编码 A10)、其余归运输大类; + * 费用项经费用项档案判为 装卸(A10)/仓储(A05)/运输 三大类(仓储不做名称关键字兜底); * 各大类内按计费方案规则税率分组(未配置税率的费用项归入无税率组), * 费用项金额合计与结算金额的残差并入运输类无税率组,保证拆分后各行金额合计与结算金额勾稽 * @@ -691,11 +720,11 @@ public class KingdeeApBillSyncSupport { if (amount.signum() == 0) { continue; } - boolean loading = isLoadingCategory(item.getKey(), feeCategoryByName); + FeeCategory category = resolveCategory(item.getKey(), feeCategoryByName); BigDecimal taxRate = taxRateByRule == null ? null : taxRateByRule.get(item.getKey()); - FeeLineSplit split = grouped.get(splitGroupKey(loading, taxRate)); + FeeLineSplit split = grouped.get(splitGroupKey(category, taxRate)); if (split == null) { - grouped.put(splitGroupKey(loading, taxRate), new FeeLineSplit(loading, taxRate, amount, item.getKey())); + grouped.put(splitGroupKey(category, taxRate), new FeeLineSplit(category, taxRate, amount, item.getKey())); } else { split.addAmount(amount); } @@ -704,16 +733,37 @@ public class KingdeeApBillSyncSupport { BigDecimal residual = (rowAmount == null ? BigDecimal.ZERO : rowAmount).subtract(grouped.values().stream() .map(FeeLineSplit::getAmount).reduce(BigDecimal.ZERO, BigDecimal::add)); if (residual.signum() != 0) { - FeeLineSplit freightDefault = grouped.computeIfAbsent(splitGroupKey(false, null), - key -> new FeeLineSplit(false, null, BigDecimal.ZERO, FREIGHT_FEE_ITEM)); + FeeLineSplit freightDefault = grouped.computeIfAbsent(splitGroupKey(FeeCategory.FREIGHT, null), + key -> new FeeLineSplit(FeeCategory.FREIGHT, null, BigDecimal.ZERO, FREIGHT_FEE_ITEM)); freightDefault.addAmount(residual); } return new ArrayList<>(grouped.values()); } - /** 费用大类/税率分组键(装卸大类与运输大类、税率去重分组) */ - private static String splitGroupKey(boolean loading, BigDecimal taxRate) { - return (loading ? "装卸" : "运输费") + "@" + (taxRate == null ? "" : taxRate.stripTrailingZeros().toPlainString()); + /** 费用大类/税率分组键(运输/仓储/装卸大类、税率去重分组) */ + private static String splitGroupKey(FeeCategory category, BigDecimal taxRate) { + return category.getLabel() + "@" + (taxRate == null ? "" : taxRate.stripTrailingZeros().toPlainString()); + } + + /** + * 分录费用大类判定(三分类):费用项档案分类编码 A10 归装卸大类(保留名称关键字兜底现状)、 + * A05 归仓储大类(仓储仅认档案编码,不做名称关键字兜底,分类以档案维护为准),其余归运输大类 + */ + static FeeCategory resolveCategory(String feeItemName, Map feeCategoryByName) { + String categoryCode = feeItemName == null || feeCategoryByName == null ? null : feeCategoryByName.get(feeItemName); + if (FEE_CATEGORY_WAREHOUSE.equals(categoryCode)) { + return FeeCategory.WAREHOUSE; + } + return isLoadingCategory(feeItemName, feeCategoryByName) ? FeeCategory.LOADING : FeeCategory.FREIGHT; + } + + /** + * 仓储大类判定(手动添加结算合计费用行物料映射用):费用项档案维护的费用大类编码为 A05 归仓储大类, + * 无名称关键字兜底,分类以档案维护为准 + */ + static boolean isWarehouseCategory(String feeItemName, Map feeCategoryByName) { + String categoryCode = feeItemName == null || feeCategoryByName == null ? null : feeCategoryByName.get(feeItemName); + return FEE_CATEGORY_WAREHOUSE.equals(categoryCode); } /** 装卸大类费用项判定(关键字兜底):费用项名称含“装卸” */ @@ -781,17 +831,17 @@ public class KingdeeApBillSyncSupport { } /** - * 费用大类/税率拆分行:装卸大类 loading(true=装卸)、税率 taxRate(百分数,未配置为 null)、 + * 费用大类/税率拆分行:费用大类 category(运输/仓储/装卸)、税率 taxRate(百分数,未配置为 null)、 * 组内金额合计 amount、组内首个费用项 firstFeeItem */ public static final class FeeLineSplit { - private final boolean loading; + private final FeeCategory category; private final BigDecimal taxRate; private final String firstFeeItem; private BigDecimal amount; - FeeLineSplit(boolean loading, BigDecimal taxRate, BigDecimal amount, String firstFeeItem) { - this.loading = loading; + FeeLineSplit(FeeCategory category, BigDecimal taxRate, BigDecimal amount, String firstFeeItem) { + this.category = category; this.taxRate = taxRate; this.amount = amount; this.firstFeeItem = firstFeeItem; @@ -801,8 +851,18 @@ public class KingdeeApBillSyncSupport { amount = amount.add(value); } + /** 装卸大类判定(兼容保留) */ public boolean isLoading() { - return loading; + return category == FeeCategory.LOADING; + } + + /** 仓储大类判定 */ + public boolean isWarehouse() { + return category == FeeCategory.WAREHOUSE; + } + + public FeeCategory getCategory() { + return category; } public BigDecimal getTaxRate() { @@ -851,7 +911,7 @@ public class KingdeeApBillSyncSupport { /** * 费用项档案分类:费用项名称 -> 费用大类编码(blade_fee_item.fee_category,业务字典 fee_category 键值, - * 运输 A01 / 装卸 A10;批量同步时全批共享一次加载结果),费用大类判定用; + * 运输 A01 / 装卸 A10 / 仓储 A05;批量同步时全批共享一次加载结果),费用大类判定用; * 档案读取失败直接拦截 */ public Map feeCategoryByName(KingdeeSyncContext context) { @@ -1133,7 +1193,7 @@ public class KingdeeApBillSyncSupport { /** * 财务应付单/付款申请分录:结算明细按 货物→费用大类→税率 拆分行 + 结算合计费用项(手动添加的每行一条) * - * @param materialNumber 物料编码(按装卸大类/项目业务模式/运单运输方式映射,thirdparty.bfi.material.* 配置) + * @param materialNumber 物料编码(按费用大类/项目业务模式/运单运输方式映射,thirdparty.bfi.material.* 配置) * @param entryCode 分录唯一标识(awrp_entry_code):结算明细拆分行取单据号-货物序号-大类-税率,结算合计费用项行取结算单号-费用类型-费用项 * @param quantity 该行分录数量(运输类取运输量,装卸类/其它默认 1) * @param price 该行分录单价(结算金额/数量反算)