!5 金蝶测试问题修复

Merge pull request !5 from mindless/feature/20261019-bfi-all
This commit is contained in:
2026-10-27 06:23:34 +00:00
committed by Gitee
13 changed files with 542 additions and 68 deletions
@@ -84,10 +84,9 @@ public class InvoiceApplicationController extends BladeController {
public R<IPage<Map<String, Object>>> settlementCandidates(Query query,
@RequestParam(required = false) String keyword,
@RequestParam(required = false) String contractCategory,
@RequestParam(defaultValue = "receivable") String settlementType,
@RequestParam(defaultValue = "unreceived") String invoiceStatus) {
@RequestParam(defaultValue = "receivable") String settlementType) {
return R.data(invoiceApplicationService.settlementCandidates(
Condition.getPage(query), keyword, contractCategory, settlementType, invoiceStatus));
Condition.getPage(query), keyword, contractCategory, settlementType));
}
@GetMapping("/settlement-details")
@@ -45,7 +45,7 @@ public interface IInvoiceApplicationService extends BaseService<InvoiceApplicati
IPage<InvoiceApplicationVO> selectPage(IPage<InvoiceApplication> page, InvoiceApplicationVO query);
InvoiceApplicationVO detail(Long id);
IPage<Map<String, Object>> settlementCandidates(IPage<?> page, String keyword, String contractCategory,
String settlementType, String invoiceStatus);
String settlementType);
List<FormalSettlementDetail> settlementDetails(String settlementIds);
Map<String, Object> receiverInformation(String settlementIds);
Long saveDraft(InvoiceApplicationSaveRequest request);
@@ -274,7 +274,8 @@ public class CustomerArchiveServiceImpl extends BaseServiceImpl<CustomerArchiveM
boolean created = Func.isEmpty(customer.getId());
CustomerArchive before = created ? null : ensureCustomerAccessible(customer.getId());
if (created) {
customer.setCustomerCode(nextCustomerCode());
// 新增时金蝶已有客商,前端回填金蝶客商编码,建档直接沿用;未回填则自动生成
customer.setCustomerCode(Func.isEmpty(customer.getCustomerCode()) ? this.nextCustomerCode() : customer.getCustomerCode());
}
CustomerArchive entity = Objects.requireNonNull(BeanUtil.copyProperties(customer, CustomerArchive.class));
// 曾审核通过后的评分替换:保存/再提交时仍沿用原生效等级与额度,待再次审核通过后再切换
@@ -516,10 +517,10 @@ public class CustomerArchiveServiceImpl extends BaseServiceImpl<CustomerArchiveM
after.setKingdeeSyncTime(LocalDateTime.now());
BfiCustomerVO kingdeeCustomer = queryKingdeeCustomer(customer.getUnifiedCreditCode());
if (kingdeeCustomer != null && BfiConstant.BILL_STATUS_AUDITED.equals(kingdeeCustomer.getStatus())) {
// 金蝶已有已审核档案:跳过推送
// 金蝶已有已审核档案:跳过推送,客商编号采纳金蝶客商编码
after.setKingdeeStatus(KINGDEE_STATUS_EXIST);
after.setKingdeeFailReason(null);
addChangeRecord(id, "金蝶已存在该客商档案,跳过推送", customer, after);
addChangeRecord(id, "金蝶已存在该客商档案,跳过推送" + this.adoptKingdeeCode(customer, after, kingdeeCustomer.getNumber()), customer, after);
updateById(after);
return after;
}
@@ -618,6 +619,31 @@ public class CustomerArchiveServiceImpl extends BaseServiceImpl<CustomerArchiveM
return rows == null || rows.isEmpty() ? null : rows.get(0);
}
/**
* 金蝶已存在档案时,客商编号(customer_code)采纳金蝶客商编码,后续业务接口沿用客商编号即拿到金蝶编码;
* 金蝶编码为空或与当前编号一致时无需处理;同租户下已被其他客商占用时跳过回填,避免违反 uk_customer_archive_code 唯一约束
*
* @param customer 变更前档案
* @param after 变更后档案(编码采纳结果回填其上)
* @param kingdeeNumber A902-1 查询返回的金蝶客商编码
* @return 变更说明后缀,无需处理返回空串
*/
private String adoptKingdeeCode(CustomerArchive customer, CustomerArchive after, String kingdeeNumber) {
if (Func.isEmpty(kingdeeNumber) || kingdeeNumber.equals(customer.getCustomerCode())) {
return "";
}
long occupied = count(Wrappers.<CustomerArchive>lambdaQuery()
.eq(CustomerArchive::getCustomerCode, kingdeeNumber)
.ne(CustomerArchive::getId, customer.getId()));
if (occupied > 0) {
log.warn("金蝶客商编码 {} 已被其他客商占用,跳过采纳客商编号,id={},customerCode={}",
kingdeeNumber, customer.getId(), customer.getCustomerCode());
return ",金蝶客商编码 " + kingdeeNumber + " 已被占用未同步";
}
after.setCustomerCode(kingdeeNumber);
return ",客商编号已同步为金蝶客商编码:" + kingdeeNumber;
}
private BfiBillSaveResultItemVO firstSaveItem(BfiBillSaveResultVO result) {
if (result == null || result.getResult() == null || result.getResult().isEmpty()) {
return null;
@@ -740,11 +766,11 @@ public class CustomerArchiveServiceImpl extends BaseServiceImpl<CustomerArchiveM
CustomerArchive after = copyCustomer(customer);
after.setKingdeeSyncTime(LocalDateTime.now());
if (BfiConstant.BILL_STATUS_AUDITED.equals(kingdeeCustomer.getStatus())) {
// 金蝶已存在已审核档案:跳过推送,直接建档
// 金蝶已存在已审核档案:跳过推送直接建档,客商编号采纳金蝶客商编码
after.setKingdeeStatus(KINGDEE_STATUS_EXIST);
after.setKingdeeFailReason(null);
existed++;
collectChangeRecord(changeRecords, customer.getId(), "金蝶已存在该客商档案,跳过推送", customer, after);
collectChangeRecord(changeRecords, customer.getId(), "金蝶已存在该客商档案,跳过推送" + this.adoptKingdeeCode(customer, after, kingdeeCustomer.getNumber()), customer, after);
} else {
// 金蝶侧暂存/已提交:审核中,置失败待重推
after.setKingdeeStatus(KINGDEE_STATUS_FAILED);
@@ -822,6 +848,7 @@ public class CustomerArchiveServiceImpl extends BaseServiceImpl<CustomerArchiveM
CustomerArchive after = copyCustomer(customer);
after.setKingdeeStatus(KINGDEE_STATUS_EXIST);
after.setKingdeeFailReason(null);
this.adoptKingdeeCode(customer, after, kingdeeCustomer.getNumber());
updateList.add(after);
collectChangeRecord(changeRecords, customer.getId(), "金蝶客商已审核,推送最终成功", customer, after);
log.info("金蝶客商已审核,推送最终成功并升级为金蝶已有档案,id={},unifiedCreditCode={}",
@@ -955,6 +982,7 @@ public class CustomerArchiveServiceImpl extends BaseServiceImpl<CustomerArchiveM
}
private void prepare(CustomerArchiveVO customer) {
customer.setCustomerCode(trimToNull(customer.getCustomerCode()));
customer.setShortName(trimToNull(customer.getShortName()));
customer.setFullName(trimToEmpty(customer.getFullName()));
customer.setCustomerKind(normalizeCustomerKind(customer.getCustomerKind()));
@@ -1046,6 +1074,7 @@ public class CustomerArchiveServiceImpl extends BaseServiceImpl<CustomerArchiveM
throw new ServiceException("固定营业期限必须填写营业期限截止日");
}
validateLength(customer.getShortName(), 20, "客商简称最多20个汉字");
validateLength(customer.getCustomerCode(), 20, "客商编号最多20个字符");
validateLength(customer.getFullName(), 100, "客商全称最多100个字符");
validateLength(customer.getLegalPerson(), 10, "法人/负责人最多10个汉字");
validateLength(customer.getContactPhone(), 11, "联系电话必须为11位手机号");
@@ -85,6 +85,7 @@ import org.springblade.transport.pojo.entity.SettlementAdjustment;
import org.springblade.transport.pojo.entity.SettlementProjectAdvanceWriteOff;
import org.springblade.transport.pojo.entity.Waybill;
import org.springblade.transport.pojo.vo.FormalSettlementVO;
import org.springblade.transport.pojo.vo.PaymentApplicationVO;
import org.springblade.transport.pojo.vo.PreSettlementVO;
import org.springblade.transport.pojo.vo.SettlementAdvancePaymentVO;
import org.springblade.transport.pojo.vo.SettlementAdjustmentVO;
@@ -327,13 +328,14 @@ public class FormalSettlementServiceImpl extends BaseServiceImpl<FormalSettlemen
vo.setProjectAdvanceWriteOffs(projectAdvanceWriteOffService.listBySettlement(
TransportBusiConstant.SETTLEMENT_BILL_FORMAL, id));
vo.setAdvancePayments(this.listAdvancePayments(sources, vo.getProjectAdvanceWriteOffs()));
Set<Long> relationApplicationIds = paymentApplicationSettlementMapper.selectList(
Map<Long, PaymentApplicationSettlement> relationByApplicationId = paymentApplicationSettlementMapper.selectList(
Wrappers.<PaymentApplicationSettlement>lambdaQuery()
.select(PaymentApplicationSettlement::getPaymentApplicationId)
.eq(PaymentApplicationSettlement::getFormalSettlementId, id)
.eq(PaymentApplicationSettlement::getIsDeleted, 0)).stream()
.map(PaymentApplicationSettlement::getPaymentApplicationId).filter(Objects::nonNull)
.collect(Collectors.toSet());
.filter(relation -> relation.getPaymentApplicationId() != null)
.collect(Collectors.toMap(PaymentApplicationSettlement::getPaymentApplicationId, relation -> relation,
(first, second) -> first));
Set<Long> relationApplicationIds = relationByApplicationId.keySet();
LambdaQueryWrapper<PaymentApplication> paymentApplicationQuery = Wrappers.<PaymentApplication>lambdaQuery();
paymentApplicationQuery.and(query -> {
query.eq(PaymentApplication::getSettlementId, id);
@@ -345,7 +347,16 @@ public class FormalSettlementServiceImpl extends BaseServiceImpl<FormalSettlemen
.eq(PaymentApplication::getIsDeleted, 0);
vo.setPaymentApplications(paymentApplicationMapper.selectList(paymentApplicationQuery
.orderByDesc(PaymentApplication::getCreateTime)).stream()
.map(item -> PaymentApplicationWrapper.build().entityVO(item)).toList());
.map(item -> {
PaymentApplicationVO application = PaymentApplicationWrapper.build().entityVO(item);
// 多结算单合并付款时,付款申请金额为整单合计;详情按当前结算单关联行展示分摊金额
PaymentApplicationSettlement relation = relationByApplicationId.get(item.getId());
if (relation != null) {
application.setAppliedAmount(money(relation.getAppliedAmount()));
application.setPaidAmount(money(relation.getPaidAmount()));
}
return application;
}).toList());
vo.setAdjustments(settlementAdjustmentMapper.selectList(Wrappers.<SettlementAdjustment>lambdaQuery()
.eq(SettlementAdjustment::getFormalSettlementId, id)
.orderByDesc(SettlementAdjustment::getCreateTime)).stream()
@@ -1374,6 +1385,9 @@ public class FormalSettlementServiceImpl extends BaseServiceImpl<FormalSettlemen
List<FormalSettlementSummaryFee> existingManualRows = existingRows.stream()
.filter(row -> Integer.valueOf(1).equals(row.getManualFlag())).toList();
Map<String, BigDecimal> aggregates = new LinkedHashMap<>();
// 预结算手工费用行不并入自动汇总:金蝶分录只送手工行(manualFlag=1),并入自动汇总
// 会被当作明细再汇总跳过,推送金蝶时金额缺失(如预付单手工水费转正式后丢失)
Map<String, BigDecimal> sourceManualAmounts = new LinkedHashMap<>();
for (FormalSettlementSource source : sourceMapper.selectList(Wrappers.<FormalSettlementSource>lambdaQuery()
.eq(FormalSettlementSource::getFormalSettlementId, settlementId))) {
List<PreSettlementSummaryFee> sourceSummary = preSummaryFeeMapper.selectList(
@@ -1381,9 +1395,12 @@ public class FormalSettlementServiceImpl extends BaseServiceImpl<FormalSettlemen
.eq(PreSettlementSummaryFee::getPreSettlementId, source.getPreSettlementId())
.eq(PreSettlementSummaryFee::getIsDeleted, 0));
sourceSummary.forEach(row -> {
BigDecimal additionalAmount = Integer.valueOf(1).equals(row.getManualFlag())
? money(row.getSettlementAmount()) : money(row.getAdjustAmount());
aggregates.merge(summaryKey(row.getFeeType(), row.getFeeItem()), additionalAmount, BigDecimal::add);
if (Integer.valueOf(1).equals(row.getManualFlag())) {
sourceManualAmounts.merge(summaryKey(row.getFeeType(), row.getFeeItem()),
money(row.getSettlementAmount()), BigDecimal::add);
} else {
aggregates.merge(summaryKey(row.getFeeType(), row.getFeeItem()), money(row.getAdjustAmount()), BigDecimal::add);
}
});
}
Map<String, String> feeTypeMap = feeTypeMap();
@@ -1418,6 +1435,25 @@ public class FormalSettlementServiceImpl extends BaseServiceImpl<FormalSettlemen
manualRow.setLineNo(lineNo++);
summaryFeeMapper.updateById(manualRow);
}
// 预结算手工费用行结转为正式手工行(保留 manualFlag=1 供金蝶分录取数);已存在同键手工行时沿用页面维护值不重复插入
for (Map.Entry<String, BigDecimal> entry : sourceManualAmounts.entrySet()) {
boolean existed = existingManualRows.stream().anyMatch(row -> summaryKey(row).equals(entry.getKey()));
if (existed) {
continue;
}
String[] keyParts = entry.getKey().split("@@@", 2);
FormalSettlementSummaryFee manualRow = new FormalSettlementSummaryFee();
manualRow.setFormalSettlementId(settlementId);
manualRow.setFeeType(keyParts[0]);
manualRow.setFeeItem(keyParts.length > 1 ? keyParts[1] : "");
manualRow.setOriginalAmount(BigDecimal.ZERO.setScale(2));
manualRow.setAdjustAmount(money(entry.getValue()));
manualRow.setSettlementAmount(money(entry.getValue()));
manualRow.setRemark("");
manualRow.setManualFlag(1);
manualRow.setLineNo(lineNo++);
summaryFeeMapper.insert(manualRow);
}
renumberSummaryFees(settlementId);
}
@@ -1514,7 +1550,14 @@ public class FormalSettlementServiceImpl extends BaseServiceImpl<FormalSettlemen
}
private void applySummaryRequest(Long settlementId, List<FormalSettlementSaveRequest.SummaryFee> requestRows) {
if (requestRows == null) return;
if (requestRows == null) {
return;
}
// 空列表表示沿用 rebuildSummaryFees 结果(如预结算转正式结转的手工合计行),不得按空请求删光手工行
if (requestRows.isEmpty()) {
renumberSummaryFees(settlementId);
return;
}
Map<String, Set<String>> allowedManualFees = new LinkedHashMap<>();
for (Map<String, Object> option : preSettlementService.feeOptions()) {
Set<String> feeItems = new LinkedHashSet<>();
@@ -231,9 +231,23 @@ public class InvoiceApplicationServiceImpl extends BaseServiceImpl<InvoiceApplic
InvoiceApplication entity = existing(id);
InvoiceApplicationVO vo = toListVO(entity);
fillKingdeeLockStatus(List.of(vo));
vo.setSettlements(settlementRelationMapper.selectList(Wrappers.<InvoiceApplicationSettlement>lambdaQuery()
.eq(InvoiceApplicationSettlement::getInvoiceApplicationId, id)
.orderByAsc(InvoiceApplicationSettlement::getCreateTime)));
// 剩余可开票金额按当前占用实时重算(排除本申请),避免编辑时展示保存时点的过期额度
List<InvoiceApplicationSettlement> relations = settlementRelationMapper.selectList(
Wrappers.<InvoiceApplicationSettlement>lambdaQuery()
.eq(InvoiceApplicationSettlement::getInvoiceApplicationId, id)
.orderByAsc(InvoiceApplicationSettlement::getCreateTime));
if (!relations.isEmpty()) {
Map<Long, FormalSettlement> settlementMap = formalSettlementMapper.selectBatchIds(
relations.stream().map(InvoiceApplicationSettlement::getFormalSettlementId).distinct().toList())
.stream().collect(Collectors.toMap(FormalSettlement::getId, Function.identity(), (first, duplicate) -> first));
relations.forEach(relation -> {
FormalSettlement settlement = settlementMap.get(relation.getFormalSettlementId());
if (settlement != null) {
relation.setAvailableInvoiceAmount(availableAmount(settlement, id));
}
});
}
vo.setSettlements(relations);
List<InvoiceApplicationSheet> sheets = sheetMapper.selectList(Wrappers.<InvoiceApplicationSheet>lambdaQuery()
.eq(InvoiceApplicationSheet::getInvoiceApplicationId, id).orderByAsc(InvoiceApplicationSheet::getSheetNo));
vo.setSheets(sheets.stream().map(sheet -> {
@@ -255,9 +269,8 @@ public class InvoiceApplicationServiceImpl extends BaseServiceImpl<InvoiceApplic
@Override
public IPage<Map<String, Object>> settlementCandidates(IPage<?> page, String keyword,
String contractCategory, String settlementType, String invoiceStatus) {
String contractCategory, String settlementType) {
String candidateSettlementType = Func.isEmpty(settlementType) ? "receivable" : settlementType;
String candidateInvoiceStatus = Func.isEmpty(invoiceStatus) ? "unreceived" : invoiceStatus;
List<Long> contractIds = Func.isEmpty(contractCategory) ? List.of()
: contractManageMapper.selectList(Wrappers.<ContractManage>lambdaQuery()
.select(ContractManage::getId)
@@ -269,7 +282,6 @@ public class InvoiceApplicationServiceImpl extends BaseServiceImpl<InvoiceApplic
}
List<FormalSettlement> settlements = formalSettlementMapper.selectList(Wrappers.<FormalSettlement>lambdaQuery()
.eq(FormalSettlement::getSettlementType, candidateSettlementType)
.eq(FormalSettlement::getInvoiceStatus, candidateInvoiceStatus)
.eq(FormalSettlement::getApprovalStatus, TransportBusiConstant.APPROVED)
.eq(FormalSettlement::getStatus, 1)
.in(Func.isNotEmpty(contractCategory), FormalSettlement::getContractId, contractIds)
@@ -337,6 +349,7 @@ public class InvoiceApplicationServiceImpl extends BaseServiceImpl<InvoiceApplic
row.put("receiverName", settlement.getPayerName());
row.put("settlementAmount", money(settlement.getSettlementAmount()));
row.put("availableInvoiceAmount", available);
row.put("invoicedAmount", money(settlement.getSettlementAmount()).subtract(available));
return row;
}
@@ -402,28 +415,31 @@ public class InvoiceApplicationServiceImpl extends BaseServiceImpl<InvoiceApplic
.collect(Collectors.toMap(InvoiceApplicationSaveRequest.SettlementRow::getSettlementId,
Function.identity(), (first, duplicate) -> first, LinkedHashMap::new)).values().stream().toList();
List<FormalSettlement> settlements = requestedRows.stream().map(row -> availableSettlement(row.getSettlementId())).toList();
if (settlements.stream().anyMatch(settlement -> !"receivable".equals(settlement.getSettlementType())
|| (creating && !"unreceived".equals(settlement.getInvoiceStatus())))) {
throw new ServiceException("只能选择审批通过、未作废、未收票的应收正式结算单");
if (settlements.stream().anyMatch(settlement -> !"receivable".equals(settlement.getSettlementType()))) {
throw new ServiceException("只能选择审批通过、未作废的应收正式结算单");
}
assertCompatible(settlements);
FormalSettlement first = settlements.get(0);
Map<Long, FormalSettlement> settlementMap = settlements.stream()
.collect(Collectors.toMap(FormalSettlement::getId, Function.identity()));
BigDecimal totalAvailable = BigDecimal.ZERO;
for (InvoiceApplicationSaveRequest.SettlementRow row : requestedRows) {
FormalSettlement settlement = settlementMap.get(row.getSettlementId());
BigDecimal available = availableAmount(settlement, entity.getId());
BigDecimal allocated = nonNegative(row.getAllocatedInvoiceAmount(), "分摊发票金额");
if (allocated.compareTo(available) > 0) {
throw new ServiceException("结算单" + settlement.getFormalSettlementNo() + "的分摊金额超过剩余可开票金额");
}
totalAvailable = totalAvailable.add(available);
}
BigDecimal lineTotal = validateSheets(request.getSheets());
if (lineTotal.compareTo(BigDecimal.ZERO) <= 0) {
throw new ServiceException("本次开票金额必须大于0");
}
// 本次开票金额按选择顺序自动分摊到各结算单并登记累计占用,摊不完即超过可开票金额
BigDecimal totalAvailable = BigDecimal.ZERO;
BigDecimal remaining = lineTotal;
for (InvoiceApplicationSaveRequest.SettlementRow row : requestedRows) {
FormalSettlement settlement = settlementMap.get(row.getSettlementId());
BigDecimal available = availableAmount(settlement, entity.getId());
totalAvailable = totalAvailable.add(available);
BigDecimal allocated = available.min(remaining);
row.setAllocatedInvoiceAmount(allocated);
remaining = remaining.subtract(allocated);
}
if (remaining.compareTo(BigDecimal.ZERO) > 0) {
throw new ServiceException("本次开票金额不能超过可开票金额");
}
CustomerArchive customer = findCustomer(first.getPayerName());
if (customer == null) throw new ServiceException("未找到正式结算单受票方对应的客商档案");
List<CustomerInvoiceInfoVO> invoiceInfos = activeInvoiceInfoVOs(customer.getId());
@@ -32,11 +32,9 @@ import org.springblade.core.log.exception.ServiceException;
import org.springblade.core.tool.api.R;
import org.springblade.core.tool.jackson.JsonUtil;
import org.springblade.core.tool.utils.Func;
import org.springblade.system.cache.DictBizCache;
import org.springblade.system.cache.SysCache;
import org.springblade.system.feign.ISysClient;
import org.springblade.system.pojo.entity.Dept;
import org.springblade.system.pojo.entity.DictBiz;
import org.springblade.system.pojo.entity.FeeItem;
import org.springblade.thirdparty.bfi.pojo.dto.BfiApBillQueryDTO;
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 String FREIGHT_FEE_ITEM = "运输费";
/** 业务字典:费用类型(fee_category),费用项档案分类编码 -> 名称 */
private static final String FEE_CATEGORY_DICT = "fee_category";
/** 费用大类编码(业务字典 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 ContractManageMapper contractManageMapper;
@@ -424,7 +424,7 @@ public class KingdeeApBillSyncSupport {
ContractManage contract = requireContract(settlement.getContractId(), context);
// 合同计费方案规则:费用类型/费用项 -> 税率(拆行分组用)
Map<String, BigDecimal> taxRateByRule = taxRateByRule(contract);
// 费用项档案:费用项名称 -> 费用类型名称(费用大类判定,字典 fee_category 翻译)
// 费用项档案:费用项名称 -> 费用大类编码(费用大类判定)
Map<String, String> feeCategoryByName = feeCategoryByName(context);
EntryResolveContext resolveContext = new EntryResolveContext(settlement, project, taxRateByRule, feeCategoryByName);
// 结算明细费用行:一车多货按货物拆分的费用快照行
@@ -540,7 +540,7 @@ 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);
// 结算合计费用项行无运输量,数量默认 1
@@ -667,12 +667,12 @@ public class KingdeeApBillSyncSupport {
/**
* 费用项按费用大类/税率拆分(纯函数):
* 费用项经费用项档案/业务字典判为装卸大类(名称含“装卸”的费用类型)、其余归运输大类;
* 费用项经费用项档案判为装卸大类(费用大类编码 A10)、其余归运输大类;
* 各大类内按计费方案规则税率分组(未配置税率的费用项归入无税率组),
* 费用项金额合计与结算金额的残差并入运输类无税率组,保证拆分后各行金额合计与结算金额勾稽
*
* @param feeItems 费用项名称 -> 金额(费用项JSON解析结果,保持行序)
* @param feeCategoryByName 费用项档案:费用项名称 -> 费用类型名称(空 Map 时回退名称关键字判定)
* @param feeCategoryByName 费用项档案:费用项名称 -> 费用大类编码(空 Map 时回退名称关键字判定)
* @param taxRateByRule 合同计费方案:费用类型/费用项 -> 税率(百分数)
* @param rowAmount 货物费用行结算金额(含税),残差勾稽基准
* @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) {
String categoryName = feeItemName == null || feeCategoryByName == null ? null : feeCategoryByName.get(feeItemName);
if (Func.isNotBlank(categoryName)) {
return categoryName.contains("装卸");
String categoryCode = feeItemName == null || feeCategoryByName == null ? null : feeCategoryByName.get(feeItemName);
if (Func.isNotBlank(categoryCode)) {
return FEE_CATEGORY_LOADING.equals(categoryCode);
}
return isLoadingFeeItem(feeItemName);
}
/** 运费类费用判定(关键字兜底):名称含“运费”或“运输费” */
/** 运费类费用判定(关键字兜底):名称含“运费”或“运输”(业务字典 fee_category 运输类的字典值为“运输”) */
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) {
String categoryName = feeItemName == null || feeCategoryByName == null ? null : feeCategoryByName.get(feeItemName);
return isFreightFeeText(categoryName) || isFreightFeeText(feeTypeName) || isFreightFeeText(feeItemName);
public static boolean isFreightCategory(String feeTypeCode, String feeItemName, Map<String, String> feeCategoryByName) {
String categoryCode = feeItemName == null || feeCategoryByName == null ? null : feeCategoryByName.get(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 编码经业务字典
* {@link #FEE_CATEGORY_DICT} 翻译;批量同步时全批共享一次加载结果),费用大类判定用;
* 费用项档案分类:费用项名称 -> 费用大类编码(blade_fee_item.fee_category,业务字典 fee_category 键值,
* 运输 A01 / 装卸 A10;批量同步时全批共享一次加载结果),费用大类判定用;
* 档案读取失败直接拦截
*/
public Map<String, String> feeCategoryByName(KingdeeSyncContext context) {
@@ -853,8 +857,6 @@ public class KingdeeApBillSyncSupport {
if (!cache.isEmpty()) {
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);
if (response == null || !response.isSuccess() || Func.isEmpty(response.getData())) {
throw new ServiceException("推送金蝶失败:费用项基础档案读取失败,请稍后重试");
@@ -864,10 +866,9 @@ public class KingdeeApBillSyncSupport {
if (feeItem == null || Func.isBlank(feeItem.getName()) || Func.isBlank(feeItem.getFeeCategory())) {
continue;
}
loaded.putIfAbsent(feeItem.getName().trim(),
categoryNames.getOrDefault(feeItem.getFeeCategory().trim(), feeItem.getFeeCategory().trim()));
loaded.putIfAbsent(feeItem.getName().trim(), feeItem.getFeeCategory().trim());
}
log.info("费用项档案加载:{}项(费用项名称 -> 费用类型名称)", loaded.size());
log.info("费用项档案加载:{}项(费用项名称 -> 费用大类编码)", loaded.size());
cache.putAll(loaded);
return cache;
}
@@ -86,11 +86,11 @@ class KingdeeEntrySplitTest {
assertEquals(new BigDecimal("800"), splits.get(1).getAmount());
}
/** 费用项档案分类判定:名称不含“装卸”的费用项,档案费用类型名称含“装卸”即归装卸大类 */
/** 费用项档案分类判定:名称不含“装卸”的费用项,档案费用大类编码 A10 即归装卸大类 */
@Test
void loadingCategoryFromFeeItemArchive() {
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"));
assertEquals(1, splits.size());
assertTrue(splits.get(0).isLoading());
@@ -166,4 +166,23 @@ class KingdeeEntrySplitTest {
List<FeeLineSplit> splits = KingdeeApBillSyncSupport.splitFeeItems(feeItems, Map.of(), Map.of(), BigDecimal.ZERO);
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()));
}
}