fix: 金蝶分录费用大类改按档案编码精确判定
- 费用大类判定弃用字典名称文本匹配,改为档案 blade_fee_item.fee_category 编码比对:运输 A01 / 装卸 A10(写死常量) - 修复 SJS2026101900004 车辆维修费(A01)因字典值「运输」未命中「运费/运输费」关键字被误映射装卸物料的问题 - 档案未维护时回退行费用类型编码与名称关键字判定;金蝶应收/应付分录解析共用,一处修复两侧生效 - 补充分录拆分回归用例(档案编码优先于名称关键字 + fallback 编码判定)
This commit is contained in:
+28
-27
@@ -32,11 +32,9 @@ import org.springblade.core.log.exception.ServiceException;
|
|||||||
import org.springblade.core.tool.api.R;
|
import org.springblade.core.tool.api.R;
|
||||||
import org.springblade.core.tool.jackson.JsonUtil;
|
import org.springblade.core.tool.jackson.JsonUtil;
|
||||||
import org.springblade.core.tool.utils.Func;
|
import org.springblade.core.tool.utils.Func;
|
||||||
import org.springblade.system.cache.DictBizCache;
|
|
||||||
import org.springblade.system.cache.SysCache;
|
import org.springblade.system.cache.SysCache;
|
||||||
import org.springblade.system.feign.ISysClient;
|
import org.springblade.system.feign.ISysClient;
|
||||||
import org.springblade.system.pojo.entity.Dept;
|
import org.springblade.system.pojo.entity.Dept;
|
||||||
import org.springblade.system.pojo.entity.DictBiz;
|
|
||||||
import org.springblade.system.pojo.entity.FeeItem;
|
import org.springblade.system.pojo.entity.FeeItem;
|
||||||
import org.springblade.thirdparty.bfi.pojo.dto.BfiApBillQueryDTO;
|
import org.springblade.thirdparty.bfi.pojo.dto.BfiApBillQueryDTO;
|
||||||
import org.springblade.thirdparty.bfi.pojo.dto.BfiApBillSaveDTO;
|
import org.springblade.thirdparty.bfi.pojo.dto.BfiApBillSaveDTO;
|
||||||
@@ -102,8 +100,10 @@ public class KingdeeApBillSyncSupport {
|
|||||||
private static final int PAY_RESULT_PAGE_SIZE = 200;
|
private static final int PAY_RESULT_PAGE_SIZE = 200;
|
||||||
/** 结算明细行(运费)计费方案匹配费用项(运输类无税率残差组标识用) */
|
/** 结算明细行(运费)计费方案匹配费用项(运输类无税率残差组标识用) */
|
||||||
private static final String FREIGHT_FEE_ITEM = "运输费";
|
private static final String FREIGHT_FEE_ITEM = "运输费";
|
||||||
/** 业务字典:费用类型(fee_category),费用项档案分类编码 -> 名称 */
|
/** 费用大类编码(业务字典 fee_category 键值):运输 */
|
||||||
private static final String FEE_CATEGORY_DICT = "fee_category";
|
public static final String FEE_CATEGORY_FREIGHT = "A01";
|
||||||
|
/** 费用大类编码(业务字典 fee_category 键值):装卸 */
|
||||||
|
public static final String FEE_CATEGORY_LOADING = "A10";
|
||||||
|
|
||||||
private final IBfiService bfiService;
|
private final IBfiService bfiService;
|
||||||
private final ContractManageMapper contractManageMapper;
|
private final ContractManageMapper contractManageMapper;
|
||||||
@@ -424,7 +424,7 @@ public class KingdeeApBillSyncSupport {
|
|||||||
ContractManage contract = requireContract(settlement.getContractId(), context);
|
ContractManage contract = requireContract(settlement.getContractId(), context);
|
||||||
// 合同计费方案规则:费用类型/费用项 -> 税率(拆行分组用)
|
// 合同计费方案规则:费用类型/费用项 -> 税率(拆行分组用)
|
||||||
Map<String, BigDecimal> taxRateByRule = taxRateByRule(contract);
|
Map<String, BigDecimal> taxRateByRule = taxRateByRule(contract);
|
||||||
// 费用项档案:费用项名称 -> 费用类型名称(费用大类判定,字典 fee_category 翻译)
|
// 费用项档案:费用项名称 -> 费用大类编码(费用大类判定)
|
||||||
Map<String, String> feeCategoryByName = feeCategoryByName(context);
|
Map<String, String> feeCategoryByName = feeCategoryByName(context);
|
||||||
EntryResolveContext resolveContext = new EntryResolveContext(settlement, project, taxRateByRule, feeCategoryByName);
|
EntryResolveContext resolveContext = new EntryResolveContext(settlement, project, taxRateByRule, feeCategoryByName);
|
||||||
// 结算明细费用行:一车多货按货物拆分的费用快照行
|
// 结算明细费用行:一车多货按货物拆分的费用快照行
|
||||||
@@ -540,7 +540,7 @@ public class KingdeeApBillSyncSupport {
|
|||||||
String feeType = Func.toStr(summaryFee.getFeeType()).trim();
|
String feeType = Func.toStr(summaryFee.getFeeType()).trim();
|
||||||
String feeItem = Func.toStr(summaryFee.getFeeItem()).trim();
|
String feeItem = Func.toStr(summaryFee.getFeeItem()).trim();
|
||||||
String sourceDesc = "结算合计[" + feeType + "-" + feeItem + "]";
|
String sourceDesc = "结算合计[" + feeType + "-" + feeItem + "]";
|
||||||
// 非运费类(费用项档案分类/费用类型/费用项名称均不含“运费/运输费”)统一映射装卸物料
|
// 非运费类(费用项档案分类编码非 A01 且名称无运费字样)统一映射装卸物料
|
||||||
boolean loadingFee = !isFreightCategory(feeType, feeItem, context.feeCategoryByName());
|
boolean loadingFee = !isFreightCategory(feeType, feeItem, context.feeCategoryByName());
|
||||||
String materialNumber = resolveMaterialNumber(loadingFee, context.project(), fallbackTransportType, sourceDesc);
|
String materialNumber = resolveMaterialNumber(loadingFee, context.project(), fallbackTransportType, sourceDesc);
|
||||||
// 结算合计费用项行无运输量,数量默认 1
|
// 结算合计费用项行无运输量,数量默认 1
|
||||||
@@ -667,12 +667,12 @@ public class KingdeeApBillSyncSupport {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 费用项按费用大类/税率拆分(纯函数):
|
* 费用项按费用大类/税率拆分(纯函数):
|
||||||
* 费用项经费用项档案/业务字典判为装卸大类(名称含“装卸”的费用类型)、其余归运输大类;
|
* 费用项经费用项档案判为装卸大类(费用大类编码 A10)、其余归运输大类;
|
||||||
* 各大类内按计费方案规则税率分组(未配置税率的费用项归入无税率组),
|
* 各大类内按计费方案规则税率分组(未配置税率的费用项归入无税率组),
|
||||||
* 费用项金额合计与结算金额的残差并入运输类无税率组,保证拆分后各行金额合计与结算金额勾稽
|
* 费用项金额合计与结算金额的残差并入运输类无税率组,保证拆分后各行金额合计与结算金额勾稽
|
||||||
*
|
*
|
||||||
* @param feeItems 费用项名称 -> 金额(费用项JSON解析结果,保持行序)
|
* @param feeItems 费用项名称 -> 金额(费用项JSON解析结果,保持行序)
|
||||||
* @param feeCategoryByName 费用项档案:费用项名称 -> 费用类型名称(空 Map 时回退名称关键字判定)
|
* @param feeCategoryByName 费用项档案:费用项名称 -> 费用大类编码(空 Map 时回退名称关键字判定)
|
||||||
* @param taxRateByRule 合同计费方案:费用类型/费用项 -> 税率(百分数)
|
* @param taxRateByRule 合同计费方案:费用类型/费用项 -> 税率(百分数)
|
||||||
* @param rowAmount 货物费用行结算金额(含税),残差勾稽基准
|
* @param rowAmount 货物费用行结算金额(含税),残差勾稽基准
|
||||||
* @return 拆分行列表(保持费用项出现顺序)
|
* @return 拆分行列表(保持费用项出现顺序)
|
||||||
@@ -720,29 +720,33 @@ public class KingdeeApBillSyncSupport {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 装卸大类判定:费用项在费用项档案(blade_fee_item)中的费用类型名称(业务字典 fee_category 翻译)
|
* 装卸大类判定:费用项在费用项档案(blade_fee_item)中的费用大类编码为 A10 归装卸大类;
|
||||||
* 含“装卸”归装卸大类;档案未维护该费用项时回退费用项名称关键字判定
|
* 档案未维护该费用项时回退费用项名称关键字判定
|
||||||
*/
|
*/
|
||||||
static boolean isLoadingCategory(String feeItemName, Map<String, String> feeCategoryByName) {
|
static boolean isLoadingCategory(String feeItemName, Map<String, String> feeCategoryByName) {
|
||||||
String categoryName = feeItemName == null || feeCategoryByName == null ? null : feeCategoryByName.get(feeItemName);
|
String categoryCode = feeItemName == null || feeCategoryByName == null ? null : feeCategoryByName.get(feeItemName);
|
||||||
if (Func.isNotBlank(categoryName)) {
|
if (Func.isNotBlank(categoryCode)) {
|
||||||
return categoryName.contains("装卸");
|
return FEE_CATEGORY_LOADING.equals(categoryCode);
|
||||||
}
|
}
|
||||||
return isLoadingFeeItem(feeItemName);
|
return isLoadingFeeItem(feeItemName);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 运费类费用判定(关键字兜底):名称含“运费”或“运输费” */
|
/** 运费类费用判定(关键字兜底):名称含“运费”或“运输”(业务字典 fee_category 运输类的字典值为“运输”) */
|
||||||
static boolean isFreightFeeText(String name) {
|
static boolean isFreightFeeText(String name) {
|
||||||
return name != null && (name.contains("运费") || name.contains("运输费"));
|
return name != null && (name.contains("运费") || name.contains("运输"));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 运费类判定(手动添加费用行物料映射用):费用项档案分类名称、费用类型、费用项名称任一
|
* 运费类判定(手动添加费用行物料映射用):费用项档案维护的费用大类编码为 A01 归运费类映射运输物料;
|
||||||
* 含“运费/运输费”即归运费类映射运输物料;档案未维护时退化为名称关键字判定(与原口径一致)
|
* 档案未维护时回退行费用类型编码与费用项名称关键字判定
|
||||||
*/
|
*/
|
||||||
static boolean isFreightCategory(String feeTypeName, String feeItemName, Map<String, String> feeCategoryByName) {
|
public static boolean isFreightCategory(String feeTypeCode, String feeItemName, Map<String, String> feeCategoryByName) {
|
||||||
String categoryName = feeItemName == null || feeCategoryByName == null ? null : feeCategoryByName.get(feeItemName);
|
String categoryCode = feeItemName == null || feeCategoryByName == null ? null : feeCategoryByName.get(feeItemName);
|
||||||
return isFreightFeeText(categoryName) || isFreightFeeText(feeTypeName) || isFreightFeeText(feeItemName);
|
if (Func.isNotBlank(categoryCode)) {
|
||||||
|
// 档案维护的费用项以档案分类编码为准
|
||||||
|
return FEE_CATEGORY_FREIGHT.equals(categoryCode);
|
||||||
|
}
|
||||||
|
return FEE_CATEGORY_FREIGHT.equals(feeTypeCode) || isFreightFeeText(feeItemName);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -844,8 +848,8 @@ public class KingdeeApBillSyncSupport {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 费用项档案分类:费用项名称 -> 费用类型名称(blade_fee_item.fee_category 编码经业务字典
|
* 费用项档案分类:费用项名称 -> 费用大类编码(blade_fee_item.fee_category,业务字典 fee_category 键值,
|
||||||
* {@link #FEE_CATEGORY_DICT} 翻译;批量同步时全批共享一次加载结果),费用大类判定用;
|
* 运输 A01 / 装卸 A10;批量同步时全批共享一次加载结果),费用大类判定用;
|
||||||
* 档案读取失败直接拦截
|
* 档案读取失败直接拦截
|
||||||
*/
|
*/
|
||||||
public Map<String, String> feeCategoryByName(KingdeeSyncContext context) {
|
public Map<String, String> feeCategoryByName(KingdeeSyncContext context) {
|
||||||
@@ -853,8 +857,6 @@ public class KingdeeApBillSyncSupport {
|
|||||||
if (!cache.isEmpty()) {
|
if (!cache.isEmpty()) {
|
||||||
return cache;
|
return cache;
|
||||||
}
|
}
|
||||||
Map<String, String> categoryNames = DictBizCache.getList(FEE_CATEGORY_DICT).stream()
|
|
||||||
.collect(Collectors.toMap(DictBiz::getDictKey, DictBiz::getDictValue, (first, second) -> first));
|
|
||||||
R<List<FeeItem>> response = sysClient.getFeeItems(false);
|
R<List<FeeItem>> response = sysClient.getFeeItems(false);
|
||||||
if (response == null || !response.isSuccess() || Func.isEmpty(response.getData())) {
|
if (response == null || !response.isSuccess() || Func.isEmpty(response.getData())) {
|
||||||
throw new ServiceException("推送金蝶失败:费用项基础档案读取失败,请稍后重试");
|
throw new ServiceException("推送金蝶失败:费用项基础档案读取失败,请稍后重试");
|
||||||
@@ -864,10 +866,9 @@ public class KingdeeApBillSyncSupport {
|
|||||||
if (feeItem == null || Func.isBlank(feeItem.getName()) || Func.isBlank(feeItem.getFeeCategory())) {
|
if (feeItem == null || Func.isBlank(feeItem.getName()) || Func.isBlank(feeItem.getFeeCategory())) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
loaded.putIfAbsent(feeItem.getName().trim(),
|
loaded.putIfAbsent(feeItem.getName().trim(), feeItem.getFeeCategory().trim());
|
||||||
categoryNames.getOrDefault(feeItem.getFeeCategory().trim(), feeItem.getFeeCategory().trim()));
|
|
||||||
}
|
}
|
||||||
log.info("费用项档案加载:{}项(费用项名称 -> 费用类型名称)", loaded.size());
|
log.info("费用项档案加载:{}项(费用项名称 -> 费用大类编码)", loaded.size());
|
||||||
cache.putAll(loaded);
|
cache.putAll(loaded);
|
||||||
return cache;
|
return cache;
|
||||||
}
|
}
|
||||||
|
|||||||
+21
-2
@@ -86,11 +86,11 @@ class KingdeeEntrySplitTest {
|
|||||||
assertEquals(new BigDecimal("800"), splits.get(1).getAmount());
|
assertEquals(new BigDecimal("800"), splits.get(1).getAmount());
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 费用项档案分类判定:名称不含“装卸”的费用项,档案费用类型名称含“装卸”即归装卸大类 */
|
/** 费用项档案分类判定:名称不含“装卸”的费用项,档案费用大类编码 A10 即归装卸大类 */
|
||||||
@Test
|
@Test
|
||||||
void loadingCategoryFromFeeItemArchive() {
|
void loadingCategoryFromFeeItemArchive() {
|
||||||
Map<String, BigDecimal> feeItems = Map.of("吊装作业", new BigDecimal("500"));
|
Map<String, BigDecimal> feeItems = Map.of("吊装作业", new BigDecimal("500"));
|
||||||
Map<String, String> feeCategoryByName = Map.of("吊装作业", "装卸");
|
Map<String, String> feeCategoryByName = Map.of("吊装作业", "A10");
|
||||||
List<FeeLineSplit> splits = KingdeeApBillSyncSupport.splitFeeItems(feeItems, feeCategoryByName, Map.of(), new BigDecimal("500"));
|
List<FeeLineSplit> splits = KingdeeApBillSyncSupport.splitFeeItems(feeItems, feeCategoryByName, Map.of(), new BigDecimal("500"));
|
||||||
assertEquals(1, splits.size());
|
assertEquals(1, splits.size());
|
||||||
assertTrue(splits.get(0).isLoading());
|
assertTrue(splits.get(0).isLoading());
|
||||||
@@ -166,4 +166,23 @@ class KingdeeEntrySplitTest {
|
|||||||
List<FeeLineSplit> splits = KingdeeApBillSyncSupport.splitFeeItems(feeItems, Map.of(), Map.of(), BigDecimal.ZERO);
|
List<FeeLineSplit> splits = KingdeeApBillSyncSupport.splitFeeItems(feeItems, Map.of(), Map.of(), BigDecimal.ZERO);
|
||||||
assertTrue(splits.isEmpty());
|
assertTrue(splits.isEmpty());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 手工合计行运费类判定:费用项档案分类编码 A01 归运费类映射运输物料(SJS2026101900004 车辆维修费误映射装卸物料回归用例) */
|
||||||
|
@Test
|
||||||
|
void freightCategoryMatchesArchiveCode() {
|
||||||
|
Map<String, String> feeCategoryByName = Map.of(
|
||||||
|
"车辆维修费", "A01",
|
||||||
|
"人工装卸费用", "A10");
|
||||||
|
assertTrue(KingdeeApBillSyncSupport.isFreightCategory("A01", "车辆维修费", feeCategoryByName));
|
||||||
|
assertFalse(KingdeeApBillSyncSupport.isFreightCategory("A10", "人工装卸费用", feeCategoryByName));
|
||||||
|
// 档案编码优先于名称关键字:A02 档案的“装卸搬运作业费”不归装卸大类
|
||||||
|
Map<String, BigDecimal> feeItems = Map.of("装卸搬运作业费", new BigDecimal("100"));
|
||||||
|
List<FeeLineSplit> splits = KingdeeApBillSyncSupport.splitFeeItems(feeItems,
|
||||||
|
Map.of("装卸搬运作业费", "A02"), Map.of(), new BigDecimal("100"));
|
||||||
|
assertFalse(splits.get(0).isLoading());
|
||||||
|
// 档案未维护:回退行费用类型编码与名称关键字判定
|
||||||
|
assertTrue(KingdeeApBillSyncSupport.isFreightCategory("A01", "车辆维修费", Map.of()));
|
||||||
|
assertFalse(KingdeeApBillSyncSupport.isFreightCategory("A10", "人工装卸费用", Map.of()));
|
||||||
|
assertTrue(KingdeeApBillSyncSupport.isFreightCategory("", "运输费", Map.of()));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user