fix: 财务应付单保存A102-2,传业务字段

This commit is contained in:
刘泉佳
2026-09-17 20:34:27 +08:00
parent bdd1d7571f
commit 25a5b63ecc
2 changed files with 316 additions and 65 deletions
@@ -58,21 +58,12 @@ public final class KingdeeBfiConstant {
/**
* A102-2 付款方式(赊购 CREDIT,允许金蝶侧后续调整付款金额;
* 现购 CASH 会由金蝶自动 100% 推付款)。待确认:如业务要求现购再调整。
*/
public static final String BFI_AP_BILL_PUR_MODE = "CREDIT";
public static final String BFI_AP_BILL_PUR_MODE_CREDIT = "CREDIT";
/**
* A102-2 明细必填:物料编码。TMS 无物料档案,当前为占位空值,
* 推送前会校验拦截。待确认:需金蝶侧提供运输服务类物料编码后补充。
* A102-2 付款方式 现购 CASH 会由金蝶自动 100% 推付款)。
*/
public static final String BFI_AP_BILL_MATERIAL = "GWZB01.003";
/**
* A102-2 明细必填:计量单位编码。TMS 无对应档案,当前为占位空值,
* 推送前会校验拦截。待确认:需金蝶侧确认编码(如"吨")后补充。
*/
public static final String BFI_AP_BILL_MEASURE_UNIT = "PCS";
public static final String BFI_AP_BILL_PUR_MODE_CASH = "CASH";
/** A102-2 必填:外部单据类型(应付单来源标识)。待确认:取值需金蝶侧确认,确认后如有调整直接改此常量。 */
public static final String BFI_AP_BILL_SRC_TYPE = "TMS财务应付单";
@@ -31,9 +31,12 @@ import org.springblade.core.tool.jackson.JsonUtil;
import org.springblade.core.tool.utils.BeanUtil;
import org.springblade.core.tool.utils.Func;
import org.springblade.core.tool.utils.SpringUtil;
import org.springblade.core.tool.api.R;
import org.springblade.transport.constant.KingdeeBfiConstant;
import org.springblade.system.cache.DictBizCache;
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.DictBiz;
import org.springblade.system.pojo.entity.Dept;
import org.springblade.thirdparty.bfi.pojo.dto.BfiApBillSaveDTO;
@@ -51,6 +54,8 @@ import org.springblade.thirdparty.bfi.pojo.vo.BfiPayApplyVO;
import org.springblade.thirdparty.bfi.pojo.vo.BfiPayBillVO;
import org.springblade.thirdparty.bfi.service.IBfiService;
import org.springblade.thirdparty.bfi.util.BfiUtil;
import org.springblade.transport.mapper.FormalSettlementDetailFeeMapper;
import org.springblade.transport.mapper.FormalSettlementDetailMapper;
import org.springblade.transport.mapper.FormalSettlementMapper;
import org.springblade.transport.mapper.FormalSettlementPaymentMapper;
import org.springblade.transport.mapper.BillLedgerMapper;
@@ -71,6 +76,8 @@ import org.springblade.transport.pojo.dto.PaymentApplicationRecordRequest;
import org.springblade.transport.pojo.dto.PaymentApplicationSaveRequest;
import org.springblade.transport.pojo.dto.PaymentApplicationStatusRequest;
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.FormalSettlementPayment;
import org.springblade.transport.pojo.entity.BillLedger;
import org.springblade.transport.pojo.entity.BillLedgerUsage;
@@ -100,6 +107,7 @@ import java.time.LocalDate;
import java.time.LocalDateTime;
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;
@@ -131,6 +139,8 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl<PaymentApplic
private final PreSettlementAdvanceMapper preSettlementAdvanceMapper;
private final PreSettlementMapper preSettlementMapper;
private final FormalSettlementMapper formalSettlementMapper;
private final FormalSettlementDetailMapper formalSettlementDetailMapper;
private final FormalSettlementDetailFeeMapper formalSettlementDetailFeeMapper;
private final FormalSettlementPaymentMapper formalSettlementPaymentMapper;
private final ProjectApplyMapper projectApplyMapper;
private final ContractManageMapper contractManageMapper;
@@ -140,6 +150,7 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl<PaymentApplic
private final TemporaryCreditLimitMapper temporaryCreditLimitMapper;
private final IFormalSettlementService formalSettlementService;
private final IBfiService bfiService;
private final ISysClient sysClient;
@Value("${thirdParty.bfi.payApply.srcSys:WLTMS}")
private String bfiPayApplySrcSys;
@Value("${thirdParty.bfi.payApply.billtype:ap_payapply_oth_BT_S}")
@@ -360,13 +371,17 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl<PaymentApplic
requirePayableSettlement(entity);
// 正式结算付款:先推财务应付单(A102-2,仅校验保存成功,应付单id不落库,
// 后续如需可经 A102-4 按 awrp_src_sys + awrp_src_code 查询),再由应付下推付款申请(A103-1)
pushBfiApBill(entity);
kingdeeBillId = pushBfiPayApplyPush(entity);
// 分录按结算明细货物编码+计量单位分组拆分(物料编码取货物类型档案编码、计量单位暂传货物单位、
// 数量取组内运输数量合计),财务应付单 detailentry 与付款申请 entry 一一对应
BfiEntryGroupResult entryGroupResult = resolveBfiEntryGroups(entity);
pushBfiApBill(entity, entryGroupResult.entryGroups(), entryGroupResult.documentNos());
kingdeeBillId = pushBfiPayApplyPush(entity, entryGroupResult.entryGroups());
} else {
// 预付款:直接推付款申请单(A103-3),不绑应付单
kingdeeBillId = pushBfiPayApply(entity);
}
} catch (ServiceException exception) {
log.error("付款申请[{}]推送金蝶付款申请单异常", entity.getPaymentNo(), exception);
markSyncFailed(entity);
throw exception;
} catch (Exception exception) {
@@ -756,17 +771,14 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl<PaymentApplic
}
/**
* 推送金蝶财务应付单(A102-2 财务应付单保存,正式结算付款第一步),仅校验保存成功
* 推送金蝶财务应付单(A102-2 财务应付单保存,正式结算付款第一步),仅校验保存成功。
* 明细分录按结算明细货物编码+计量单位分组推送,金额为各组分配后的申请付款金额。
*
* @param entity 付款申请实体
* @param entity 付款申请实体
* @param entryGroups 结算明细聚合出的应付单分录组
* @param documentNos 结算明细单据号(去重),用于拼接外部系统来源单据号
*/
private void pushBfiApBill(PaymentApplication entity) {
if (Func.isBlank(KingdeeBfiConstant.BFI_AP_BILL_MATERIAL)) {
throw new ServiceException("推送金蝶财务应付单失败:明细物料编码待金蝶侧确认后补充(常量 KingdeeBfiConstant.BFI_AP_BILL_MATERIAL)");
}
if (Func.isBlank(KingdeeBfiConstant.BFI_AP_BILL_MEASURE_UNIT)) {
throw new ServiceException("推送金蝶财务应付单失败:明细计量单位待金蝶侧确认后补充(常量 KingdeeBfiConstant.BFI_AP_BILL_MEASURE_UNIT)");
}
private void pushBfiApBill(PaymentApplication entity, List<BfiEntryGroup> entryGroups, List<String> documentNos) {
ContractManage contract = requireContract(entity);
// 单据类型按映射字典转换:默认服务采购应付,金蝶确认其他类型后修改 BFI_AP_BILL_BILLTYPE_NAME 即可
String billtypeNumber = KingdeeBfiConstant.billtypeBy(KingdeeBfiConstant.AP_BILL_BILLTYPE_MAP, KingdeeBfiConstant.BFI_AP_BILL_BILLTYPE_NAME);
@@ -778,7 +790,6 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl<PaymentApplic
String currency = bfiCurrency(contract);
// TODO 待确认:日期格式沿用现有 A103-3 的 yyyyMMdd,若金蝶校验要求 yyyy-MM-dd 再统一调整
String billDateText = (entity.getApplyDate() == null ? LocalDate.now() : entity.getApplyDate()).format(DateTimeFormatter.BASIC_ISO_DATE);
BigDecimal applyAmount = money(entity.getAppliedAmount()).setScale(2, RoundingMode.HALF_UP);
BfiApBillSaveDTO param = new BfiApBillSaveDTO();
// 【必填】单据类型.编码
param.setBilltypeidNumber(billtypeNumber);
@@ -791,7 +802,7 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl<PaymentApplic
// 【必填】往来户.编码
param.setAsstactNumber(bfiSupplierCode(entity));
// 【必填】付款方式(CASH:现购, CREDIT:赊购)
param.setPurmode(KingdeeBfiConstant.BFI_AP_BILL_PUR_MODE);
param.setPurmode(KingdeeBfiConstant.BFI_AP_BILL_PUR_MODE_CASH);
// 【必填】结算币别.货币代码
param.setCurrencyNumber(currency);
// 【必填】付款组织.编码
@@ -815,8 +826,11 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl<PaymentApplic
param.setRemark(entity.getPaymentNo() + (Func.isEmpty(entity.getSettlementNo()) ? "" : " " + entity.getSettlementNo()));
// 【必填】外部系统编号(候选键)
param.setAwrpSrcSys(bfiPayApplySrcSys);
// 【必填】外部系统来源单据号(候选键)
param.setAwrpSrcCode(entity.getPaymentNo());
// 【必填】外部系统来源单据号(候选键):付款申请号_结算明细单据号1_单据号2...
List<String> awrpSrcCodeParts = new ArrayList<>();
awrpSrcCodeParts.add(entity.getPaymentNo());
awrpSrcCodeParts.addAll(documentNos);
param.setAwrpSrcCode(String.join("_", awrpSrcCodeParts));
// 来源单据(非必传,有上游合同时传 awrp_contract_info)
param.setAwrpSrcBt(KingdeeBfiConstant.BFI_CONTRACT_SRC_BT);
// 上游外部系统单据号/上游合同号(非必传)
@@ -826,22 +840,28 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl<PaymentApplic
// 【必填】外部单据类型
param.setAwrpSrcType(KingdeeBfiConstant.BFI_AP_BILL_SRC_TYPE);
BfiApBillSaveEntryDTO entry = new BfiApBillSaveEntryDTO();
// 【必填】物料编码.编码
entry.setMaterialNumber(KingdeeBfiConstant.BFI_AP_BILL_MATERIAL);
// 【必填】计量单位.编码
entry.setMeasureunitNumber(KingdeeBfiConstant.BFI_AP_BILL_MEASURE_UNIT);
// 【必填】数量(无吨位数据:数量固定 1,配合 ispricetotal=true 录入总价)
entry.setQuantity(BigDecimal.ONE);
// 应付金额(含税)(非必传)
entry.setEPricetaxtotal(applyAmount);
// 【必填】项目.项目编码
entry.setProjectNumber(bfiProjectNumber(entity));
// 【必填】外部系统来源分录标识(候选键),A103-1 下推时以 awrp_finapbill_entryid 关联此行
entry.setAwrpEntryCode(entity.getPaymentNo() + "-1");
// 分录备注(非必传)
entry.setERemark(entity.getRemark());
param.setDetailentry(List.of(entry));
// 明细分录:按货物编码+计量单位分组(数量取组内运输数量合计,无吨位时兜底 1)
List<BfiApBillSaveEntryDTO> detailentry = new ArrayList<>();
for (int index = 0; index < entryGroups.size(); index++) {
BfiEntryGroup entryGroup = entryGroups.get(index);
BfiApBillSaveEntryDTO entry = new BfiApBillSaveEntryDTO();
// 【必填】物料编码.编码:结算明细货物类型匹配货物类型档案货物编码
entry.setMaterialNumber(entryGroup.materialNumber());
// 【必填】计量单位.编码:货物费用行运费计算单位(快照 price_unit,档案暂无编码字段暂传单位名称)
entry.setMeasureunitNumber(entryGroup.measureunitNumber());
// 【必填】数量:组内结算明细运输数量合计,无吨位数据时兜底 1
entry.setQuantity(entryGroup.quantity().compareTo(BigDecimal.ZERO) > 0 ? entryGroup.quantity() : BigDecimal.ONE);
// 应付金额(含税)(非必传):按组内结算金额占比分配的申请付款金额
entry.setEPricetaxtotal(entryGroup.amount());
// 【必填】项目.项目编码
entry.setProjectNumber(bfiProjectNumber(entity));
// 【必填】外部系统来源分录标识(候选键),A103-1 下推时以 awrp_finapbill_entryid 关联此行
entry.setAwrpEntryCode(entity.getPaymentNo() + "-" + (index + 1));
// 分录备注(非必传)
entry.setERemark(entity.getRemark());
detailentry.add(entry);
}
param.setDetailentry(detailentry);
// 付款计划 planentity 不传,由金蝶按明细自动汇总生成
// TODO 待业务确认:存在预付款抵扣时是否需传预付信息 preentry(y_billtype/y_billid/y_settleamt),本期不传、后续迭代
@@ -852,17 +872,18 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl<PaymentApplic
}
/**
* 推送金蝶付款申请(A103-1 应付下推付款申请,正式结算付款第二步),返回星瀚付款申请单id
* 推送金蝶付款申请(A103-1 应付下推付款申请,正式结算付款第二步),返回星瀚付款申请单id。
* 付款申请分录与财务应付单明细分录一一对应,金额为各组分配后的申请付款金额。
*
* @param entity 付款申请实体
* @param entity 付款申请实体
* @param entryGroups 结算明细聚合出的应付单分录组
* @return 星瀚付款申请单id
*/
private String pushBfiPayApplyPush(PaymentApplication entity) {
private String pushBfiPayApplyPush(PaymentApplication entity, List<BfiEntryGroup> entryGroups) {
ContractManage contract = requireContract(entity);
String orgNumber = bfiOrgNumber(contract);
// TODO 待确认:日期格式沿用现有 A103-3 的 yyyyMMdd,若金蝶校验要求 yyyy-MM-dd 再统一调整
String applyDateText = (entity.getApplyDate() == null ? LocalDate.now() : entity.getApplyDate()).format(DateTimeFormatter.BASIC_ISO_DATE);
BigDecimal applyAmount = money(entity.getAppliedAmount()).setScale(2, RoundingMode.HALF_UP);
BfiPayApplyPushDTO param = new BfiPayApplyPushDTO();
param.setAwrpSrcCode(entity.getPaymentNo());
param.setAwrpSrcSys(bfiPayApplySrcSys);
@@ -875,26 +896,258 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl<PaymentApplic
param.setAwrpTextfield1(bfiPaymentMethodLabel(entity.getPaymentMethod()));
param.setCreatorName(entity.getApplicantName());
BfiPayApplyPushEntryDTO entry = new BfiPayApplyPushEntryDTO();
// 正式付款固定 201
entry.setEPaymenttypeNumber(KingdeeBfiConstant.BFI_PAYMENT_TYPE_FORMAL);
// 关联 A102-2 财务应付单分录,对应应付单明细的 awrp_entry_code
entry.setAwrpFinapbillEntryid(entity.getPaymentNo() + "-1");
entry.setAwrpEntryCode(entity.getPaymentNo() + "-1");
entry.setEAssacct(entity.getBankAccount());
// TODO 待确认:往来银行为必填编码(e_bebank_number),当前暂传银行名称,待建立银行档案编码映射后调整
entry.setEBebankNumber(entity.getBankName());
entry.setEApplyamount(applyAmount);
entry.setEExpaydate(applyDateText);
entry.setProjectNumber(bfiProjectNumber(entity));
entry.setERemark(entity.getRemark());
entry.setAwrpSrcType(bfiPayApplySrcType);
param.setEntry(List.of(entry));
// 付款申请分录与财务应付单明细分录一一对应(awrp_finapbill_entryid 关联应付单明细 awrp_entry_code)
List<BfiPayApplyPushEntryDTO> entries = new ArrayList<>();
for (int index = 0; index < entryGroups.size(); index++) {
BfiEntryGroup entryGroup = entryGroups.get(index);
BfiPayApplyPushEntryDTO entry = new BfiPayApplyPushEntryDTO();
// 正式付款固定 201
entry.setEPaymenttypeNumber(KingdeeBfiConstant.BFI_PAYMENT_TYPE_FORMAL);
// 关联 A102-2 财务应付单分录,对应应付单明细的 awrp_entry_code
entry.setAwrpFinapbillEntryid(entity.getPaymentNo() + "-" + (index + 1));
entry.setAwrpEntryCode(entity.getPaymentNo() + "-" + (index + 1));
entry.setEAssacct(entity.getBankAccount());
// TODO 待确认:往来银行为必填编码(e_bebank_number),当前暂传银行名称,待建立银行档案编码映射后调整
entry.setEBebankNumber(entity.getBankName());
// 申请金额:与对应应付单分录分配金额保持一致
entry.setEApplyamount(entryGroup.amount());
entry.setEExpaydate(applyDateText);
entry.setProjectNumber(bfiProjectNumber(entity));
entry.setERemark(entity.getRemark());
entry.setAwrpSrcType(bfiPayApplySrcType);
entries.add(entry);
}
param.setEntry(entries);
BfiBillSaveResultVO saveResult = bfiService.pushPayApply(param);
return requireBfiBillId(saveResult, "推送金蝶付款申请(应付下推)");
}
/**
* 财务应付单/付款申请分录组:结算明细按货物编码+计量单位聚合后的分录数据
*
* @param materialNumber 物料编码(货物类型档案货物编码)
* @param measureunitNumber 计量单位编码(运费计算单位,档案暂无编码字段暂传单位名称)
* @param quantity 组内运输数量合计
* @param baseAmount 组内结算金额(含税)合计,作为申请金额分配基数
* @param amount 分配后的申请付款金额
*/
private record BfiEntryGroup(String materialNumber, String measureunitNumber, BigDecimal quantity,
BigDecimal baseAmount, BigDecimal amount) {
}
/**
* 应付单分录解析结果
*
* @param entryGroups 结算明细聚合出的应付单分录组(已分配金额)
* @param documentNos 结算明细单据号(去重、保持行序),用于拼接外部系统来源单据号
*/
private record BfiEntryGroupResult(List<BfiEntryGroup> entryGroups, List<String> documentNos) {
}
/**
* 解析付款申请关联的正式结算明细,按货物编码+计量单位聚合为应付单分录组。
* <p>物料编码:货物费用行/结算明细货物类型(支持“、”分隔多类型)匹配货物类型档案货物编码;
* 计量单位:快照运费计算单位(历史快照为空时兜底数量单位),档案暂无编码字段暂以单位名称作为编码。
* 申请付款金额按各组结算金额占比分配,尾差计入最后一组分录。</p>
*
* @param entity 付款申请实体
* @return 应付单分录解析结果(已分配金额的分录组 + 结算明细单据号)
*/
private BfiEntryGroupResult resolveBfiEntryGroups(PaymentApplication entity) {
List<FormalSettlementDetail> settlementDetails = formalSettlementDetails(entity);
if (Func.isEmpty(settlementDetails)) {
throw new ServiceException("推送金蝶财务应付单失败:正式结算单无结算明细,无法解析物料与计量单位");
}
// 去重收集结算明细单据号(保持行序),用于拼接财务应付单外部系统来源单据号
List<String> documentNos = settlementDetails.stream()
.map(FormalSettlementDetail::getDocumentNo)
.filter(Func::isNotBlank)
.map(String::trim)
.distinct()
.toList();
Map<String, String> cargoCodeByName = cargoCodeByName();
LinkedHashMap<String, BfiEntryGroup> groupMap = new LinkedHashMap<>();
// 优先按货物费用行逐条取数(blade_formal_settlement_detail_fee 每货物一条,金额为结算调整后的精确值)
List<FormalSettlementDetailFee> detailFees = formalSettlementDetailFees(settlementDetails);
if (Func.isNotEmpty(detailFees)) {
for (FormalSettlementDetailFee detailFee : detailFees) {
collectEntryGroups(groupMap, cargoCodeByName, detailFee.getCargoType(),
resolveMeasureunitNumber(null, detailFee.getQuantityUnit()),
money(detailFee.getSettlementAmountTax()), money(detailFee.getTransportQuantity()),
"货物费用[" + Func.toStr(detailFee.getCargoName()) + " 第" + Func.toStr(detailFee.getLineNo()) + "行]");
}
} else {
for (FormalSettlementDetail settlementDetail : settlementDetails) {
collectEntryGroups(groupMap, cargoCodeByName, settlementDetail.getCargoType(),
resolveMeasureunitNumber(null, settlementDetail.getQuantityUnit()),
money(settlementDetail.getSettlementAmountTax()), money(settlementDetail.getTransportQuantity()),
"结算明细[" + Func.toStr(settlementDetail.getDocumentNo()) + " 第" + Func.toStr(settlementDetail.getLineNo()) + "行]");
}
}
if (groupMap.isEmpty()) {
throw new ServiceException("推送金蝶财务应付单失败:结算明细未解析出任何分录组,请检查货物类型维护");
}
// 申请付款金额按各组结算金额占比分配(保留 2 位小数、向下取整,尾差计入最后一组);无结算金额时均摊
return new BfiEntryGroupResult(allocateEntryAmounts(groupMap, money(entity.getAppliedAmount()).setScale(2, RoundingMode.HALF_UP)), documentNos);
}
/**
* 申请付款金额按各组结算金额占比分配到分录组(保留 2 位小数、向下取整,尾差计入最后一组);
* 各组结算金额合计为 0 时按组数均摊
*
* @param groupMap 分录组(按物料编码+计量单位聚合,保持行序)
* @param appliedAmount 申请付款金额(已按 2 位小数格式化)
* @return 应付单分录组(已分配金额)
*/
private List<BfiEntryGroup> allocateEntryAmounts(LinkedHashMap<String, BfiEntryGroup> groupMap, BigDecimal appliedAmount) {
List<BfiEntryGroup> groups = new ArrayList<>(groupMap.values());
BigDecimal totalBaseAmount = groups.stream().map(BfiEntryGroup::baseAmount).reduce(BigDecimal.ZERO, BigDecimal::add);
List<BfiEntryGroup> entryGroups = new ArrayList<>(groups.size());
BigDecimal allocatedAmount = BigDecimal.ZERO;
for (int index = 0; index < groups.size(); index++) {
BfiEntryGroup group = groups.get(index);
BigDecimal amount;
if (index == groups.size() - 1) {
amount = appliedAmount.subtract(allocatedAmount);
} else if (totalBaseAmount.compareTo(BigDecimal.ZERO) > 0) {
amount = appliedAmount.multiply(group.baseAmount()).divide(totalBaseAmount, 2, RoundingMode.DOWN);
} else {
amount = appliedAmount.divide(BigDecimal.valueOf(groups.size()), 2, RoundingMode.DOWN);
}
allocatedAmount = allocatedAmount.add(amount);
entryGroups.add(new BfiEntryGroup(group.materialNumber(), group.measureunitNumber(), group.quantity(), group.baseAmount(), amount));
}
return entryGroups;
}
/**
* 单条来源行(货物费用行或结算明细行)按货物类型并入分录组;一行含多种类型时金额与数量按份数均分(尾差计入最后一份)
*
* @param groupMap 分录组(按物料编码+计量单位聚合,保持行序)
* @param cargoCodeByName 货物类型名称->货物编码映射
* @param cargoType 货物类型(支持“、”分隔多类型)
* @param measureunitNumber 计量单位编码(运费计算单位,档案暂无编码字段暂传单位名称)
* @param baseAmount 结算金额(含税),作为申请金额分配基数
* @param quantity 运输数量
* @param sourceDesc 来源行描述,用于异常提示
*/
private void collectEntryGroups(LinkedHashMap<String, BfiEntryGroup> groupMap, Map<String, String> cargoCodeByName,
String cargoType, String measureunitNumber, BigDecimal baseAmount, BigDecimal quantity,
String sourceDesc) {
List<String> cargoNames = splitCargoTypes(cargoType);
if (Func.isBlank(measureunitNumber)) {
throw new ServiceException("推送金蝶财务应付单失败:" + sourceDesc + "计量单位为空,请先维护运费计算单位");
}
List<BigDecimal> baseShares = splitEqually(baseAmount, cargoNames.size());
List<BigDecimal> quantityShares = splitEqually(quantity, cargoNames.size());
for (int index = 0; index < cargoNames.size(); index++) {
String cargoName = cargoNames.get(index);
String materialNumber = cargoCodeByName.get(cargoName);
if (Func.isBlank(materialNumber)) {
throw new ServiceException("推送金蝶财务应付单失败:" + sourceDesc + "货物类型[" + cargoName + "]未在货物类型档案中维护编码,请补充后重试");
}
groupMap.merge(materialNumber + "/" + measureunitNumber,
new BfiEntryGroup(materialNumber, measureunitNumber, quantityShares.get(index), baseShares.get(index), BigDecimal.ZERO),
(left, right) -> new BfiEntryGroup(left.materialNumber(), left.measureunitNumber(),
left.quantity().add(right.quantity()), left.baseAmount().add(right.baseAmount()), BigDecimal.ZERO));
}
}
/** 付款申请关联的正式结算货物费用行(blade_formal_settlement_detail_fee,每货物一条,金额为结算调整后精确值) */
private 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();
}
return formalSettlementDetailFeeMapper.selectList(Wrappers.<FormalSettlementDetailFee>lambdaQuery()
.in(FormalSettlementDetailFee::getFormalSettlementDetailId, settlementDetailIds)
.eq(FormalSettlementDetailFee::getIsDeleted, 0)
.orderByAsc(FormalSettlementDetailFee::getLineNo));
}
/** 付款申请关联的正式结算明细(关联表优先,兼容仅有 settlementId 的存量数据) */
private List<FormalSettlementDetail> formalSettlementDetails(PaymentApplication entity) {
List<Long> settlementIds = settlementRelationMapper.selectList(Wrappers.<PaymentApplicationSettlement>lambdaQuery()
.eq(PaymentApplicationSettlement::getPaymentApplicationId, entity.getId())
.eq(PaymentApplicationSettlement::getIsDeleted, 0))
.stream().map(PaymentApplicationSettlement::getFormalSettlementId).filter(Objects::nonNull).distinct().toList();
if (Func.isEmpty(settlementIds) && entity.getSettlementId() != null) {
settlementIds = List.of(entity.getSettlementId());
}
if (Func.isEmpty(settlementIds)) {
throw new ServiceException("推送金蝶财务应付单失败:付款申请未关联正式结算单,无法解析结算明细");
}
return formalSettlementDetailMapper.selectList(Wrappers.<FormalSettlementDetail>lambdaQuery()
.in(FormalSettlementDetail::getFormalSettlementId, settlementIds)
.eq(FormalSettlementDetail::getIsDeleted, 0)
.orderByAsc(FormalSettlementDetail::getLineNo));
}
/** 货物类型名称->货物编码映射(取自货物类型档案;名称重复且编码不一致时拦截) */
private Map<String, String> cargoCodeByName() {
R<List<CargoType>> response = sysClient.getCargoTypes();
if (response == null || !response.isSuccess() || Func.isEmpty(response.getData())) {
throw new ServiceException("推送金蝶财务应付单失败:货物类型档案读取失败,请稍后重试");
}
Map<String, String> cargoCodeByName = new HashMap<>();
for (CargoType cargoType : response.getData()) {
if (cargoType == null || Func.isBlank(cargoType.getCargoName()) || Func.isBlank(cargoType.getCargoCode())) {
continue;
}
String cargoName = cargoType.getCargoName().trim();
String cargoCode = cargoType.getCargoCode().trim();
String existingCode = cargoCodeByName.putIfAbsent(cargoName, cargoCode);
if (existingCode != null && !existingCode.equals(cargoCode)) {
throw new ServiceException("推送金蝶财务应付单失败:货物类型[" + cargoName + "]在货物类型档案中存在多个编码,请检查档案数据");
}
}
return cargoCodeByName;
}
/**
* 解析计量单位编码:优先取快照运费计算单位(快照表 price_unit,随结算自动带出),
* 为空(历史快照)时兜底数量单位。计量单位档案暂无编码字段,暂以单位名称作为编码;
* 档案补充编码字段后在此处增加档案映射。
*
* @param priceUnit 快照运费计算单位(可能为空)
* @param fallbackUnit 兜底数量单位
* @return 计量单位编码,无法解析时返回 null
*/
private String resolveMeasureunitNumber(String priceUnit, String fallbackUnit) {
String unitName = Func.isNotBlank(priceUnit) ? priceUnit : fallbackUnit;
return Func.isBlank(unitName) ? null : unitName.trim();
}
/** 货物类型支持“、/,”分隔的多类型写法,拆分为单类型名称 */
private 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();
}
/** 数值按份数均分(保留 2 位小数、向下取整,尾差计入最后一份) */
private List<BigDecimal> splitEqually(BigDecimal total, int parts) {
if (parts <= 1) {
return List.of(total);
}
List<BigDecimal> shares = new ArrayList<>(parts);
BigDecimal remaining = total;
for (int index = 0; index < parts; index++) {
if (index == parts - 1) {
shares.add(remaining);
} else {
BigDecimal share = total.divide(BigDecimal.valueOf(parts), 2, RoundingMode.DOWN);
shares.add(share);
remaining = remaining.subtract(share);
}
}
return shares;
}
/**
* 校验金蝶保存/下推返回结果(单据级失败以 errors 明细抛出)
*
@@ -938,11 +1191,18 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl<PaymentApplic
return contract;
}
/**
* 合同所属组织编码
* @param contract
* @return
*/
private String bfiOrgNumber(ContractManage contract) {
Dept dept = contract.getOrganizationId() == null ? null : SysCache.getDept(contract.getOrganizationId());
String orgNumber = dept == null || Func.isBlank(dept.getDeptCode()) ? contract.getOrganizationName() : dept.getDeptCode();
if (Func.isBlank(orgNumber)) throw new ServiceException("推送金蝶付款申请失败,合同所属组织编码不能为空");
return orgNumber;
// dept为空 或者 deptCode为空,直接报错
if (dept == null || Func.isBlank(dept.getDeptCode())) {
throw new ServiceException("推送金蝶付款申请失败,合同所属组织编码不能为空,请检查部门ID【"+contract.getOrganizationId()+"】对应的部门是否维护部门编码");
}
return dept.getDeptCode();
}
private String bfiCurrency(ContractManage contract) {