feat: 金蝶应付分录拆分新增仓储大类并映射仓储物料
This commit is contained in:
+5
@@ -230,6 +230,11 @@ public class BfiProperties {
|
||||
* 装卸大类、手动添加非运费杂费物料编码(装卸)
|
||||
*/
|
||||
private String loading = "XDWL11002004";
|
||||
|
||||
/**
|
||||
* 仓储大类、手动添加仓储费用物料编码(仓储)
|
||||
*/
|
||||
private String warehouse = "XDWL11002001";
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+100
-40
@@ -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 {
|
||||
* 解析单张正式结算单分录:页面结算明细每行(一个运单)按 货物 → 费用大类 → 税率 拆分多行
|
||||
* + 手动添加的结算合计费用项每行一条(系统自动汇总行是明细金额的再汇总不送)。
|
||||
* <p>取数口径:
|
||||
* 结算明细拆行——一车多货(存在货物费用行)先按货物拆;费用项名称含“装卸”归装卸大类、其余归运输大类,
|
||||
* 各大类内按合同计费方案规则税率分组、不同税率分行,运输类每行数量取运单运输量、装卸类每行数量默认 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<BfiEntryGroup> 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<String, String> 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<String, String> 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<String, String> 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 该行分录单价(结算金额/数量反算)
|
||||
|
||||
Reference in New Issue
Block a user