feat: 金蝶应付/应收/收票单推送支撑组件重构——结算明细行与结算合计费用项同口径,物料/计量单位映射完善
This commit is contained in:
+435
-223
@@ -17,7 +17,6 @@
|
||||
* <p>
|
||||
* Using this software signifies agreement to this License, and the software
|
||||
* must not be used for illegal purposes.
|
||||
* <p>
|
||||
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
|
||||
* not liable for any claims arising from secondary or illegal development.
|
||||
* <p>
|
||||
@@ -30,11 +29,12 @@ import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
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.SysCache;
|
||||
import org.springblade.system.feign.ISysClient;
|
||||
import org.springblade.system.pojo.entity.CargoType;
|
||||
import org.springblade.system.pojo.entity.Dept;
|
||||
import org.springblade.system.pojo.entity.MeasurementUnit;
|
||||
import org.springblade.thirdparty.bfi.pojo.dto.BfiApBillQueryDTO;
|
||||
import org.springblade.thirdparty.bfi.pojo.dto.BfiApBillSaveDTO;
|
||||
import org.springblade.thirdparty.bfi.pojo.dto.BfiApBillSaveEntryDTO;
|
||||
@@ -48,15 +48,16 @@ import org.springblade.transport.constant.KingdeeBfiConstant;
|
||||
import org.springblade.transport.mapper.ContractManageMapper;
|
||||
import org.springblade.transport.mapper.CustomerArchiveMapper;
|
||||
import org.springblade.transport.mapper.FormalSettlementDetailFeeMapper;
|
||||
import org.springblade.transport.mapper.PreSettlementDetailFeeMapper;
|
||||
import org.springblade.transport.mapper.ReceivablePayableCargoFeeMapper;
|
||||
import org.springblade.transport.mapper.FormalSettlementMapper;
|
||||
import org.springblade.transport.mapper.FormalSettlementSummaryFeeMapper;
|
||||
import org.springblade.transport.mapper.ProjectApplyMapper;
|
||||
import org.springblade.transport.pojo.entity.ContractManage;
|
||||
import org.springblade.transport.pojo.entity.CustomerArchive;
|
||||
import org.springblade.transport.pojo.entity.FormalSettlement;
|
||||
import org.springblade.transport.pojo.entity.FormalSettlementDetail;
|
||||
import org.springblade.transport.pojo.entity.FormalSettlementDetailFee;
|
||||
import org.springblade.transport.pojo.entity.PreSettlementDetailFee;
|
||||
import org.springblade.transport.pojo.entity.ReceivablePayableCargoFee;
|
||||
import org.springblade.transport.pojo.entity.FormalSettlementSummaryFee;
|
||||
import org.springblade.transport.pojo.entity.ProjectApply;
|
||||
import org.springblade.transport.service.KingdeeSyncContext;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@@ -65,7 +66,6 @@ import java.math.RoundingMode;
|
||||
import java.time.LocalDate;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
@@ -81,6 +81,10 @@ import java.util.stream.Collectors;
|
||||
* 推送前经 A102-4 预检应付单已生成、下推时经 A102-4 反查付款计划分录 awrp_entry_code_p。
|
||||
* 两侧共用的防重键组装、A102-4 查询/反查、A102-2 保存、结算明细分录解析与档案取数统一收口在本组件;
|
||||
* 财务应收单(A302-2)同步组件 {@link KingdeeArBillSyncSupport} 亦复用本组件的结算明细分录解析与档案取数方法(public 成员)。</p>
|
||||
* <p>明细分录口径:取正式结算单页面结算明细(每行一条)+ 结算合计费用项(每行一条);
|
||||
* 物料编码按项目业务类型/业务模式/装卸大类映射(见 {@link #resolveMaterialNumber});
|
||||
* 计量单位编码按费用类型匹配合同计费方案规则计费单位,再匹配计量单位档案编码;
|
||||
* 数量运费费用项目取运输量、其余默认 1;单价按结算金额/数量反算。</p>
|
||||
*
|
||||
* @author Chill
|
||||
*/
|
||||
@@ -93,13 +97,18 @@ public class KingdeeApBillSyncSupport {
|
||||
private static final int PAY_RESULT_QUERY_BATCH = 50;
|
||||
/** A102-4 分页查询页大小 */
|
||||
private static final int PAY_RESULT_PAGE_SIZE = 200;
|
||||
/** 结算明细行(运费)计费方案匹配费用项 */
|
||||
private static final String FREIGHT_FEE_ITEM = "运输费";
|
||||
/** 结算明细行(运费)计费方案匹配费用类型 */
|
||||
private static final String FREIGHT_FEE_TYPE = "物流配送";
|
||||
|
||||
private final IBfiService bfiService;
|
||||
private final ContractManageMapper contractManageMapper;
|
||||
private final CustomerArchiveMapper customerArchiveMapper;
|
||||
private final FormalSettlementDetailFeeMapper formalSettlementDetailFeeMapper;
|
||||
private final PreSettlementDetailFeeMapper preSettlementDetailFeeMapper;
|
||||
private final ReceivablePayableCargoFeeMapper receivablePayableCargoFeeMapper;
|
||||
private final FormalSettlementSummaryFeeMapper formalSettlementSummaryFeeMapper;
|
||||
private final FormalSettlementMapper formalSettlementMapper;
|
||||
private final ProjectApplyMapper projectApplyMapper;
|
||||
private final ISysClient sysClient;
|
||||
private final BfiProperties bfiProperties;
|
||||
|
||||
@@ -168,17 +177,18 @@ public class KingdeeApBillSyncSupport {
|
||||
* 推送金蝶财务应付单(A102-2 财务应付单保存,由正式结算单「同步金蝶」触发)。
|
||||
* 推送前先经 A102-4 按防重键(awrp_src_sys + awrp_src_code)查询:金蝶已存在财务应付单时
|
||||
* 幂等跳过保存并返回已有单据编号(说明上次同步已保存成功、在后续环节中断,应续推而非拦截报错)。
|
||||
* 明细分录逐费用行生成(不聚合),含税金额取应付货物费用行调整后总金额、不含税取结算货物费用行不含税;
|
||||
* 付款计划 planentity 暂不传(启用方案见方法内注释),当前由金蝶按明细自动汇总生成
|
||||
* 明细分录取页面结算明细(每行一条)+ 结算合计费用项(每行一条),结算金额逐行直送;
|
||||
* 汇率日期/汇率取结算单页面维护值;付款计划 planentity 暂不传(启用方案见方法内注释),
|
||||
* 当前由金蝶按明细自动汇总生成
|
||||
*
|
||||
* @param apBillSrcCode 财务应付单防重键(正式结算单号_结算明细单据号拼接)
|
||||
* @param contractId 正式结算单关联合同id
|
||||
* @param entryGroups 结算明细解析出的应付单分录
|
||||
* @param settlement 正式结算单(合同/汇率日期/汇率取数来源)
|
||||
* @param entryGroups 结算明细+结算合计费用项解析出的应付单分录
|
||||
* @param subject 业务单据号(正式结算单号,日志与异常提示用)
|
||||
* @param context 金蝶同步上下文(缓存载体)
|
||||
* @return 财务应付单推送结果(单据编号 + 是否金蝶已存在幂等跳过)
|
||||
*/
|
||||
public BfiApBillPushOutcome pushApBill(String apBillSrcCode, Long contractId, List<BfiEntryGroup> entryGroups,
|
||||
public BfiApBillPushOutcome pushApBill(String apBillSrcCode, FormalSettlement settlement, List<BfiEntryGroup> entryGroups,
|
||||
String subject, KingdeeSyncContext context) {
|
||||
// A102-4 幂等预检:按防重键查询金蝶是否已生成财务应付单(批量同步时整批已预热缓存,此处仅懒加载未命中编码),
|
||||
// 已存在则幂等跳过保存,直接返回已有单据编号
|
||||
@@ -188,7 +198,7 @@ public class KingdeeApBillSyncSupport {
|
||||
log.info("正式结算单[{}]金蝶财务应付单已生成(单据编号[{}]),幂等跳过 A102-2 保存", subject, existBillNo);
|
||||
return new BfiApBillPushOutcome(existBillNo, true);
|
||||
}
|
||||
ContractManage contract = requireContract(contractId, context);
|
||||
ContractManage contract = requireContract(settlement.getContractId(), context);
|
||||
// 单据类型按映射字典转换:默认服务采购应付,金蝶确认其他类型后修改 BFI_AP_BILL_BILLTYPE_NAME 即可
|
||||
String billtypeNumber = KingdeeBfiConstant.billtypeBy(KingdeeBfiConstant.AP_BILL_BILLTYPE_MAP, KingdeeBfiConstant.BFI_AP_BILL_BILLTYPE_NAME);
|
||||
if (Func.isBlank(billtypeNumber)) {
|
||||
@@ -229,10 +239,16 @@ public class KingdeeApBillSyncSupport {
|
||||
param.setIsintax(false);
|
||||
// 汇率表.编码(非必传)
|
||||
// param.setExratetableNumber(bfiProperties.getPayApply().getExratetable());
|
||||
// 汇率日期(非必传)
|
||||
// param.setExratedate(billDateText);
|
||||
// 汇率日期:正式结算单页面维护的汇率日期(非必传,未维护不送)
|
||||
if (settlement.getExchangeRateDate() != null) {
|
||||
param.setExratedate(bfiDateTimeText(settlement.getExchangeRateDate()));
|
||||
}
|
||||
// 汇率:正式结算单页面维护的结算汇率(非必传,未维护不送)
|
||||
if (settlement.getExchangeRate() != null) {
|
||||
param.setExchangerate(settlement.getExchangeRate());
|
||||
}
|
||||
// 往来单位名称(非必传)
|
||||
// param.setAsstactname(entity.getPayeeName());
|
||||
// param.setAsstactname(settlement.getPayeeName());
|
||||
// 备注(非必传)
|
||||
// param.setRemark(subject);
|
||||
// 【必填】外部系统编号(候选键)
|
||||
@@ -248,38 +264,94 @@ public class KingdeeApBillSyncSupport {
|
||||
// 【必填】外部单据类型
|
||||
param.setAwrpSrcType(KingdeeBfiConstant.BFI_AP_BILL_SRC_TYPE);
|
||||
|
||||
// 明细分录:每条应付货物费用行一条(不聚合),无吨位时数量兜底 1
|
||||
// 明细分录:结算明细行 + 结算合计费用项各一条(不聚合)
|
||||
List<BfiApBillSaveEntryDTO> detailentry = new ArrayList<>();
|
||||
for (BfiEntryGroup entryGroup : entryGroups) {
|
||||
BfiApBillSaveEntryDTO entry = new BfiApBillSaveEntryDTO();
|
||||
// 【必填】物料编码.编码:结算明细货物类型匹配货物类型档案货物编码
|
||||
entry.setMaterialNumber(entryGroup.materialNumber());
|
||||
// 【必填】计量单位.编码:货物费用行运费计算单位(快照 price_unit,档案暂无编码字段暂传单位名称)
|
||||
// 【必填】物料编码.编码:按项目业务类型/业务模式/装卸大类映射(国内道路货物运输服务-运输方式 / 国际货物运输代理服务 / 装卸)
|
||||
String materialNumber = entryGroup.materialNumber();
|
||||
|
||||
|
||||
|
||||
entry.setMaterialNumber(materialNumber);
|
||||
// 运输货物(金蝶接口新字段):结算明细货物名称,结算合计费用项行不传
|
||||
entry.setAwrpTransport(entryGroup.transportGoods());
|
||||
// 车号(金蝶接口新字段):结算明细车号,结算合计费用项行不传
|
||||
entry.setAwrpLpn(entryGroup.vehicleNo());
|
||||
// 【必填】计量单位.编码:费用类型匹配合同计费方案计费单位,再匹配计量单位档案编码
|
||||
entry.setMeasureunitNumber(entryGroup.measureunitNumber());
|
||||
// 【必填】数量:该行运输数量,无吨位数据时兜底 1
|
||||
// 【必填】数量:运费费用项目取运输量,其它费用项目默认 1
|
||||
entry.setQuantity(entryGroup.quantity().compareTo(BigDecimal.ZERO) > 0 ? entryGroup.quantity() : BigDecimal.ONE);
|
||||
// 明细.单价:该行运输单价(费用明细 unit_price;单据头 ispricetotal=true 录入总价,单价仅作参考)
|
||||
// 明细.单价:结算金额/数量反算(单据头 ispricetotal=true 录入总价,单价仅作参考)
|
||||
entry.setPrice(entryGroup.price());
|
||||
// 应付金额(含税):该行应付货物费用行调整后总金额(含税)
|
||||
// 应付金额(含税):结算明细行取结算金额(含税),结算合计费用项行取合计行结算金额
|
||||
entry.setEPricetaxtotal(entryGroup.amount());
|
||||
// 金额(不含税):该行结算金额(不含税)(应付费用行无不含税字段,经结算货物费用行回链)
|
||||
// 金额(不含税):结算明细行结算金额(不含税),结算合计费用项行无不含税金额不传
|
||||
entry.setEAmount(entryGroup.amountNoTax());
|
||||
// 【必填】项目.项目编码(联调期临时传金蝶测试档案值)
|
||||
entry.setProjectNumber(KingdeeBfiConstant.BFI_AP_BILL_PROJECT_NUMBER_TEST);
|
||||
// 【必填】外部系统来源分录标识(候选键):费用明细行id(唯一),A103-1 下推时 awrp_entry_code 与此对应
|
||||
// 【必填】外部系统来源分录标识(候选键):结算明细行取单据号,结算合计费用项行取结算单号-费用类型-费用项拼接
|
||||
entry.setAwrpEntryCode(entryGroup.entryCode());
|
||||
|
||||
// 调试
|
||||
|
||||
if (true) {
|
||||
// 【必填】物料编码.编码:结算明细货物类型匹配货物类型档案货物编码
|
||||
entry.setMaterialNumber(KingdeeBfiConstant.BFI_AP_BILL_MATERIAL_NUMBER_TEST);
|
||||
// 【必填】计量单位.编码:货物费用行运费计算单位(快照 price_unit,档案暂无编码字段暂传单位名称)
|
||||
entry.setMeasureunitNumber(KingdeeBfiConstant.BFI_AP_BILL_MEASUREUNIT_NUMBER_TEST);
|
||||
}
|
||||
|
||||
// 分录备注(非必传)
|
||||
// entry.setERemark(subject);
|
||||
|
||||
// 调试:物料编码和计量单位编码映射(4个固定组合)
|
||||
if (true) {
|
||||
// 物料+单位组合映射,只保留【已审核】物料
|
||||
Map<String, String[]> comboMap = Map.of(
|
||||
"国际货运代理服务费", new String[]{"Item-00001081", "ci"},
|
||||
"物流配送", new String[]{"Item-00001080", "TON"},
|
||||
"国内道路货物运输服务-公路运输-吨", new String[]{"XDWL11010001", "TON"},
|
||||
"国内道路货物运输服务-公路运输-公斤", new String[]{"XDWL11010002", "KG"}
|
||||
);
|
||||
// ⚠️兜底改成【已审核】物流配送 Item-00001080 + TON
|
||||
String[] defaultCombo = comboMap.get("物流配送");
|
||||
|
||||
// 构建组合键:物料名称-计量单位
|
||||
String measureunitName = entryGroup.measureunitNumber();
|
||||
String comboKey = materialNumber;
|
||||
if (materialNumber.contains("国内道路货物运输服务")) {
|
||||
comboKey = materialNumber + "-" + measureunitName;
|
||||
}
|
||||
|
||||
// 匹配组合,不匹配使用兜底组合
|
||||
String[] combo = comboMap.getOrDefault(comboKey, defaultCombo);
|
||||
materialNumber = combo[0];
|
||||
entry.setMaterialNumber(materialNumber);
|
||||
entry.setMeasureunitNumber(combo[1]);
|
||||
}
|
||||
|
||||
|
||||
/*if (true) {
|
||||
// 物料+单位组合映射
|
||||
Map<String, String[]> comboMap = Map.of(
|
||||
"国际货运代理服务费", new String[]{"Item-00001081", "ci"},
|
||||
"物流配送", new String[]{"Item-00001080", "TON"},
|
||||
"国内道路货物运输服务-公路运输-吨", new String[]{"XDWL11010001", "TON"},
|
||||
"国内道路货物运输服务-公路运输-公斤", new String[]{"XDWL11010002", "KG"}
|
||||
);
|
||||
|
||||
// 构建组合键:物料名称-计量单位
|
||||
String measureunitName = entryGroup.measureunitNumber();
|
||||
String comboKey = materialNumber;
|
||||
if (materialNumber.contains("国内道路货物运输服务")) {
|
||||
comboKey = materialNumber + "-" + measureunitName;
|
||||
}
|
||||
|
||||
// 匹配组合或随机取一个
|
||||
if (comboMap.containsKey(comboKey)) {
|
||||
String[] combo = comboMap.get(comboKey);
|
||||
materialNumber = combo[0];
|
||||
entry.setMeasureunitNumber(combo[1]);
|
||||
} else {
|
||||
List<String[]> combos = new ArrayList<>(comboMap.values());
|
||||
String[] randomCombo = combos.get(new java.util.Random().nextInt(combos.size()));
|
||||
materialNumber = randomCombo[0];
|
||||
entry.setMeasureunitNumber(randomCombo[1]);
|
||||
}
|
||||
}*/
|
||||
|
||||
|
||||
detailentry.add(entry);
|
||||
}
|
||||
param.setDetailentry(detailentry);
|
||||
@@ -289,211 +361,338 @@ public class KingdeeApBillSyncSupport {
|
||||
// 当前不传由金蝶按明细自动汇总生成,A103-1 推送前经 A102-4 反查金蝶生成的 awrp_entry_code_p
|
||||
// TODO 待业务确认:存在预付款抵扣时是否需传预付信息 preentry(y_billtype/y_billid/y_settleamt),本期不传、后续迭代
|
||||
|
||||
// 推送入参摘要(联调排查:防重键、分录条数、页面维护的汇率口径)
|
||||
log.info("推送金蝶财务应付单:正式结算单[{}],防重键[{}],明细分录{}条,汇率日期[{}],汇率[{}]",
|
||||
subject, apBillSrcCode, detailentry.size(), param.getExratedate(), param.getExchangerate());
|
||||
|
||||
BfiBillSaveResultVO saveResult = bfiService.saveApBill(param);
|
||||
// 应付单id不落库,后续如需可经 A102-4 按防重键(awrp_src_sys + awrp_src_code)查询
|
||||
requireBfiSaveSuccess(saveResult, "推送金蝶财务应付单");
|
||||
return new BfiApBillPushOutcome(saveResult.getResult().get(0).getNumber(), false);
|
||||
String billNo = saveResult.getResult().get(0).getNumber();
|
||||
log.info("金蝶财务应付单保存成功:正式结算单[{}],单据编号[{}],明细分录{}条", subject, billNo, detailentry.size());
|
||||
return new BfiApBillPushOutcome(billNo, false);
|
||||
}
|
||||
|
||||
// ==================== 结算明细分录解析 ====================
|
||||
// ==================== 结算明细分录解析(结算明细行 + 结算合计费用项) ====================
|
||||
|
||||
/**
|
||||
* 解析正式结算明细,逐行生成应付单分录(不聚合,每条来源费用行一条分录),分录携带所属正式结算单id
|
||||
* 解析正式结算明细为应付单分录(不聚合):按所属正式结算单分组,逐结算单取页面结算明细
|
||||
* (每行一条分录)与结算合计费用项(每行一条分录);分录携带所属正式结算单id
|
||||
* (多结算单付款申请 A103-1 下推时按结算单分组勾稽各自应付单付款计划分录)。
|
||||
* <p>取数来源:应付明细货物费用行(blade_receivable_payable_cargo_fee,经结算明细 sourceDetailId 关联应付明细,
|
||||
* 每货物一条)——货物类型/计量单位/运输数量/含税金额直接取应付费用行(含税金额取数链:调整后总金额 after_amount →
|
||||
* 原总金额 → 单价×数量兜底,见 {@link #resolveFeeAmount});
|
||||
* 应付费用行无不含税字段,不含税金额经结算货物费用行 sourceFeeId 回链(直接选明细路径 sourceFeeId=应付费用行id,
|
||||
* 预结算合并路径 sourceFeeId=预结算费用行id,经预结算费用行 sourceFeeId 回溯应付费用行id);
|
||||
* 结算明细对应应付明细无货物费用行时直接报错拦截(取数必须来自应付费用明细,无兜底)。物料编码:货物类型(支持“、”分隔多类型)
|
||||
* 匹配货物类型档案货物编码;分录唯一标识(awrp_entry_code)取费用明细行id(雪花id天然唯一,满足 A103-1
|
||||
* 对方系统唯一行id不能重复;A103-1 与应付单的勾稽关联走 awrp_finapbill_entryid=付款计划
|
||||
* awrp_entry_code_p,与本标识无关);分录金额不按申请付款金额分摊。</p>
|
||||
* 应收单(A302-2)、收票单(A303-2)分录与本解析同源同口径
|
||||
*
|
||||
* @param settlementDetails 正式结算明细列表
|
||||
* @param settlementDetails 正式结算明细列表(可能跨多张结算单)
|
||||
* @param context 金蝶同步上下文(缓存载体)
|
||||
* @return 应付单分录列表(保持行序)
|
||||
*/
|
||||
public List<BfiEntryGroup> resolveEntryGroups(List<FormalSettlementDetail> settlementDetails, KingdeeSyncContext context) {
|
||||
if (Func.isEmpty(settlementDetails)) {
|
||||
throw new ServiceException("推送金蝶财务应付单失败:正式结算单无结算明细,无法解析物料与计量单位");
|
||||
throw new ServiceException("推送金蝶失败:正式结算单无结算明细,无法解析分录");
|
||||
}
|
||||
Map<String, String> cargoCodeByName = cargoCodeByName(context);
|
||||
// 应付明细id->应付货物费用行(分录主取数来源)
|
||||
Map<Long, List<ReceivablePayableCargoFee>> payableFeesByDetailId = payableCargoFeesByDetailId(settlementDetails);
|
||||
// 应付费用行id->结算金额(不含税):应付费用行无不含税字段,经结算货物费用行 sourceFeeId 回链
|
||||
Map<Long, BigDecimal> noTaxByPayableFeeId = settlementNoTaxByPayableFeeId(settlementDetails);
|
||||
List<BfiEntryGroup> entryGroups = new ArrayList<>();
|
||||
Map<Long, List<FormalSettlementDetail>> detailsBySettlementId = new LinkedHashMap<>();
|
||||
for (FormalSettlementDetail settlementDetail : settlementDetails) {
|
||||
List<ReceivablePayableCargoFee> payableFees =
|
||||
payableFeesByDetailId.getOrDefault(settlementDetail.getSourceDetailId(), List.of());
|
||||
if (Func.isEmpty(payableFees)) {
|
||||
throw new ServiceException("推送金蝶财务应付单失败:结算明细[" + Func.toStr(settlementDetail.getDocumentNo())
|
||||
+ "]对应应付明细无货物费用行,分录取数必须来自应付费用明细,请先维护费用数据");
|
||||
}
|
||||
for (ReceivablePayableCargoFee payableFee : payableFees) {
|
||||
String sourceDesc = "应付货物费用[" + Func.toStr(payableFee.getCargoName()) + " 第" + Func.toStr(payableFee.getLineNo()) + "行]";
|
||||
appendEntryGroups(entryGroups, cargoCodeByName, settlementDetail.getFormalSettlementId(),
|
||||
payableFee.getCargoType(),
|
||||
resolveMeasureunitNumber(payableFee.getPriceUnit(), payableFee.getQuantityUnit()),
|
||||
payableFee.getId(), money(payableFee.getUnitPrice()),
|
||||
resolveFeeAmount(payableFee, sourceDesc),
|
||||
noTaxByPayableFeeId.getOrDefault(payableFee.getId(), BigDecimal.ZERO),
|
||||
money(payableFee.getTransportQuantity()),
|
||||
sourceDesc);
|
||||
if (settlementDetail.getFormalSettlementId() == null) {
|
||||
throw new ServiceException("推送金蝶失败:结算明细[" + Func.toStr(settlementDetail.getDocumentNo()) + "]未关联正式结算单");
|
||||
}
|
||||
detailsBySettlementId.computeIfAbsent(settlementDetail.getFormalSettlementId(), key -> new ArrayList<>()).add(settlementDetail);
|
||||
}
|
||||
List<BfiEntryGroup> entryGroups = new ArrayList<>();
|
||||
detailsBySettlementId.forEach((settlementId, details) ->
|
||||
entryGroups.addAll(this.resolveEntryGroups(requireSettlement(settlementId, context), details, context)));
|
||||
return entryGroups;
|
||||
}
|
||||
|
||||
/**
|
||||
* 单条应付货物费用行生成一条分录;
|
||||
* 货物类型含多种(“、/,”分隔)时不拆行,物料编码取第一种货物类型,金额数量整行直送
|
||||
* 解析单张正式结算单分录:页面结算明细每行一条 + 结算合计费用项每行一条。
|
||||
* <p>取数口径:
|
||||
* 物料编码按项目业务类型/业务模式/装卸大类映射——装卸大类费用行映射“装卸”;国际运输业务模式
|
||||
* (或国际货物运输代理服务项目业务类型)映射“国际货物运输代理服务”;国内映射“国内道路货物运输服务-”+运单运输方式;
|
||||
* 计量单位编码按费用类型/费用项匹配合同计费方案规则计费单位,再按计量单位档案取编码;
|
||||
* 数量运费费用项目取运输量(未维护兜底 1)、结算合计费用项默认 1;单价按结算金额/数量反算;
|
||||
* 结算金额:结算明细行取结算金额(含税)、结算合计费用项行取合计行结算金额;
|
||||
* 运输货物/车号仅结算明细行传(货物名称/车号);分录标识:结算明细行取单据号、
|
||||
* 结算合计费用项行取结算单号-费用类型-费用项拼接。</p>
|
||||
*
|
||||
* @param entryGroups 分录集合(保持行序)
|
||||
* @param cargoCodeByName 货物类型名称->货物编码映射
|
||||
* @param formalSettlementId 分录所属正式结算单id
|
||||
* @param cargoType 货物类型(支持“、”分隔多类型)
|
||||
* @param measureunitNumber 计量单位编码(运费计算单位,档案暂无编码字段暂传单位名称)
|
||||
* @param sourceRowId 应付货物费用行id,雪花id天然唯一
|
||||
* @param price 该行运输单价(费用明细 unit_price)
|
||||
* @param amount 该行金额(含税)
|
||||
* @param amountNoTax 该行金额(不含税)
|
||||
* @param quantity 该行运输数量
|
||||
* @param sourceDesc 来源行描述,用于异常提示
|
||||
* @param settlement 正式结算单
|
||||
* @param settlementDetails 该结算单下的结算明细
|
||||
* @param context 金蝶同步上下文(缓存载体)
|
||||
* @return 应付单分录列表(保持行序)
|
||||
*/
|
||||
private void appendEntryGroups(List<BfiEntryGroup> entryGroups, Map<String, String> cargoCodeByName,
|
||||
Long formalSettlementId, String cargoType, String measureunitNumber, Long sourceRowId,
|
||||
BigDecimal price, BigDecimal amount, BigDecimal amountNoTax, BigDecimal quantity,
|
||||
String sourceDesc) {
|
||||
if (Func.isBlank(measureunitNumber)) {
|
||||
throw new ServiceException("推送金蝶财务应付单失败:" + sourceDesc + "计量单位为空,请先维护运费计算单位");
|
||||
public List<BfiEntryGroup> resolveEntryGroups(FormalSettlement settlement, List<FormalSettlementDetail> settlementDetails,
|
||||
KingdeeSyncContext context) {
|
||||
if (Func.isEmpty(settlementDetails)) {
|
||||
throw new ServiceException("推送金蝶失败:正式结算单[" + Func.toStr(settlement.getFormalSettlementNo()) + "]无结算明细,无法解析分录");
|
||||
}
|
||||
// 货物类型含多种时不拆行(业务约定整行一条分录),物料编码取第一种货物类型
|
||||
String cargoName = splitCargoTypes(cargoType).get(0);
|
||||
String materialNumber = cargoCodeByName.get(cargoName);
|
||||
if (Func.isBlank(materialNumber)) {
|
||||
throw new ServiceException("推送金蝶财务应付单失败:" + sourceDesc + "货物类型[" + cargoName + "]未在货物类型档案中维护编码,请补充后重试");
|
||||
ProjectApply project = requireProject(settlement.getProjectId(), context);
|
||||
ContractManage contract = requireContract(settlement.getContractId(), context);
|
||||
// 合同计费方案规则:费用类型/费用项 -> 计费单位
|
||||
Map<String, String> billingUnitByRule = billingUnitByRule(contract);
|
||||
// 计量单位档案:单位名称 -> 计量单位编码
|
||||
Map<String, String> unitCodeByName = measureUnitCodeByName(context);
|
||||
// 结算明细费用行:判断运单是否存在装卸大类费用
|
||||
Map<Long, List<FormalSettlementDetailFee>> feesByDetailId = detailFeesByDetailId(settlementDetails);
|
||||
List<BfiEntryGroup> entryGroups = new ArrayList<>();
|
||||
for (FormalSettlementDetail settlementDetail : settlementDetails) {
|
||||
appendDetailEntry(entryGroups, settlement, project, settlementDetail,
|
||||
feesByDetailId.getOrDefault(settlementDetail.getId(), List.of()), billingUnitByRule, unitCodeByName);
|
||||
}
|
||||
// 分录唯一标识(awrp_entry_code,A103-1 要求对方系统唯一行id不能重复):应付货物费用行id,雪花id天然唯一
|
||||
String entryCode = String.valueOf(sourceRowId);
|
||||
entryGroups.add(new BfiEntryGroup(materialNumber, measureunitNumber, entryCode,
|
||||
quantity, price, amount, amountNoTax, formalSettlementId));
|
||||
log.info("金蝶应付单分录生成:来源[{}],分录标识[{}],物料编码[{}],计量单位[{}],数量[{}],单价[{}],含税金额[{}],不含税金额[{}]",
|
||||
sourceDesc, entryCode, materialNumber, measureunitNumber, quantity, price, amount, amountNoTax);
|
||||
}
|
||||
|
||||
/** 付款申请关联的正式结算货物费用行(blade_formal_settlement_detail_fee,用于不含税金额回链应付费用行;应收单同步组件复用) */
|
||||
public List<FormalSettlementDetailFee> formalSettlementDetailFees(List<FormalSettlementDetail> settlementDetails) {
|
||||
List<Long> settlementDetailIds = settlementDetails.stream().map(FormalSettlementDetail::getId)
|
||||
.filter(Objects::nonNull).distinct().toList();
|
||||
if (Func.isEmpty(settlementDetailIds)) {
|
||||
return List.of();
|
||||
// 结算合计只取手工添加的费用项(manualFlag=1);系统自动汇总行是结算明细金额的再汇总,再送会与明细重复
|
||||
List<FormalSettlementSummaryFee> manualSummaryFees = listSummaryFees(settlement.getId()).stream()
|
||||
.filter(summaryFee -> Integer.valueOf(1).equals(summaryFee.getManualFlag())).toList();
|
||||
for (FormalSettlementSummaryFee summaryFee : manualSummaryFees) {
|
||||
appendSummaryEntry(entryGroups, settlement, project, summaryFee,
|
||||
settlementDetails.get(0).getTransportType(), billingUnitByRule, unitCodeByName);
|
||||
}
|
||||
return formalSettlementDetailFeeMapper.selectList(Wrappers.<FormalSettlementDetailFee>lambdaQuery()
|
||||
.in(FormalSettlementDetailFee::getFormalSettlementDetailId, settlementDetailIds)
|
||||
.eq(FormalSettlementDetailFee::getIsDeleted, 0)
|
||||
.orderByAsc(FormalSettlementDetailFee::getLineNo));
|
||||
}
|
||||
|
||||
/** 应付明细id->应付货物费用行(blade_receivable_payable_cargo_fee,经结算明细 sourceDetailId 关联,每货物一条,分录主取数来源;应收单同步组件复用) */
|
||||
public Map<Long, List<ReceivablePayableCargoFee>> payableCargoFeesByDetailId(List<FormalSettlementDetail> settlementDetails) {
|
||||
List<Long> sourceDetailIds = settlementDetails.stream()
|
||||
.map(FormalSettlementDetail::getSourceDetailId).filter(Objects::nonNull).distinct().toList();
|
||||
if (Func.isEmpty(sourceDetailIds)) {
|
||||
return Map.of();
|
||||
if (entryGroups.isEmpty()) {
|
||||
throw new ServiceException("推送金蝶失败:正式结算单[" + Func.toStr(settlement.getFormalSettlementNo())
|
||||
+ "]无结算明细与结算合计费用项,无法解析分录");
|
||||
}
|
||||
return receivablePayableCargoFeeMapper.selectList(Wrappers.<ReceivablePayableCargoFee>lambdaQuery()
|
||||
.in(ReceivablePayableCargoFee::getDetailId, sourceDetailIds)
|
||||
.eq(ReceivablePayableCargoFee::getIsDeleted, 0)
|
||||
.orderByAsc(ReceivablePayableCargoFee::getLineNo))
|
||||
.stream().collect(Collectors.groupingBy(ReceivablePayableCargoFee::getDetailId));
|
||||
// 分录解析摘要(联调排查:明细条数=结算明细行+手工添加的结算合计费用项,物料/计量单位/分录标识逐条见下方分录生成日志)
|
||||
log.info("金蝶应付单分录解析完成:正式结算单[{}],结算明细{}条 + 手工结算合计{}条 = 共{}条分录",
|
||||
settlement.getFormalSettlementNo(), settlementDetails.size(),
|
||||
entryGroups.size() - settlementDetails.size(), entryGroups.size());
|
||||
return entryGroups;
|
||||
}
|
||||
|
||||
/**
|
||||
* 应付货物费用行id->结算金额(不含税):应付费用行无不含税字段,经结算货物费用行 sourceFeeId 回链获取。
|
||||
* 直接选应收应付明细路径 sourceFeeId=应付费用行id;预结算合并路径 sourceFeeId=预结算费用行id,
|
||||
* 经预结算费用行 sourceFeeId(指向应付费用行)回溯;无对应结算费用行时无映射,调用方兜底 0(应收单同步组件复用)
|
||||
* 单条结算明细行生成一条分录:物料按项目业务类型/业务模式/装卸大类映射,数量取运输量(兜底 1),
|
||||
* 金额取结算金额(含税),不含税取结算金额(不含税),运输货物/车号取货物名称/车号,分录标识取单据号
|
||||
*/
|
||||
public Map<Long, BigDecimal> settlementNoTaxByPayableFeeId(List<FormalSettlementDetail> settlementDetails) {
|
||||
List<FormalSettlementDetailFee> settlementFees = formalSettlementDetailFees(settlementDetails);
|
||||
if (Func.isEmpty(settlementFees)) {
|
||||
return Map.of();
|
||||
}
|
||||
// 上游费用行id(应付费用行id或预结算费用行id)-> 不含税合计
|
||||
Map<Long, BigDecimal> noTaxBySourceFeeId = new LinkedHashMap<>();
|
||||
for (FormalSettlementDetailFee settlementFee : settlementFees) {
|
||||
if (settlementFee.getSourceFeeId() == null) {
|
||||
continue;
|
||||
}
|
||||
noTaxBySourceFeeId.merge(settlementFee.getSourceFeeId(), money(settlementFee.getSettlementAmountNoTax()), BigDecimal::add);
|
||||
}
|
||||
if (noTaxBySourceFeeId.isEmpty()) {
|
||||
return Map.of();
|
||||
}
|
||||
// sourceFeeId 指向预结算费用行的:经预结算费用行回溯应付费用行id(直接路径无预结算费用行,保持原 id)
|
||||
Map<Long, Long> payableFeeIdByPreFeeId = preSettlementDetailFeeMapper.selectList(
|
||||
Wrappers.<PreSettlementDetailFee>lambdaQuery().in(PreSettlementDetailFee::getId, new ArrayList<>(noTaxBySourceFeeId.keySet())))
|
||||
.stream().filter(preSettlementFee -> preSettlementFee.getSourceFeeId() != null)
|
||||
.collect(Collectors.toMap(PreSettlementDetailFee::getId, PreSettlementDetailFee::getSourceFeeId, (left, right) -> left));
|
||||
Map<Long, BigDecimal> noTaxByPayableFeeId = new LinkedHashMap<>();
|
||||
noTaxBySourceFeeId.forEach((sourceFeeId, noTax) ->
|
||||
noTaxByPayableFeeId.merge(payableFeeIdByPreFeeId.getOrDefault(sourceFeeId, sourceFeeId), noTax, BigDecimal::add));
|
||||
return noTaxByPayableFeeId;
|
||||
private void appendDetailEntry(List<BfiEntryGroup> entryGroups, FormalSettlement settlement, ProjectApply project,
|
||||
FormalSettlementDetail detail, List<FormalSettlementDetailFee> detailFees,
|
||||
Map<String, String> billingUnitByRule, Map<String, String> unitCodeByName) {
|
||||
String sourceDesc = "结算明细[" + Func.toStr(detail.getDocumentNo()) + "]";
|
||||
// 该条运单存在装卸大类费用(明细费用项或明细费用行费用项含“装卸”)时物料映射为装卸
|
||||
boolean loadingFee = hasLoadingFeeItem(detail.getFeeItemsJson())
|
||||
|| detailFees.stream().anyMatch(fee -> hasLoadingFeeItem(fee.getFeeItemsJson()));
|
||||
String materialNumber = resolveMaterialNumber(loadingFee, project, detail.getTransportType(), sourceDesc);
|
||||
// 计量单位编码:运费费用项匹配合同计费方案计费单位,再匹配计量单位档案编码
|
||||
String measureunitNumber = resolveMeasureunitNumber(billingUnitByRule, unitCodeByName, FREIGHT_FEE_ITEM, FREIGHT_FEE_TYPE, sourceDesc);
|
||||
// 数量:运费费用项目取运输量,未维护兜底 1
|
||||
BigDecimal quantity = detail.getTransportQuantity() != null && detail.getTransportQuantity().compareTo(BigDecimal.ZERO) > 0
|
||||
? money(detail.getTransportQuantity()) : BigDecimal.ONE;
|
||||
BigDecimal amount = money(detail.getSettlementAmountTax());
|
||||
BigDecimal price = entryPrice(amount, quantity);
|
||||
// 分录唯一标识(awrp_entry_code):结算明细单据号;未维护时兜底明细行id,避免候选键为空被金蝶拦截
|
||||
String entryCode = Func.isBlank(detail.getDocumentNo()) ? String.valueOf(detail.getId()) : detail.getDocumentNo().trim();
|
||||
entryGroups.add(new BfiEntryGroup(materialNumber, measureunitNumber, entryCode, quantity, price, amount,
|
||||
money(detail.getSettlementAmountNoTax()), detail.getCargoName(), detail.getVehicleNo(), settlement.getId()));
|
||||
log.info("金蝶应付单分录生成:来源[{}],分录标识[{}],物料编码[{}],计量单位[{}],数量[{}],单价[{}],含税金额[{}],不含税金额[{}]",
|
||||
sourceDesc, entryCode, materialNumber, measureunitNumber, quantity, price, amount, money(detail.getSettlementAmountNoTax()));
|
||||
}
|
||||
|
||||
/** 货物类型名称->货物编码映射(取自货物类型档案;名称重复且编码不一致时拦截;批量同步时全批共享一次加载结果;应收单同步组件复用) */
|
||||
public Map<String, String> cargoCodeByName(KingdeeSyncContext context) {
|
||||
Map<String, String> cache = context.cargoCodeByName();
|
||||
/**
|
||||
* 单条结算合计费用项生成一条分录:物料按装卸大类/项目业务类型映射(运输方式取结算明细首行),
|
||||
* 数量默认 1,金额取合计行结算金额,运输货物/车号不传,分录标识取结算单号-费用类型-费用项拼接
|
||||
*/
|
||||
private void appendSummaryEntry(List<BfiEntryGroup> entryGroups, FormalSettlement settlement, ProjectApply project,
|
||||
FormalSettlementSummaryFee summaryFee, String fallbackTransportType,
|
||||
Map<String, String> billingUnitByRule, Map<String, String> unitCodeByName) {
|
||||
String feeType = Func.toStr(summaryFee.getFeeType()).trim();
|
||||
String feeItem = Func.toStr(summaryFee.getFeeItem()).trim();
|
||||
String sourceDesc = "结算合计[" + feeType + "-" + feeItem + "]";
|
||||
boolean loadingFee = feeType.contains("装卸") || feeItem.contains("装卸");
|
||||
String materialNumber = resolveMaterialNumber(loadingFee, project, fallbackTransportType, sourceDesc);
|
||||
String measureunitNumber = resolveMeasureunitNumber(billingUnitByRule, unitCodeByName, feeItem, feeType, sourceDesc);
|
||||
// 结算合计费用项行无运输量,数量默认 1
|
||||
BigDecimal quantity = BigDecimal.ONE;
|
||||
BigDecimal amount = money(summaryFee.getSettlementAmount());
|
||||
BigDecimal price = entryPrice(amount, quantity);
|
||||
// 分录唯一标识(awrp_entry_code):结算单号-费用类型-费用项(费用项为空时仅拼结算单号-费用类型)
|
||||
String entryCode = settlement.getFormalSettlementNo() + "-"
|
||||
+ (Func.isBlank(feeItem) ? feeType : feeType + "-" + feeItem);
|
||||
entryGroups.add(new BfiEntryGroup(materialNumber, measureunitNumber, entryCode, quantity, price, amount,
|
||||
null, null, null, settlement.getId()));
|
||||
log.info("金蝶应付单分录生成:来源[{}],分录标识[{}],物料编码[{}],计量单位[{}],数量[{}],单价[{}],含税金额[{}]",
|
||||
sourceDesc, entryCode, materialNumber, measureunitNumber, quantity, price, amount);
|
||||
}
|
||||
|
||||
/**
|
||||
* 物料编码映射:装卸大类费用映射“装卸”;业务模式/项目业务类型含“国际”(国际运输、国际货物运输代理服务)
|
||||
* 映射“国际货物运输代理服务”;其余(国内运输/国内道路货物运输服务)拼接运单运输方式,
|
||||
* 如“国内道路货物运输服务-水路”
|
||||
*
|
||||
* @param loadingFee 该行是否属于装卸大类费用
|
||||
* @param project 结算单关联项目(业务类型/业务模式取数来源)
|
||||
* @param transportType 运单运输方式(结算合计费用项行取结算明细首行运输方式)
|
||||
* @param sourceDesc 来源行描述,用于异常提示
|
||||
* @return 金蝶物料编码(按映射规则拼出的物料名称)
|
||||
*/
|
||||
private String resolveMaterialNumber(boolean loadingFee, ProjectApply project, String transportType, String sourceDesc) {
|
||||
String materialNumber;
|
||||
if (loadingFee) {
|
||||
materialNumber = KingdeeBfiConstant.BFI_MATERIAL_LOADING;
|
||||
} else if (isInternationalBusiness(project)) {
|
||||
materialNumber = KingdeeBfiConstant.BFI_MATERIAL_INTERNATIONAL;
|
||||
} else {
|
||||
if (Func.isBlank(transportType)) {
|
||||
throw new ServiceException("推送金蝶失败:" + sourceDesc + "运单运输方式为空,无法拼接国内道路货物运输服务物料,请先维护运输方式");
|
||||
}
|
||||
materialNumber = KingdeeBfiConstant.BFI_MATERIAL_DOMESTIC_PREFIX + transportType.trim();
|
||||
}
|
||||
// 物料映射判定依据(联调排查:装卸大类/业务模式/项目业务类型/运输方式 -> 物料)
|
||||
log.info("金蝶物料映射:来源[{}],装卸大类[{}],业务模式[{}],项目业务类型[{}],运单运输方式[{}] -> 物料编码[{}]",
|
||||
sourceDesc, loadingFee ? "是" : "否",
|
||||
project == null ? null : project.getBusinessMode(), project == null ? null : project.getBusinessType(),
|
||||
transportType, materialNumber);
|
||||
return materialNumber;
|
||||
}
|
||||
|
||||
/** 项目业务模式/业务类型含“国际”即视为国际运输(映射国际货代服务物料);项目缺失时拦截 */
|
||||
private boolean isInternationalBusiness(ProjectApply project) {
|
||||
if (project == null) {
|
||||
throw new ServiceException("推送金蝶失败:结算单未关联项目,无法按项目业务类型/业务模式匹配物料");
|
||||
}
|
||||
return containsInternational(project.getBusinessMode()) || containsInternational(project.getBusinessType());
|
||||
}
|
||||
|
||||
private boolean containsInternational(String text) {
|
||||
return text != null && text.contains("国际");
|
||||
}
|
||||
|
||||
/**
|
||||
* 计量单位编码解析:费用项优先、费用类型兜底匹配合同计费方案规则计费单位,
|
||||
* 再按计量单位档案(单位名称匹配)取计量单位编码,任一环节缺失直接拦截报错
|
||||
*
|
||||
* @param billingUnitByRule 合同计费方案:费用类型/费用项 -> 计费单位
|
||||
* @param unitCodeByName 计量单位档案:单位名称 -> 计量单位编码
|
||||
* @param feeItem 费用项目(结算合计费用项行取行费用项,结算明细行固定运输费)
|
||||
* @param feeType 费用类型(结算合计费用项行取行费用类型,结算明细行固定物流配送)
|
||||
* @param sourceDesc 来源行描述,用于异常提示
|
||||
* @return 计量单位编码
|
||||
*/
|
||||
private String resolveMeasureunitNumber(Map<String, String> billingUnitByRule, Map<String, String> unitCodeByName,
|
||||
String feeItem, String feeType, String sourceDesc) {
|
||||
String billingUnit = Func.isBlank(feeItem) ? null : billingUnitByRule.get(feeItem.trim());
|
||||
if (Func.isBlank(billingUnit) && Func.isNotBlank(feeType)) {
|
||||
billingUnit = billingUnitByRule.get(feeType.trim());
|
||||
}
|
||||
if (Func.isBlank(billingUnit)) {
|
||||
throw new ServiceException("推送金蝶失败:" + sourceDesc + "未在合同计费方案中配置计费单位,请先维护计费方案规则");
|
||||
}
|
||||
String unitCode = unitCodeByName.get(billingUnit.trim());
|
||||
if (Func.isBlank(unitCode)) {
|
||||
throw new ServiceException("推送金蝶失败:" + sourceDesc + "计费单位[" + billingUnit + "]未在计量单位档案中维护编码,请补充后重试");
|
||||
}
|
||||
// 计量单位匹配链路(联调排查:费用项/费用类型 -> 合同计费方案计费单位 -> 计量单位档案编码)
|
||||
log.info("金蝶计量单位匹配:来源[{}],费用项[{}],费用类型[{}] -> 计费单位[{}] -> 计量单位编码[{}]",
|
||||
sourceDesc, feeItem, feeType, billingUnit, unitCode);
|
||||
return unitCode;
|
||||
}
|
||||
|
||||
/**
|
||||
* 合同计费方案规则解析:费用类型/费用项 -> 计费单位(取首个配置了计费单位的规则,
|
||||
* 计费方案 JSON 解析失败按未配置处理,由计量单位解析处统一拦截报错)
|
||||
*
|
||||
* @param contract 结算合同
|
||||
* @return 费用类型/费用项 -> 计费单位映射
|
||||
*/
|
||||
private Map<String, String> billingUnitByRule(ContractManage contract) {
|
||||
Map<String, String> mapping = new HashMap<>();
|
||||
if (Func.isBlank(contract.getBillingPlanJson())) {
|
||||
return mapping;
|
||||
}
|
||||
try {
|
||||
Object parsed = JsonUtil.parse(contract.getBillingPlanJson(), List.class);
|
||||
if (!(parsed instanceof List<?> plans)) {
|
||||
return mapping;
|
||||
}
|
||||
for (Object planValue : plans) {
|
||||
if (!(planValue instanceof Map<?, ?> plan) || !(plan.get("rules") instanceof List<?> rules)) {
|
||||
continue;
|
||||
}
|
||||
for (Object ruleValue : rules) {
|
||||
if (!(ruleValue instanceof Map<?, ?> rule)) {
|
||||
continue;
|
||||
}
|
||||
String billingUnit = textValue(rule.get("billingUnit"));
|
||||
if (Func.isBlank(billingUnit)) {
|
||||
continue;
|
||||
}
|
||||
String feeItem = textValue(rule.get("feeItem"));
|
||||
String feeType = textValue(rule.get("feeType"));
|
||||
if (Func.isNotBlank(feeItem)) {
|
||||
mapping.putIfAbsent(feeItem, billingUnit);
|
||||
}
|
||||
if (Func.isNotBlank(feeType)) {
|
||||
mapping.putIfAbsent(feeType, billingUnit);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (RuntimeException ignored) {
|
||||
// 计费方案 JSON 兼容历史数据,解析失败按未配置处理
|
||||
}
|
||||
log.info("合同[{}]计费方案解析:{}项费用类型/费用项配置计费单位{}", contract.getContractNo(), mapping.size(), mapping);
|
||||
return mapping;
|
||||
}
|
||||
|
||||
/** 费用项 JSON 是否包含装卸大类费用(费用项名称含“装卸”) */
|
||||
private boolean hasLoadingFeeItem(String feeItemsJson) {
|
||||
if (Func.isEmpty(feeItemsJson)) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
Object parsed = JsonUtil.parse(feeItemsJson, Map.class);
|
||||
if (parsed instanceof Map<?, ?> map) {
|
||||
return map.keySet().stream().map(String::valueOf).anyMatch(key -> key.contains("装卸"));
|
||||
}
|
||||
} catch (RuntimeException ignored) {
|
||||
// 兼容历史费用JSON
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private String textValue(Object value) {
|
||||
return value == null ? null : String.valueOf(value).trim();
|
||||
}
|
||||
|
||||
/** 结算明细id -> 结算明细费用行(判断运单是否存在装卸大类费用) */
|
||||
private Map<Long, List<FormalSettlementDetailFee>> detailFeesByDetailId(List<FormalSettlementDetail> settlementDetails) {
|
||||
List<Long> detailIds = settlementDetails.stream().map(FormalSettlementDetail::getId)
|
||||
.filter(Objects::nonNull).distinct().toList();
|
||||
if (Func.isEmpty(detailIds)) {
|
||||
return Map.of();
|
||||
}
|
||||
return formalSettlementDetailFeeMapper.selectList(Wrappers.<FormalSettlementDetailFee>lambdaQuery()
|
||||
.in(FormalSettlementDetailFee::getFormalSettlementDetailId, detailIds)
|
||||
.eq(FormalSettlementDetailFee::getIsDeleted, 0)
|
||||
.orderByAsc(FormalSettlementDetailFee::getLineNo))
|
||||
.stream().collect(Collectors.groupingBy(FormalSettlementDetailFee::getFormalSettlementDetailId));
|
||||
}
|
||||
|
||||
/** 正式结算合计费用项(页面结算合计,保持行序) */
|
||||
public List<FormalSettlementSummaryFee> listSummaryFees(Long settlementId) {
|
||||
return formalSettlementSummaryFeeMapper.selectList(Wrappers.<FormalSettlementSummaryFee>lambdaQuery()
|
||||
.eq(FormalSettlementSummaryFee::getFormalSettlementId, settlementId)
|
||||
.eq(FormalSettlementSummaryFee::getIsDeleted, 0)
|
||||
.orderByAsc(FormalSettlementSummaryFee::getLineNo));
|
||||
}
|
||||
|
||||
/**
|
||||
* 计量单位档案:单位名称 -> 计量单位编码(批量同步时全批共享一次加载结果;
|
||||
* 应收单同步组件复用)
|
||||
*/
|
||||
public Map<String, String> measureUnitCodeByName(KingdeeSyncContext context) {
|
||||
Map<String, String> cache = context.measureUnitCodeByName();
|
||||
if (!cache.isEmpty()) {
|
||||
return cache;
|
||||
}
|
||||
R<List<CargoType>> response = sysClient.getCargoTypes();
|
||||
R<List<MeasurementUnit>> response = sysClient.getMeasurementUnits();
|
||||
if (response == null || !response.isSuccess() || Func.isEmpty(response.getData())) {
|
||||
throw new ServiceException("推送金蝶财务应付单失败:货物类型档案读取失败,请稍后重试");
|
||||
throw new ServiceException("推送金蝶失败:计量单位档案读取失败,请稍后重试");
|
||||
}
|
||||
Map<String, String> loaded = new HashMap<>();
|
||||
for (CargoType cargoType : response.getData()) {
|
||||
if (cargoType == null || Func.isBlank(cargoType.getCargoName()) || Func.isBlank(cargoType.getCargoCode())) {
|
||||
for (MeasurementUnit measurementUnit : response.getData()) {
|
||||
if (measurementUnit == null || Func.isBlank(measurementUnit.getUnitName()) || Func.isBlank(measurementUnit.getUnitCode())) {
|
||||
continue;
|
||||
}
|
||||
String cargoName = cargoType.getCargoName().trim();
|
||||
String cargoCode = cargoType.getCargoCode().trim();
|
||||
String existingCode = loaded.putIfAbsent(cargoName, cargoCode);
|
||||
if (existingCode != null && !existingCode.equals(cargoCode)) {
|
||||
throw new ServiceException("推送金蝶财务应付单失败:货物类型[" + cargoName + "]在货物类型档案中存在多个编码,请检查档案数据");
|
||||
}
|
||||
loaded.putIfAbsent(measurementUnit.getUnitName().trim(), measurementUnit.getUnitCode().trim());
|
||||
}
|
||||
log.info("计量单位档案加载:{}项(单位名称 -> 金蝶计量单位编码)", loaded.size());
|
||||
cache.putAll(loaded);
|
||||
return cache;
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析计量单位编码:优先取快照运费计算单位(快照表 price_unit,随结算自动带出),
|
||||
* 为空(历史快照)时兜底数量单位。计量单位档案暂无编码字段,暂以单位名称作为编码;
|
||||
* 档案补充编码字段后在此处增加档案映射。
|
||||
*
|
||||
* @param priceUnit 快照运费计算单位(可能为空)
|
||||
* @param fallbackUnit 兜底数量单位
|
||||
* @return 计量单位编码,无法解析时返回 null(应收单同步组件复用)
|
||||
*/
|
||||
public String resolveMeasureunitNumber(String priceUnit, String fallbackUnit) {
|
||||
String unitName = Func.isNotBlank(priceUnit) ? priceUnit : fallbackUnit;
|
||||
return Func.isBlank(unitName) ? null : unitName.trim();
|
||||
}
|
||||
|
||||
/**
|
||||
* 货物类型支持“、/,”分隔的多类型写法,解析为类型名称列表;
|
||||
* 分录不按类型拆行(业务约定整行一条),物料编码仅取列表第一种类型(应收单同步组件复用)
|
||||
*/
|
||||
public List<String> splitCargoTypes(String cargoType) {
|
||||
if (Func.isBlank(cargoType)) {
|
||||
throw new ServiceException("推送金蝶财务应付单失败:结算明细货物类型为空,无法解析物料编码");
|
||||
}
|
||||
return Arrays.stream(cargoType.split("[、,,]"))
|
||||
.map(String::trim).filter(Func::isNotBlank).distinct().toList();
|
||||
}
|
||||
|
||||
// ==================== 金蝶公共校验与档案取数 ====================
|
||||
|
||||
/**
|
||||
@@ -541,6 +740,27 @@ public class KingdeeApBillSyncSupport {
|
||||
return contract;
|
||||
}
|
||||
|
||||
/** 校验并获取结算明细所属正式结算单(批量同步时按结算单id缓存复用;应收单同步组件复用) */
|
||||
public FormalSettlement requireSettlement(Long settlementId, KingdeeSyncContext context) {
|
||||
FormalSettlement settlement = context.settlementCache().computeIfAbsent(settlementId, formalSettlementMapper::selectById);
|
||||
if (settlement == null || Objects.equals(settlement.getIsDeleted(), 1)) {
|
||||
throw new ServiceException("推送金蝶失败:正式结算单[" + settlementId + "]不存在");
|
||||
}
|
||||
return settlement;
|
||||
}
|
||||
|
||||
/** 校验并获取结算单关联项目(项目业务类型/业务模式决定物料映射;应收单同步组件复用) */
|
||||
public ProjectApply requireProject(Long projectId, KingdeeSyncContext context) {
|
||||
if (projectId == null) {
|
||||
throw new ServiceException("推送金蝶失败:结算单未关联项目,无法按项目业务类型/业务模式匹配物料");
|
||||
}
|
||||
ProjectApply project = context.projectCache().computeIfAbsent(projectId, projectApplyMapper::selectById);
|
||||
if (project == null) {
|
||||
throw new ServiceException("推送金蝶失败:项目[" + projectId + "]不存在,无法匹配物料");
|
||||
}
|
||||
return project;
|
||||
}
|
||||
|
||||
/**
|
||||
* 金蝶接口日期文本:yyyy-MM-dd HH:mm:ss(A102-2/A103-1/A103-3 各接口统一格式)
|
||||
*
|
||||
@@ -630,45 +850,37 @@ public class KingdeeApBillSyncSupport {
|
||||
}
|
||||
|
||||
/**
|
||||
* 费用行含税金额取数链:优先调整后总金额(after_amount),未维护(null/0)回退原总金额(original_amount),
|
||||
* 仍取不到按运输单价×运输量兜底(单据头 isincludetax=true 录入含税价,单价按含税口径相乘);
|
||||
* 全部无法取值时拦截报错,避免分录价税合计送 0 被金蝶校验拦截("整单'价税合计'不能为空"),
|
||||
* 应付/应收/收票单分录共用(应收单同步组件复用)
|
||||
* 分录单价反算:结算金额/数量(保持单价×数量=金额勾稽一致);数量无效时返回 0
|
||||
* (应付/应收/收票单分录共用,应收单同步组件复用)
|
||||
*
|
||||
* @param payableFee 应收应付货物费用行
|
||||
* @param sourceDesc 来源行描述,用于异常提示
|
||||
* @return 该行含税金额(保留 2 位小数)
|
||||
* @param amount 该行结算金额(含税)
|
||||
* @param quantity 该行分录数量
|
||||
* @return 反算单价(保留 2 位小数)
|
||||
*/
|
||||
public BigDecimal resolveFeeAmount(ReceivablePayableCargoFee payableFee, String sourceDesc) {
|
||||
BigDecimal amount = payableFee.getAfterAmount();
|
||||
if (amount == null || amount.compareTo(BigDecimal.ZERO) == 0) {
|
||||
amount = payableFee.getOriginalAmount();
|
||||
public BigDecimal entryPrice(BigDecimal amount, BigDecimal quantity) {
|
||||
if (quantity == null || quantity.compareTo(BigDecimal.ZERO) <= 0) {
|
||||
return BigDecimal.ZERO;
|
||||
}
|
||||
if (amount == null || amount.compareTo(BigDecimal.ZERO) == 0) {
|
||||
amount = money(payableFee.getUnitPrice()).multiply(money(payableFee.getTransportQuantity()));
|
||||
}
|
||||
if (amount == null || amount.compareTo(BigDecimal.ZERO) <= 0) {
|
||||
throw new ServiceException("推送金蝶失败:" + sourceDesc
|
||||
+ "未维护费用金额(调整后总金额/原总金额/单价×数量均无法取值),无法组装分录价税合计,请先维护费用数据");
|
||||
}
|
||||
return money(amount);
|
||||
return money(amount).divide(quantity, 2, RoundingMode.HALF_UP);
|
||||
}
|
||||
|
||||
/**
|
||||
* 财务应付单/付款申请分录:每条来源费用行一条(不聚合)
|
||||
* 财务应付单/付款申请分录:结算明细行 + 结算合计费用项 各一条(不聚合)
|
||||
*
|
||||
* @param materialNumber 物料编码(货物类型档案货物编码)
|
||||
* @param measureunitNumber 计量单位编码(运费计算单位,档案暂无编码字段暂传单位名称)
|
||||
* @param entryCode 分录唯一标识(awrp_entry_code):费用明细行id,雪花id天然唯一
|
||||
* @param quantity 该行运输数量
|
||||
* @param price 该行运输单价(费用明细 unit_price)
|
||||
* @param amount 该行金额(含税),对应明细应付金额 e_pricetaxtotal
|
||||
* @param amountNoTax 该行金额(不含税),对应明细金额 e_amount
|
||||
* @param materialNumber 物料编码(按项目业务类型/业务模式/装卸大类映射)
|
||||
* @param measureunitNumber 计量单位编码(费用类型匹配合同计费方案计费单位,再匹配计量单位档案)
|
||||
* @param entryCode 分录唯一标识(awrp_entry_code):结算明细行取单据号,结算合计费用项行取结算单号-费用类型-费用项
|
||||
* @param quantity 该行分录数量(运费费用项目取运输量,其它费用项目默认 1)
|
||||
* @param price 该行分录单价(结算金额/数量反算)
|
||||
* @param amount 该行金额(含税),对应明细应付金额 e_pricetaxtotal
|
||||
* @param amountNoTax 该行金额(不含税),对应明细金额 e_amount;结算合计费用项行无不含税金额为 null 不传
|
||||
* @param transportGoods 运输货物(金蝶接口新字段 awrp_transport):结算明细货物名称,结算合计费用项行为 null 不传
|
||||
* @param vehicleNo 车号(金蝶接口新字段 awrp_lpn):结算明细车号,结算合计费用项行为 null 不传
|
||||
* @param formalSettlementId 分录所属正式结算单id(多结算单付款申请 A103-1 按结算单勾稽各自应付单)
|
||||
*/
|
||||
public record BfiEntryGroup(String materialNumber, String measureunitNumber, String entryCode,
|
||||
BigDecimal quantity, BigDecimal price, BigDecimal amount, BigDecimal amountNoTax,
|
||||
Long formalSettlementId) {
|
||||
String transportGoods, String vehicleNo, Long formalSettlementId) {
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+75
-100
@@ -35,7 +35,6 @@ import org.springblade.transport.constant.KingdeeBfiConstant;
|
||||
import org.springblade.transport.pojo.entity.ContractManage;
|
||||
import org.springblade.transport.pojo.entity.FormalSettlement;
|
||||
import org.springblade.transport.pojo.entity.FormalSettlementDetail;
|
||||
import org.springblade.transport.pojo.entity.ReceivablePayableCargoFee;
|
||||
import org.springblade.transport.service.KingdeeSyncContext;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@@ -118,18 +117,17 @@ public class KingdeeArBillSyncSupport {
|
||||
* 推送金蝶财务应收单(A302-2 财务应收单保存,由应收正式结算单「同步金蝶」触发)。
|
||||
* 推送前先经 A302-4 按防重键(awrp_src_sys + awrp_src_code)查询:金蝶已存在财务应收单时
|
||||
* 幂等跳过保存并返回已有单据编号(说明上次同步已保存成功、在后续环节中断,应续推而非拦截报错)。
|
||||
* 明细分录逐应收货物费用行生成(不聚合),含税金额取应收货物费用行调整后总金额、不含税取结算货物费用行不含税;
|
||||
* 明细分录与应付单(A102-2)同源同口径:页面结算明细(每行一条)+ 结算合计费用项(每行一条);
|
||||
* 收款计划 planentity 暂不传,由金蝶按明细自动汇总生成
|
||||
*
|
||||
* @param arBillSrcCode 财务应收单防重键(正式结算单号_结算明细单据号拼接)
|
||||
* @param contractId 正式结算单关联合同id
|
||||
* @param payerName 结算单付款方(客户名称),往来户取数来源
|
||||
* @param entryGroups 结算明细解析出的应收单分录
|
||||
* @param settlement 正式结算单(合同/付款方取数来源)
|
||||
* @param entryGroups 结算明细+结算合计费用项解析出的应收单分录
|
||||
* @param subject 业务单据号(正式结算单号,日志与异常提示用)
|
||||
* @param context 金蝶同步上下文(缓存载体,预检命中与应付共用防重键缓存)
|
||||
* @return 财务应收单推送结果(单据编号 + 是否金蝶已存在幂等跳过)
|
||||
*/
|
||||
public KingdeeApBillSyncSupport.BfiApBillPushOutcome pushArBill(String arBillSrcCode, Long contractId, String payerName,
|
||||
public KingdeeApBillSyncSupport.BfiApBillPushOutcome pushArBill(String arBillSrcCode, FormalSettlement settlement,
|
||||
List<KingdeeApBillSyncSupport.BfiEntryGroup> entryGroups,
|
||||
String subject, KingdeeSyncContext context) {
|
||||
// A302-4 幂等预检:按防重键查询金蝶是否已生成财务应收单,已存在则幂等跳过保存,直接返回已有单据编号
|
||||
@@ -139,7 +137,7 @@ public class KingdeeArBillSyncSupport {
|
||||
log.info("正式结算单[{}]金蝶财务应收单已生成(单据编号[{}]),幂等跳过 A302-2 保存", subject, existBillNo);
|
||||
return new KingdeeApBillSyncSupport.BfiApBillPushOutcome(existBillNo, true);
|
||||
}
|
||||
ContractManage contract = kingdeeApBillSyncSupport.requireContract(contractId, context);
|
||||
ContractManage contract = kingdeeApBillSyncSupport.requireContract(settlement.getContractId(), context);
|
||||
// 单据类型按映射字典转换:默认服务销售应收,金蝶确认其他类型后修改 BFI_AR_BILL_BILLTYPE_NAME 即可
|
||||
String billtypeNumber = KingdeeBfiConstant.billtypeBy(KingdeeBfiConstant.AR_BILL_BILLTYPE_MAP, KingdeeBfiConstant.BFI_AR_BILL_BILLTYPE_NAME);
|
||||
if (Func.isBlank(billtypeNumber)) {
|
||||
@@ -151,6 +149,7 @@ public class KingdeeArBillSyncSupport {
|
||||
// 单据日期:yyyy-MM-dd HH:mm:ss(各接口统一格式),以同步当日作为应收单据日期
|
||||
String billDateText = kingdeeApBillSyncSupport.bfiDateTimeText(LocalDate.now());
|
||||
// 往来户.编码:应收结算单付款方(客户)关联客商档案客商编码
|
||||
String payerName = settlement.getPayerName();
|
||||
if (Func.isBlank(payerName)) {
|
||||
throw new ServiceException("推送金蝶财务应收单失败,结算单付款方不能为空,请检查正式结算单[" + Func.toStr(subject) + "]付款方信息");
|
||||
}
|
||||
@@ -190,129 +189,105 @@ public class KingdeeArBillSyncSupport {
|
||||
// 【必填】外部单据类型
|
||||
param.setAwrpSrcType(KingdeeBfiConstant.BFI_AR_BILL_SRC_TYPE);
|
||||
|
||||
// 明细分录:每条应收货物费用行一条(不聚合),无吨位时数量兜底 1
|
||||
// 汇率日期:正式结算单页面维护的汇率日期(非必传,未维护不送)
|
||||
if (settlement.getExchangeRateDate() != null) {
|
||||
param.setExratedate(kingdeeApBillSyncSupport.bfiDateTimeText(settlement.getExchangeRateDate()));
|
||||
}
|
||||
// 汇率:正式结算单页面维护的结算汇率(非必传,未维护不送)
|
||||
if (settlement.getExchangeRate() != null) {
|
||||
param.setExchangerate(settlement.getExchangeRate());
|
||||
}
|
||||
|
||||
// 明细分录:与应付单同源同口径——结算明细行 + 结算合计费用项各一条(不聚合),无吨位时数量兜底 1
|
||||
List<BfiArBillSaveEntryDTO> entry = new ArrayList<>();
|
||||
for (KingdeeApBillSyncSupport.BfiEntryGroup entryGroup : entryGroups) {
|
||||
BfiArBillSaveEntryDTO row = new BfiArBillSaveEntryDTO();
|
||||
// 【必填】物料编码.编码:结算明细货物类型匹配货物类型档案货物编码
|
||||
row.setEMaterialNumber(entryGroup.materialNumber());
|
||||
// 【必填】计量单位.编码:货物费用行运费计算单位(快照 price_unit,档案暂无编码字段暂传单位名称)
|
||||
|
||||
String materialNumber = entryGroup.materialNumber();
|
||||
|
||||
// 【必填】物料编码.编码:按项目业务类型/业务模式/装卸大类映射(与应付单同源)
|
||||
row.setEMaterialNumber(materialNumber);
|
||||
// 【必填】计量单位.编码:费用类型匹配合同计费方案计费单位,再匹配计量单位档案编码
|
||||
row.setEMeasureunitNumber(entryGroup.measureunitNumber());
|
||||
// 【必填】数量:该行运输数量,无吨位数据时兜底 1
|
||||
// 【必填】数量:运费费用项目取运输量,其它费用项目默认 1
|
||||
row.setEQuantity(entryGroup.quantity().compareTo(BigDecimal.ZERO) > 0 ? entryGroup.quantity() : BigDecimal.ONE);
|
||||
// 明细.单价:该行运输单价(费用明细 unit_price;单据头 ispricetotal=true 录入总价,单价仅作参考)
|
||||
// 运输货物(金蝶接口新字段):结算明细货物名称,结算合计费用项行不传
|
||||
row.setAwrpTransport(entryGroup.transportGoods());
|
||||
// 车号(金蝶接口新字段):结算明细车号,结算合计费用项行不传
|
||||
row.setAwrpLpn(entryGroup.vehicleNo());
|
||||
|
||||
// 明细.单价:结算金额/数量反算(单据头 ispricetotal=true 录入总价,单价仅作参考)
|
||||
row.setEUnitprice(entryGroup.price());
|
||||
// 应收金额(含税):该行应收货物费用行调整后总金额(含税)
|
||||
// 应收金额(含税):结算明细行结算金额(含税)/结算合计费用项行结算金额
|
||||
row.setERecamount(entryGroup.amount());
|
||||
// 金额(不含税):该行结算金额(不含税)(应收费用行无不含税字段,经结算货物费用行回链)
|
||||
// 金额(不含税):结算明细行结算金额(不含税),结算合计费用项行无不含税金额不传
|
||||
row.setEAmount(entryGroup.amountNoTax());
|
||||
// 【必填】项目.项目编码(联调期临时传金蝶测试档案值)
|
||||
row.setProjectNumber(KingdeeBfiConstant.BFI_AP_BILL_PROJECT_NUMBER_TEST);
|
||||
|
||||
// 调试
|
||||
|
||||
if (true) {
|
||||
// 【必填】物料编码.编码:结算明细货物类型匹配货物类型档案货物编码
|
||||
row.setEMaterialNumber(KingdeeBfiConstant.BFI_AP_BILL_MATERIAL_NUMBER_TEST);
|
||||
// 【必填】计量单位.编码:货物费用行运费计算单位(快照 price_unit,档案暂无编码字段暂传单位名称)
|
||||
row.setEMeasureunitNumber(KingdeeBfiConstant.BFI_AP_BILL_MEASUREUNIT_NUMBER_TEST);
|
||||
}
|
||||
|
||||
// 【必填】外部系统来源分录标识(候选键):应收货物费用行id(唯一)
|
||||
// 【必填】外部系统来源分录标识(候选键):结算明细行取单据号,结算合计费用项行取结算单号-费用类型-费用项拼接
|
||||
row.setAwrpEntryCode(entryGroup.entryCode());
|
||||
// 分录备注(非必传)
|
||||
// row.setERemark(subject);
|
||||
|
||||
|
||||
// 调试:物料编码和计量单位编码映射(4个固定组合)
|
||||
if (true) {
|
||||
// 物料+单位组合映射,只保留【已审核】物料
|
||||
Map<String, String[]> comboMap = Map.of(
|
||||
"国际货运代理服务费", new String[]{"Item-00001081", "ci"},
|
||||
"物流配送", new String[]{"Item-00001080", "TON"},
|
||||
"国内道路货物运输服务-公路运输-吨", new String[]{"XDWL11010001", "TON"},
|
||||
"国内道路货物运输服务-公路运输-公斤", new String[]{"XDWL11010002", "KG"}
|
||||
);
|
||||
// ⚠️兜底改成【已审核】物流配送 Item-00001080 + TON
|
||||
String[] defaultCombo = comboMap.get("物流配送");
|
||||
|
||||
// 构建组合键:物料名称-计量单位
|
||||
String measureunitName = entryGroup.measureunitNumber();
|
||||
String comboKey = materialNumber;
|
||||
if (materialNumber.contains("国内道路货物运输服务")) {
|
||||
comboKey = materialNumber + "-" + measureunitName;
|
||||
}
|
||||
|
||||
// 匹配组合,不匹配使用兜底组合
|
||||
String[] combo = comboMap.getOrDefault(comboKey, defaultCombo);
|
||||
materialNumber = combo[0];
|
||||
row.setEMaterialNumber(materialNumber);
|
||||
row.setEMeasureunitNumber(combo[1]);
|
||||
}
|
||||
|
||||
|
||||
entry.add(row);
|
||||
}
|
||||
param.setEntry(entry);
|
||||
|
||||
// 收款计划 planentity 不传:由金蝶按明细自动汇总生成(启用方案待业务确认后迭代)
|
||||
|
||||
log.info("推送金蝶财务应收单:正式结算单[{}],防重键[{}],明细分录{}条", subject, arBillSrcCode, entry.size());
|
||||
BfiBillSaveResultVO saveResult = bfiService.saveArBill(param);
|
||||
// 应收单id不落库,后续如需可经 A302-4 按防重键(awrp_src_sys + awrp_src_code)查询
|
||||
kingdeeApBillSyncSupport.requireBfiSaveSuccess(saveResult, "推送金蝶财务应收单");
|
||||
return new KingdeeApBillSyncSupport.BfiApBillPushOutcome(saveResult.getResult().get(0).getNumber(), false);
|
||||
String billNo = saveResult.getResult().get(0).getNumber();
|
||||
log.info("金蝶财务应收单保存成功:正式结算单[{}],单据编号[{}],明细分录{}条", subject, billNo, entry.size());
|
||||
return new KingdeeApBillSyncSupport.BfiApBillPushOutcome(billNo, false);
|
||||
}
|
||||
|
||||
// ==================== 结算明细分录解析 ====================
|
||||
|
||||
/**
|
||||
* 解析正式结算明细,逐行生成应收单分录(不聚合,每条来源费用行一条分录),分录携带所属正式结算单id。
|
||||
* <p>取数来源与应付单分录同源:结算明细 sourceDetailId 关联应收应付货物费用行(blade_receivable_payable_cargo_fee,
|
||||
* 每货物一条)——货物类型/计量单位/运输数量/含税金额直接取费用行(含税金额取数链:调整后总金额 after_amount →
|
||||
* 原总金额 → 单价×数量兜底,见 {@link KingdeeApBillSyncSupport#resolveFeeAmount});不含税金额经结算
|
||||
* 货物费用行 sourceFeeId 回链;无货物费用行时直接报错拦截。物料编码取第一种货物类型匹配货物类型档案货物编码;
|
||||
* 分录唯一标识(awrp_entry_code)取费用明细行id。</p>
|
||||
* 解析应收正式结算明细为应收单分录:与应付单(A102-2)分录解析同源同口径——
|
||||
* 页面结算明细每行一条 + 手工添加的结算合计费用项每行一条(系统自动汇总行是明细金额的再汇总不送),
|
||||
* 物料/计量单位/数量/单价/金额/分录标识取数规则一致,
|
||||
* 直接复用应付支撑组件解析(应收分录不含税金额取结算明细结算金额(不含税))
|
||||
*
|
||||
* @param settlementDetails 正式结算明细列表
|
||||
* @param context 金蝶同步上下文(缓存载体)
|
||||
* @param settlement 正式结算单
|
||||
* @param settlementDetails 该结算单下的结算明细
|
||||
* @param context 金蝶同步上下文(缓存载体)
|
||||
* @return 应收单分录列表(保持行序)
|
||||
*/
|
||||
public List<KingdeeApBillSyncSupport.BfiEntryGroup> resolveArEntryGroups(List<FormalSettlementDetail> settlementDetails,
|
||||
public List<KingdeeApBillSyncSupport.BfiEntryGroup> resolveArEntryGroups(FormalSettlement settlement,
|
||||
List<FormalSettlementDetail> settlementDetails,
|
||||
KingdeeSyncContext context) {
|
||||
if (Func.isEmpty(settlementDetails)) {
|
||||
throw new ServiceException("推送金蝶财务应收单失败:正式结算单无结算明细,无法解析物料与计量单位");
|
||||
}
|
||||
Map<String, String> cargoCodeByName = kingdeeApBillSyncSupport.cargoCodeByName(context);
|
||||
// 应收应付明细id->货物费用行(分录主取数来源,应收应付共用同一张费用行表)
|
||||
Map<Long, List<ReceivablePayableCargoFee>> feesByDetailId = kingdeeApBillSyncSupport.payableCargoFeesByDetailId(settlementDetails);
|
||||
// 费用行id->结算金额(不含税):经结算货物费用行 sourceFeeId 回链
|
||||
Map<Long, BigDecimal> noTaxByFeeId = kingdeeApBillSyncSupport.settlementNoTaxByPayableFeeId(settlementDetails);
|
||||
List<KingdeeApBillSyncSupport.BfiEntryGroup> entryGroups = new ArrayList<>();
|
||||
for (FormalSettlementDetail settlementDetail : settlementDetails) {
|
||||
List<ReceivablePayableCargoFee> fees = feesByDetailId.getOrDefault(settlementDetail.getSourceDetailId(), List.of());
|
||||
if (Func.isEmpty(fees)) {
|
||||
throw new ServiceException("推送金蝶财务应收单失败:结算明细[" + Func.toStr(settlementDetail.getDocumentNo())
|
||||
+ "]对应应收明细无货物费用行,分录取数必须来自应收费用明细,请先维护费用数据");
|
||||
}
|
||||
for (ReceivablePayableCargoFee fee : fees) {
|
||||
String sourceDesc = "应收货物费用[" + Func.toStr(fee.getCargoName()) + " 第" + Func.toStr(fee.getLineNo()) + "行]";
|
||||
appendArEntryGroups(entryGroups, cargoCodeByName, settlementDetail.getFormalSettlementId(),
|
||||
fee.getCargoType(),
|
||||
kingdeeApBillSyncSupport.resolveMeasureunitNumber(fee.getPriceUnit(), fee.getQuantityUnit()),
|
||||
fee.getId(), kingdeeApBillSyncSupport.money(fee.getUnitPrice()),
|
||||
kingdeeApBillSyncSupport.resolveFeeAmount(fee, sourceDesc),
|
||||
noTaxByFeeId.getOrDefault(fee.getId(), BigDecimal.ZERO),
|
||||
kingdeeApBillSyncSupport.money(fee.getTransportQuantity()),
|
||||
sourceDesc);
|
||||
}
|
||||
}
|
||||
return entryGroups;
|
||||
}
|
||||
|
||||
/**
|
||||
* 单条应收货物费用行生成一条分录;货物类型含多种(“、/,”分隔)时不拆行,物料编码取第一种货物类型,金额数量整行直送
|
||||
*
|
||||
* @param entryGroups 分录集合(保持行序)
|
||||
* @param cargoCodeByName 货物类型名称->货物编码映射
|
||||
* @param formalSettlementId 分录所属正式结算单id
|
||||
* @param cargoType 货物类型(支持“、”分隔多类型)
|
||||
* @param measureunitNumber 计量单位编码(运费计算单位,档案暂无编码字段暂传单位名称)
|
||||
* @param sourceRowId 应收货物费用行id,雪花id天然唯一
|
||||
* @param price 该行运输单价(费用明细 unit_price)
|
||||
* @param amount 该行金额(含税)
|
||||
* @param amountNoTax 该行金额(不含税)
|
||||
* @param quantity 该行运输数量
|
||||
* @param sourceDesc 来源行描述,用于异常提示
|
||||
*/
|
||||
private void appendArEntryGroups(List<KingdeeApBillSyncSupport.BfiEntryGroup> entryGroups, Map<String, String> cargoCodeByName,
|
||||
Long formalSettlementId, String cargoType, String measureunitNumber, Long sourceRowId,
|
||||
BigDecimal price, BigDecimal amount, BigDecimal amountNoTax, BigDecimal quantity,
|
||||
String sourceDesc) {
|
||||
if (Func.isBlank(measureunitNumber)) {
|
||||
throw new ServiceException("推送金蝶财务应收单失败:" + sourceDesc + "计量单位为空,请先维护运费计算单位");
|
||||
}
|
||||
// 货物类型含多种时不拆行(业务约定整行一条分录),物料编码取第一种货物类型
|
||||
String cargoName = kingdeeApBillSyncSupport.splitCargoTypes(cargoType).get(0);
|
||||
String materialNumber = cargoCodeByName.get(cargoName);
|
||||
if (Func.isBlank(materialNumber)) {
|
||||
throw new ServiceException("推送金蝶财务应收单失败:" + sourceDesc + "货物类型[" + cargoName + "]未在货物类型档案中维护编码,请补充后重试");
|
||||
}
|
||||
// 分录唯一标识(awrp_entry_code):应收货物费用行id,雪花id天然唯一
|
||||
String entryCode = String.valueOf(sourceRowId);
|
||||
entryGroups.add(new KingdeeApBillSyncSupport.BfiEntryGroup(materialNumber, measureunitNumber, entryCode,
|
||||
quantity, price, amount, amountNoTax, formalSettlementId));
|
||||
log.info("金蝶应收单分录生成:来源[{}],分录标识[{}],物料编码[{}],计量单位[{}],数量[{}],单价[{}],含税金额[{}],不含税金额[{}]",
|
||||
sourceDesc, entryCode, materialNumber, measureunitNumber, quantity, price, amount, amountNoTax);
|
||||
return kingdeeApBillSyncSupport.resolveEntryGroups(settlement, settlementDetails, context);
|
||||
}
|
||||
}
|
||||
|
||||
+18
-23
@@ -63,9 +63,9 @@ import java.util.Map;
|
||||
* (审批通过、已绑定镜像、一票一应付、A102-4 应付单已生成预检、A801-3 幂等预检)后调用本组件;
|
||||
* 报文组装取数:主表组织/币种/往来户复用应付支撑组件的合同派生方法,票面字段取自票池镜像
|
||||
* (invoice_type_code 等,A801-1 落库),明细分录与财务应付单 A102-2 同源同口径
|
||||
* (逐结算货物费用行一条,{@link KingdeeApBillSyncSupport.BfiEntryGroup},分录标识同取费用行id)。
|
||||
* 防重键:收票单 awrp_src_code=收票登记id,awrp_entry_code=应付货物费用行id(与应付单分录同键,
|
||||
* 便于金蝶侧逐行勾稽);A302-6 按「收票单防重键 ↔ 财务应付单防重键」一票一应付指定。
|
||||
* (结算明细行+结算合计费用项各一条,{@link KingdeeApBillSyncSupport.BfiEntryGroup},分录标识同键)。
|
||||
* 防重键:收票单 awrp_src_code=收票登记id,awrp_entry_code=结算明细行单据号/结算合计费用项拼接
|
||||
* (与应付单分录同键,便于金蝶侧逐行勾稽);A302-6 按「收票单防重键 ↔ 财务应付单防重键」一票一应付指定。
|
||||
* 财务应付单侧(A102-2/A102-4)取数与校验复用 {@link KingdeeApBillSyncSupport}。</p>
|
||||
*
|
||||
* @author Chill
|
||||
@@ -148,15 +148,15 @@ public class KingdeeReceivedInvoiceSyncSupport {
|
||||
/**
|
||||
* 推送金蝶收票单(A303-2 批量保存,调用方已完成 A801-3 幂等预检)。
|
||||
* 主表:组织/币种/往来户编码按分摊结算单关联合同派生(与应付单同源),销方=开票单位、购方=受票单位;
|
||||
* 明细分录与财务应付单 A102-2 同源同口径:逐结算货物费用行一条(不聚合),物料/数量/单价/金额取自
|
||||
* 与应付单分录同一解析结果({@link KingdeeApBillSyncSupport.BfiEntryGroup}),分录标识同取费用行id。
|
||||
* 明细分录与财务应付单 A102-2 同源同口径:结算明细行+结算合计费用项各一条(不聚合),物料/数量/单价/金额取自
|
||||
* 与应付单分录同一解析结果({@link KingdeeApBillSyncSupport.BfiEntryGroup}),分录标识同键。
|
||||
* 联调口径沿用应付侧:先只送 DTO 标注【必填】字段与票面核心字段(发票号码),
|
||||
* 非必填的税号/地址/电话/开户行等 setter 注释保留,联调确认后放开
|
||||
*
|
||||
* @param receipt 收票登记(审批通过、已绑定镜像)
|
||||
* @param mirror 票池镜像(票面主表字段取数来源)
|
||||
* @param contract 分摊应付结算单关联合同(组织/币种/往来户派生)
|
||||
* @param entryGroups 分摊结算单解析出的货物费用行分录(与应付单 A102-2 同一解析结果)
|
||||
* @param entryGroups 分摊结算单解析出的分录(与应付单 A102-2 同一解析结果)
|
||||
* @param context 金蝶同步上下文(缓存载体)
|
||||
* @return 金蝶收票单单据编号
|
||||
*/
|
||||
@@ -211,7 +211,7 @@ public class KingdeeReceivedInvoiceSyncSupport {
|
||||
// param.setRemark(receipt.getRemark());
|
||||
// 【必填】外部单据类型
|
||||
param.setAwrpSrcType(KingdeeBfiConstant.BFI_RECEIVED_INVOICE_SRC_TYPE);
|
||||
// 【必填】明细分录:与应付单 A102-2 同源同口径,逐结算货物费用行一条(不聚合)
|
||||
// 【必填】明细分录:与应付单 A102-2 同源同口径,结算明细行+结算合计费用项各一条(不聚合)
|
||||
param.setEntry(buildEntries(entryGroups));
|
||||
|
||||
BfiBillSaveResultVO saveResult = bfiService.saveReceivedInvoice(param);
|
||||
@@ -220,12 +220,12 @@ public class KingdeeReceivedInvoiceSyncSupport {
|
||||
}
|
||||
|
||||
/**
|
||||
* 分录组装与应付单 A102-2 明细同源同口径:逐结算货物费用行一条(不聚合),物料/数量/单价/含税/不含税金额
|
||||
* 取自 {@link KingdeeApBillSyncSupport.BfiEntryGroup}(与应付单分录同一解析结果),分录标识同取费用行id,
|
||||
* 便于金蝶侧收票单明细与应付单明细逐行勾稽;数量无吨位时兜底 1;
|
||||
* 分录组装与应付单 A102-2 明细同源同口径:结算明细行+结算合计费用项各一条(不聚合),物料/数量/单价/含税/不含税金额
|
||||
* 取自 {@link KingdeeApBillSyncSupport.BfiEntryGroup}(与应付单分录同一解析结果),分录标识同键,
|
||||
* 便于金蝶侧收票单明细与应付单明细逐行勾稽;数量无效时兜底 1;
|
||||
* 票面明细行(items_json)不再作为分录取数来源,税收分类/税率/税额随应付口径暂不送(联调强校验时再补)
|
||||
*
|
||||
* @param entryGroups 分摊结算单解析出的货物费用行分录(与应付单 A102-2 同一解析结果)
|
||||
* @param entryGroups 分摊结算单解析出的分录(与应付单 A102-2 同一解析结果)
|
||||
* @return A303-2 明细分录
|
||||
*/
|
||||
private List<BfiReceivedInvoiceSaveEntryDTO> buildEntries(List<KingdeeApBillSyncSupport.BfiEntryGroup> entryGroups) {
|
||||
@@ -238,24 +238,19 @@ public class KingdeeReceivedInvoiceSyncSupport {
|
||||
entry.setQuantity(entryGroup.quantity().compareTo(BigDecimal.ZERO) > 0 ? entryGroup.quantity() : BigDecimal.ONE);
|
||||
// 【必填】单价:该行运输单价(与应付单同源;单据头 isincludetax=true 录入价税合计,单价仅作参考)
|
||||
entry.setPrice(entryGroup.price());
|
||||
// 【必填】物料编码.编码:结算明细货物类型匹配货物类型档案货物编码
|
||||
// 【必填】物料编码.编码:按项目业务类型/业务模式/装卸大类映射(与应付单同源)
|
||||
entry.setMaterialNumber(entryGroup.materialNumber());
|
||||
// 金额(不含税):该行结算金额(不含税)(与应付单 e_amount 同源取数)
|
||||
entry.setEAmount(kingdeeApBillSyncSupport.money(entryGroup.amountNoTax()));
|
||||
// 【必填】外部系统来源分录标识(候选键):应付货物费用行id(与应付单分录同键,便于逐行勾稽)
|
||||
entry.setAwrpEntryCode(entryGroup.entryCode());
|
||||
|
||||
// 调试(与应付单 A102-2 保持一致:联调期物料编码临时传金蝶测试档案值,联调通过后与应付侧同步移除)
|
||||
|
||||
if (true) {
|
||||
// 【必填】物料编码.编码:结算明细货物类型匹配货物类型档案货物编码
|
||||
entry.setMaterialNumber(KingdeeBfiConstant.BFI_AP_BILL_MATERIAL_NUMBER_TEST);
|
||||
// 金额(不含税):该行结算金额(不含税)(与应付单 e_amount 同源取数;结算合计费用项行无不含税金额不传)
|
||||
if (entryGroup.amountNoTax() != null) {
|
||||
entry.setEAmount(kingdeeApBillSyncSupport.money(entryGroup.amountNoTax()));
|
||||
}
|
||||
// 【必填】外部系统来源分录标识(候选键):与应付单分录同键(结算明细行单据号/结算合计费用项拼接),便于逐行勾稽
|
||||
entry.setAwrpEntryCode(entryGroup.entryCode());
|
||||
|
||||
entries.add(entry);
|
||||
}
|
||||
if (entries.isEmpty()) {
|
||||
throw new ServiceException("推送金蝶收票单失败:分摊结算单无货物费用行,无法组装收票单明细");
|
||||
throw new ServiceException("推送金蝶收票单失败:分摊结算单无结算明细与结算合计费用项,无法组装收票单明细");
|
||||
}
|
||||
return entries;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user