⚡ 金蝶同步流程拆分:正式结算单同步推送财务应付单A102-2,付款申请同步仅推送付款申请单A103-1/A103-3
This commit is contained in:
+5
-2
@@ -25,6 +25,7 @@
|
||||
package org.springblade.transport.service;
|
||||
|
||||
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.ProjectApply;
|
||||
|
||||
@@ -42,10 +43,12 @@ public record KingdeeSyncContext(Map<String, String> cargoCodeByName, Map<Long,
|
||||
Map<String, String> customerCodeCache, Map<Long, ProjectApply> projectCache,
|
||||
Map<String, String> apBillNoCache,
|
||||
Map<Long, List<FormalSettlementDetail>> settlementDetailCache,
|
||||
Map<String, String> apBillPlanCodeCache) {
|
||||
Map<String, String> apBillPlanCodeCache,
|
||||
Map<Long, FormalSettlement> settlementCache) {
|
||||
|
||||
/** 空上下文:缓存可写但初始为空,首条记录命中后全批复用 */
|
||||
public static KingdeeSyncContext empty() {
|
||||
return new KingdeeSyncContext(new HashMap<>(), new HashMap<>(), new HashMap<>(), new HashMap<>(), new HashMap<>(), new HashMap<>(), new HashMap<>());
|
||||
return new KingdeeSyncContext(new HashMap<>(), new HashMap<>(), new HashMap<>(), new HashMap<>(),
|
||||
new HashMap<>(), new HashMap<>(), new HashMap<>(), new HashMap<>());
|
||||
}
|
||||
}
|
||||
|
||||
+32
-5
@@ -35,6 +35,7 @@ import org.springblade.transport.mapper.PaymentApplicationMapper;
|
||||
import org.springblade.transport.mapper.PaymentApplicationInvoiceMapper;
|
||||
import org.springblade.transport.mapper.PaymentApplicationSettlementMapper;
|
||||
import org.springblade.transport.mapper.ReceiptClaimSettlementMapper;
|
||||
import org.springblade.transport.constant.KingdeeBfiConstant;
|
||||
import org.springblade.transport.constant.TransportBusiConstant;
|
||||
import org.springblade.transport.mapper.ReceivablePayableDetailMapper;
|
||||
import org.springblade.transport.mapper.ReceivablePayableCargoFeeMapper;
|
||||
@@ -75,6 +76,8 @@ import org.springblade.transport.service.IFormalSettlementService;
|
||||
import org.springblade.transport.service.IContractManageService;
|
||||
import org.springblade.transport.service.IPreSettlementService;
|
||||
import org.springblade.transport.service.IWaybillService;
|
||||
import org.springblade.transport.service.KingdeeSyncContext;
|
||||
import org.springblade.transport.support.KingdeeApBillSyncSupport;
|
||||
import org.springblade.transport.wrapper.PreSettlementWrapper;
|
||||
import org.springblade.core.tool.jackson.JsonUtil;
|
||||
import org.springblade.transport.wrapper.FormalSettlementWrapper;
|
||||
@@ -136,6 +139,7 @@ public class FormalSettlementServiceImpl extends BaseServiceImpl<FormalSettlemen
|
||||
private final IContractManageService contractManageService;
|
||||
private final IPreSettlementService preSettlementService;
|
||||
private final IWaybillService waybillService;
|
||||
private final KingdeeApBillSyncSupport kingdeeApBillSyncSupport;
|
||||
|
||||
@Override
|
||||
public IPage<FormalSettlementVO> selectPage(IPage<FormalSettlement> page, FormalSettlementVO query) {
|
||||
@@ -472,17 +476,40 @@ public class FormalSettlementServiceImpl extends BaseServiceImpl<FormalSettlemen
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public String syncKingdee(Long id) {
|
||||
FormalSettlement settlement = existing(id);
|
||||
if (!APPROVED.equals(settlement.getApprovalStatus())) throw new ServiceException("仅审批通过的正式结算单允许同步金蝶");
|
||||
// 单据类型路由:应付对应财务应付单(A102-2),应收对应财务应收单(A302-2),推送规则待业务确认,暂拦截报错
|
||||
if (!"payable".equals(settlement.getSettlementType())) {
|
||||
throw new ServiceException("推送金蝶失败:结算单[" + Func.toStr(settlement.getFormalSettlementNo())
|
||||
+ "]结算类型为应收,对应财务应收单(A302-2),推送规则待业务确认后启用");
|
||||
}
|
||||
// 已同步幂等返回(含历史由付款申请同步联动回写的存量已同步数据)
|
||||
if ("synced".equals(settlement.getKingdeeSyncStatus())) return settlement.getKingdeeBillNo();
|
||||
String kingdeeNo = "K3AP" + DateTimeFormatter.ofPattern("yyyyMMddHHmmss").format(LocalDateTime.now());
|
||||
settlement.setKingdeeBillNo(kingdeeNo);
|
||||
settlement.setKingdeeSyncStatus("synced");
|
||||
// 流程拆分:正式结算单「同步金蝶」负责推送财务应付单(A102-2)并回写结算单单据编号;
|
||||
// 付款申请「同步金蝶」仅负责付款申请单(A103-1 应付下推),推送前经 A102-4 预检本单应付单已生成
|
||||
List<FormalSettlementDetail> details = detailMapper.selectList(Wrappers.<FormalSettlementDetail>lambdaQuery()
|
||||
.eq(FormalSettlementDetail::getFormalSettlementId, id)
|
||||
.eq(FormalSettlementDetail::getIsDeleted, 0)
|
||||
.orderByAsc(FormalSettlementDetail::getLineNo));
|
||||
if (Func.isEmpty(details)) {
|
||||
throw new ServiceException("推送金蝶财务应付单失败:正式结算单无结算明细,无法解析物料与计量单位");
|
||||
}
|
||||
KingdeeSyncContext context = KingdeeSyncContext.empty();
|
||||
// 财务应付单防重键(awrp_src_code):正式结算单号_结算明细单据号下划线拼接,
|
||||
// 付款申请 A103-1 下推前的前置校验与付款计划反查均按该键勾稽
|
||||
String apBillSrcCode = kingdeeApBillSyncSupport.apBillSrcCode(settlement, details);
|
||||
List<KingdeeApBillSyncSupport.BfiEntryGroup> entryGroups = kingdeeApBillSyncSupport.resolveEntryGroups(details, context);
|
||||
KingdeeApBillSyncSupport.BfiApBillPushOutcome outcome = kingdeeApBillSyncSupport.pushApBill(
|
||||
apBillSrcCode, settlement.getContractId(), entryGroups, settlement.getFormalSettlementNo(), context);
|
||||
// 回写财务应付单单据编号(A102-2 billNo)与同步状态;付款申请侧回填的付款申请单编号(A103-2)不是同一单据
|
||||
settlement.setKingdeeBillNo(outcome.billNo());
|
||||
settlement.setKingdeeSyncStatus(KingdeeBfiConstant.KINGDEE_STATUS_SYNCED);
|
||||
settlement.setSyncedTime(LocalDateTime.now());
|
||||
updateById(settlement);
|
||||
saveChange(settlement.getId(), "结算单基本信息", null, "调整", "同步金蝶单据" + kingdeeNo, "");
|
||||
return kingdeeNo;
|
||||
saveChange(settlement.getId(), "结算单基本信息", null, "调整", "同步金蝶财务应付单" + outcome.billNo(), "");
|
||||
return outcome.billNo();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
+96
-700
@@ -27,29 +27,18 @@ import lombok.extern.slf4j.Slf4j;
|
||||
import org.springblade.core.log.exception.ServiceException;
|
||||
import org.springblade.core.mp.base.BaseServiceImpl;
|
||||
import org.springblade.core.secure.utils.AuthUtil;
|
||||
import org.springblade.core.tool.api.R;
|
||||
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.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.Dept;
|
||||
import org.springblade.system.pojo.entity.DictBiz;
|
||||
import org.springblade.thirdparty.bfi.pojo.dto.BfiApBillQueryDTO;
|
||||
import org.springblade.thirdparty.bfi.pojo.dto.BfiApBillSaveDTO;
|
||||
import org.springblade.thirdparty.bfi.pojo.dto.BfiApBillSaveEntryDTO;
|
||||
import org.springblade.thirdparty.bfi.pojo.dto.BfiPayApplyPushDTO;
|
||||
import org.springblade.thirdparty.bfi.pojo.dto.BfiPayApplyPushEntryDTO;
|
||||
import org.springblade.thirdparty.bfi.pojo.dto.BfiPayApplyQueryDTO;
|
||||
import org.springblade.thirdparty.bfi.pojo.dto.BfiPayApplySaveDTO;
|
||||
import org.springblade.thirdparty.bfi.pojo.dto.BfiPayApplySaveEntryDTO;
|
||||
import org.springblade.thirdparty.bfi.pojo.dto.BfiPayBillQueryDTO;
|
||||
import org.springblade.thirdparty.bfi.pojo.vo.BfiApBillPlanVO;
|
||||
import org.springblade.thirdparty.bfi.pojo.vo.BfiApBillVO;
|
||||
import org.springblade.thirdparty.bfi.pojo.vo.BfiBillSaveResultItemVO;
|
||||
import org.springblade.thirdparty.bfi.pojo.vo.BfiBillSaveResultVO;
|
||||
import org.springblade.thirdparty.bfi.pojo.vo.BfiPayApplyEntryVO;
|
||||
import org.springblade.thirdparty.bfi.pojo.vo.BfiPayApplyVO;
|
||||
@@ -62,7 +51,6 @@ import org.springblade.transport.mapper.BillLedgerMapper;
|
||||
import org.springblade.transport.mapper.BillLedgerUsageMapper;
|
||||
import org.springblade.transport.mapper.ContractManageMapper;
|
||||
import org.springblade.transport.mapper.CustomerArchiveMapper;
|
||||
import org.springblade.transport.mapper.FormalSettlementDetailFeeMapper;
|
||||
import org.springblade.transport.mapper.FormalSettlementDetailMapper;
|
||||
import org.springblade.transport.mapper.FormalSettlementMapper;
|
||||
import org.springblade.transport.mapper.FormalSettlementPaymentMapper;
|
||||
@@ -72,10 +60,8 @@ import org.springblade.transport.mapper.PaymentApplicationMapper;
|
||||
import org.springblade.transport.mapper.PaymentApplicationRecordMapper;
|
||||
import org.springblade.transport.mapper.PaymentApplicationSettlementMapper;
|
||||
import org.springblade.transport.mapper.PreSettlementAdvanceMapper;
|
||||
import org.springblade.transport.mapper.PreSettlementDetailFeeMapper;
|
||||
import org.springblade.transport.mapper.PreSettlementMapper;
|
||||
import org.springblade.transport.mapper.ProjectApplyMapper;
|
||||
import org.springblade.transport.mapper.ReceivablePayableCargoFeeMapper;
|
||||
import org.springblade.transport.mapper.TemporaryCreditLimitMapper;
|
||||
import org.springblade.transport.pojo.dto.PaymentApplicationInvoiceRequest;
|
||||
import org.springblade.transport.pojo.dto.PaymentApplicationRecordRequest;
|
||||
@@ -87,7 +73,6 @@ 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.FormalSettlementPayment;
|
||||
import org.springblade.transport.pojo.entity.FormalSettlementSource;
|
||||
import org.springblade.transport.pojo.entity.PaymentApplication;
|
||||
@@ -96,16 +81,14 @@ import org.springblade.transport.pojo.entity.PaymentApplicationRecord;
|
||||
import org.springblade.transport.pojo.entity.PaymentApplicationSettlement;
|
||||
import org.springblade.transport.pojo.entity.PreSettlement;
|
||||
import org.springblade.transport.pojo.entity.PreSettlementAdvance;
|
||||
import org.springblade.transport.pojo.entity.PreSettlementDetailFee;
|
||||
import org.springblade.transport.pojo.entity.ProjectApply;
|
||||
import org.springblade.transport.pojo.entity.ReceivablePayableCargoFee;
|
||||
import org.springblade.transport.pojo.entity.TemporaryCreditLimit;
|
||||
import org.springblade.transport.pojo.vo.PaymentApplicationReferenceAmountVO;
|
||||
import org.springblade.transport.pojo.vo.PaymentApplicationVO;
|
||||
import org.springblade.transport.service.IFormalSettlementService;
|
||||
import org.springblade.transport.service.IPaymentApplicationService;
|
||||
import org.springblade.transport.service.KingdeeSyncContext;
|
||||
import org.springblade.transport.support.ExternalSrcCodeSupport;
|
||||
import org.springblade.transport.support.KingdeeApBillSyncSupport;
|
||||
import org.springblade.transport.wrapper.PaymentApplicationWrapper;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Service;
|
||||
@@ -117,7 +100,6 @@ 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;
|
||||
@@ -151,9 +133,6 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl<PaymentApplic
|
||||
private final PreSettlementMapper preSettlementMapper;
|
||||
private final FormalSettlementMapper formalSettlementMapper;
|
||||
private final FormalSettlementDetailMapper formalSettlementDetailMapper;
|
||||
private final FormalSettlementDetailFeeMapper formalSettlementDetailFeeMapper;
|
||||
private final PreSettlementDetailFeeMapper preSettlementDetailFeeMapper;
|
||||
private final ReceivablePayableCargoFeeMapper receivablePayableCargoFeeMapper;
|
||||
private final FormalSettlementPaymentMapper formalSettlementPaymentMapper;
|
||||
private final ProjectApplyMapper projectApplyMapper;
|
||||
private final ContractManageMapper contractManageMapper;
|
||||
@@ -163,7 +142,7 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl<PaymentApplic
|
||||
private final TemporaryCreditLimitMapper temporaryCreditLimitMapper;
|
||||
private final IFormalSettlementService formalSettlementService;
|
||||
private final IBfiService bfiService;
|
||||
private final ISysClient sysClient;
|
||||
private final KingdeeApBillSyncSupport kingdeeApBillSyncSupport;
|
||||
@Value("${thirdParty.bfi.payApply.srcSys:WLTMS}")
|
||||
private String bfiPayApplySrcSys;
|
||||
@Value("${thirdParty.bfi.payApply.billtype:ap_payapply_oth_BT_S}")
|
||||
@@ -403,36 +382,38 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl<PaymentApplic
|
||||
}
|
||||
log.info("付款申请[{}]开始同步金蝶:paymentType={},申请金额={},kingdeeStatus={}",
|
||||
entity.getPaymentNo(), entity.getPaymentType(), entity.getAppliedAmount(), entity.getKingdeeStatus());
|
||||
// 记录同步前状态:生成失败(上次推送中断)的恢复场景用于触发 A103-2 勾稽预检
|
||||
// 记录同步前状态:生成失败(上次推送中断)的恢复场景用于触发 A103-2 勾稽预检(预付款路径;
|
||||
// 正式结算路径统一先经 A103-2 预检,不依赖该状态)
|
||||
String priorKingdeeStatus = entity.getKingdeeStatus();
|
||||
String kingdeeBillId = null;
|
||||
if (TransportBusiConstant.PAYMENT_TYPE_SETTLEMENT.equals(entity.getPaymentType())) {
|
||||
// 单据类型路由:按正式结算单结算类型——应付对应财务应付单(A102-2),应收对应财务应收单(A302-2,推送规则待业务确认)
|
||||
// 单据类型路由:按正式结算单结算类型——应付对应财务应付单(A102-2,由正式结算单「同步金蝶」推送),
|
||||
// 应收对应财务应收单(A302-2,推送规则待业务确认)
|
||||
requirePayableSettlement(entity);
|
||||
// 正式结算付款:先推财务应付单(A102-2,仅校验保存成功,应付单id不落库,
|
||||
// 后续如需可经 A102-4 按 awrp_src_sys + awrp_src_code 查询),再由应付下推付款申请(A103-1)
|
||||
// 分录逐费用行生成不聚合(物料编码取货物类型档案编码、含税金额取应付货物费用行调整后总金额、
|
||||
// 不含税经结算货物费用行回链),财务应付单 detailentry 与付款申请 entry 一一对应
|
||||
BfiEntryGroupResult entryGroupResult = resolveBfiEntryGroups(entity, context);
|
||||
log.info("付款申请[{}]正式结算付款:开始推送金蝶财务应付单(A102-2),分录{}条", entity.getPaymentNo(), entryGroupResult.entryGroups().size());
|
||||
BfiApBillPushOutcome apBillOutcome = pushBfiApBill(entity, entryGroupResult.entryGroups(), entryGroupResult.documentNos(), context);
|
||||
// 财务应付单单据编号即时回填 kingdeeBillNo(后续付款申请单编号经 A103-2 定时回写任务覆盖)
|
||||
if (Func.isNotEmpty(apBillOutcome.billNo())) {
|
||||
entity.setKingdeeBillNo(apBillOutcome.billNo());
|
||||
}
|
||||
log.info("付款申请[{}]金蝶财务应付单(A102-2)处理完成({}),单据编号[{}]已回填 kingdeeBillNo",
|
||||
entity.getPaymentNo(), apBillOutcome.alreadyExist() ? "金蝶已存在,幂等跳过保存" : "本次推送成功", apBillOutcome.billNo());
|
||||
// A102-2 结果回写关联正式结算单(正常推送与金蝶已存在幂等恢复统一处理):
|
||||
// 结算单回写财务应付单编号(A102-2 billNo),与付款申请后续回填的付款申请单编号(A103-2)不是同一单据
|
||||
syncSettlementKingdeeResult(entity, apBillOutcome.billNo(), context);
|
||||
if (apBillOutcome.alreadyExist()) {
|
||||
// 应付单已生成说明上次同步已保存成功、在下推或落库环节中断:先经 A103-2 查询付款申请单是否已下推成功,
|
||||
// 已生成则直接回填单据id与编号并视为同步成功(本地状态因异常回滚未落库),未生成才继续 A103-1 下推,
|
||||
// 下推接口真正报错才算同步失败
|
||||
kingdeeBillId = reconcileExistPayApply(entity);
|
||||
// 流程拆分:财务应付单(A102-2)在正式付款单页面「同步金蝶」时推送并回写结算单,付款申请仅负责付款申请单。
|
||||
// 前置校验:关联的每张正式结算单均已在金蝶生成财务应付单(A102-4 按结算单防重键预检,批量同步时整批已预热缓存)
|
||||
Map<Long, String> settlementSrcCodes = settlementApBillSrcCodes(entity, context);
|
||||
for (Map.Entry<Long, String> srcCodeEntry : settlementSrcCodes.entrySet()) {
|
||||
String existBillNo = context.apBillNoCache().computeIfAbsent(srcCodeEntry.getValue(),
|
||||
code -> kingdeeApBillSyncSupport.queryApBillExistNos(List.of(code)).getOrDefault(code, ""));
|
||||
if (Func.isEmpty(existBillNo)) {
|
||||
FormalSettlement settlement = context.settlementCache().get(srcCodeEntry.getKey());
|
||||
throw new ServiceException("推送金蝶付款申请失败:正式结算单["
|
||||
+ (settlement == null ? srcCodeEntry.getKey() : settlement.getFormalSettlementNo())
|
||||
+ "]尚未同步金蝶财务应付单(A102-2),请先在正式付款单页面同步金蝶");
|
||||
}
|
||||
}
|
||||
// A103-1 幂等预检:按付款申请号查询金蝶付款申请单是否已生成(上次下推成功但本地事务回滚未落库的场景),
|
||||
// 已生成直接回填单据id与编号视为同步成功,未生成才继续 A103-1 下推,下推接口真正报错才算同步失败
|
||||
kingdeeBillId = reconcileExistPayApply(entity);
|
||||
if (Func.isEmpty(kingdeeBillId)) {
|
||||
kingdeeBillId = pushBfiPayApplyPush(entity, entryGroupResult.entryGroups(), apBillOutcome.planEntryCodes(), context);
|
||||
// A103-1 分录与财务应付单明细分录一一对应(awrp_entry_code 取费用明细行id),
|
||||
// 多结算单付款申请各分录按所属结算单勾稽各自应付单的付款计划分录 awrp_entry_code_p
|
||||
List<KingdeeApBillSyncSupport.BfiEntryGroup> entryGroups =
|
||||
kingdeeApBillSyncSupport.resolveEntryGroups(settlementDetails(entity, context), context);
|
||||
log.info("付款申请[{}]正式结算付款:金蝶财务应付单(A102-2)已生成,开始推送金蝶付款申请单(A103-1 应付下推),分录{}条",
|
||||
entity.getPaymentNo(), entryGroups.size());
|
||||
kingdeeBillId = pushBfiPayApplyPush(entity, entryGroups, context);
|
||||
log.info("付款申请[{}]金蝶付款申请单(A103-1 应付下推)推送成功:kingdeeBillId={}", entity.getPaymentNo(), kingdeeBillId);
|
||||
}
|
||||
} else {
|
||||
@@ -449,7 +430,7 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl<PaymentApplic
|
||||
}
|
||||
}
|
||||
// 星瀚单据内部主键落 kingdeeBillId(附件上传等使用);
|
||||
// 财务应付单单据编号已在推送时回填 kingdeeBillNo,付款申请单编号与付款状态由定时回写任务经 A103-2/A103-4 查询回填
|
||||
// 付款申请单编号与付款状态由定时回写任务经 A103-2/A103-4 查询回填(kingdeeBillNo 不再暂存 A102-2 应付单编号)
|
||||
entity.setKingdeeBillId(kingdeeBillId);
|
||||
entity.setKingdeeStatus(KingdeeBfiConstant.KINGDEE_STATUS_SYNCED);
|
||||
// 付款完成模拟逻辑已停用(mockPaymentRecords / 履约付款数据直接置 paid),
|
||||
@@ -560,7 +541,8 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl<PaymentApplic
|
||||
if (Func.isBlank(orgNumber)) {
|
||||
try {
|
||||
// A103-4 付款人(公司)编码必填:优先取 A103-2 返回付款组织编码,兜底按合同所属组织解析
|
||||
orgNumber = bfiOrgNumber(requireContract(application, KingdeeSyncContext.empty()));
|
||||
orgNumber = kingdeeApBillSyncSupport.bfiOrgNumber(
|
||||
kingdeeApBillSyncSupport.requireContract(application.getContractId(), KingdeeSyncContext.empty()));
|
||||
} catch (Exception exception) {
|
||||
log.warn("付款申请[{}]付款处理查询跳过,付款组织编码获取失败:{}", application.getPaymentNo(), exception.getMessage());
|
||||
continue;
|
||||
@@ -772,120 +754,6 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl<PaymentApplic
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A102-4 批量查询已生成财务应付单:来源单据号批量 + 外部系统编号(防重键),自动翻页,
|
||||
* 用于推送前幂等预检(awrp_src_code 支持多值,整批一次查询消除逐单远程查询 N+1)
|
||||
*
|
||||
* @param awrpSrcCodes 来源单据号(付款申请号_结算明细单据号拼接)批次
|
||||
* @return 已生成财务应付单的 来源单据号->单据编号 映射
|
||||
*/
|
||||
private Map<String, String> queryApBillExistNos(List<String> awrpSrcCodes) {
|
||||
Map<String, String> existBillNos = new LinkedHashMap<>();
|
||||
// 防重键批量分批查询,避免单次过滤条件过长
|
||||
batchForeach(awrpSrcCodes, PAY_RESULT_QUERY_BATCH, batch -> {
|
||||
BfiApBillQueryDTO param = new BfiApBillQueryDTO();
|
||||
param.setAwrpSrcCode(batch);
|
||||
param.setAwrpSrcSys(bfiPayApplySrcSys);
|
||||
for (BfiApBillVO apBill : BfiUtil.queryAllPages(pageNo -> {
|
||||
param.setPageNo(pageNo);
|
||||
param.setPageSize(PAY_RESULT_PAGE_SIZE);
|
||||
return bfiService.queryApBill(param);
|
||||
})) {
|
||||
if (Func.isNotEmpty(apBill.getAwrpSrcCode())) {
|
||||
existBillNos.put(apBill.getAwrpSrcCode(), apBill.getBillno());
|
||||
}
|
||||
}
|
||||
});
|
||||
return existBillNos;
|
||||
}
|
||||
|
||||
/**
|
||||
* A102-2 结果回写关联正式结算单:金蝶单据编号(财务应付单编号)、同步状态置已同步、同步时间。
|
||||
* 正式结算单列表据此回显金蝶单据号与单据状态;一个付款申请可能关联多张正式结算单,
|
||||
* 按结算明细归属的结算单去重后批量回写,结算单已同步时重复回写为幂等更新。
|
||||
* 注意回写的是财务应付单编号(A102-2 billNo),付款申请表的 kingdeeBillNo 后续由
|
||||
* A103-2 定时回写任务覆盖为付款申请单编号,两者不是同一单据,禁止混用
|
||||
*
|
||||
* @param entity 付款申请实体
|
||||
* @param apBillBillNo 金蝶财务应付单单据编号(A102-2 返回)
|
||||
* @param context 金蝶同步上下文(缓存载体)
|
||||
*/
|
||||
private void syncSettlementKingdeeResult(PaymentApplication entity, String apBillBillNo, KingdeeSyncContext context) {
|
||||
if (Func.isEmpty(apBillBillNo)) {
|
||||
return;
|
||||
}
|
||||
List<Long> settlementIds = settlementDetails(entity, context).stream()
|
||||
.map(FormalSettlementDetail::getFormalSettlementId)
|
||||
.filter(Objects::nonNull).distinct().toList();
|
||||
if (Func.isEmpty(settlementIds)) {
|
||||
return;
|
||||
}
|
||||
formalSettlementMapper.update(null, Wrappers.<FormalSettlement>lambdaUpdate()
|
||||
.in(FormalSettlement::getId, settlementIds)
|
||||
.set(FormalSettlement::getKingdeeBillNo, apBillBillNo)
|
||||
.set(FormalSettlement::getKingdeeSyncStatus, KingdeeBfiConstant.KINGDEE_STATUS_SYNCED)
|
||||
.set(FormalSettlement::getSyncedTime, LocalDateTime.now()));
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询正式结算单下绑定、单据状态为审核通过(approved)的付款信息(blade_formal_settlement_payment)。
|
||||
* 付款申请可能关联多张正式结算单,按结算明细归属的结算单汇总去重后查询,创建时间升序。
|
||||
* 当前付款计划 planentity 暂不传、本方法未启用,保留待付款计划启用后供 A102-2 取数
|
||||
*
|
||||
* @param entity 付款申请实体
|
||||
* @param context 金蝶同步上下文(缓存载体)
|
||||
* @return 审核通过的付款信息列表
|
||||
*/
|
||||
private List<FormalSettlementPayment> approvedSettlementPayments(PaymentApplication entity, KingdeeSyncContext context) {
|
||||
List<Long> settlementIds = settlementDetails(entity, context).stream()
|
||||
.map(FormalSettlementDetail::getFormalSettlementId)
|
||||
.filter(Objects::nonNull).distinct().toList();
|
||||
if (Func.isEmpty(settlementIds)) {
|
||||
return List.of();
|
||||
}
|
||||
return formalSettlementPaymentMapper.selectList(Wrappers.<FormalSettlementPayment>lambdaQuery()
|
||||
.in(FormalSettlementPayment::getFormalSettlementId, settlementIds)
|
||||
.eq(FormalSettlementPayment::getIsDeleted, 0)
|
||||
.eq(FormalSettlementPayment::getBillStatus, APPROVED)
|
||||
.and(wrapper -> wrapper.isNull(FormalSettlementPayment::getKingdeeBillNo)
|
||||
.or().eq(FormalSettlementPayment::getKingdeeBillNo, ""))
|
||||
.orderByAsc(FormalSettlementPayment::getCreateTime));
|
||||
}
|
||||
|
||||
/**
|
||||
* A102-4 反查财务应付单付款计划分录的外部系统来源分录标识(awrp_entry_code_p)。
|
||||
* A102-2 保存未传 planentity,由金蝶按明细自动汇总生成(awrp_entry_code_p 不传时自动取外部系统单据号),
|
||||
* A103-1 应付下推的 awrp_finapbill_entryid 需填该值勾稽应付单;
|
||||
* 结果按防重键(awrp_src_code)缓存,恢复场景避免重复远程查询
|
||||
*
|
||||
* @param entity 付款申请实体(异常提示用)
|
||||
* @param apBillSrcCode 财务应付单防重键(外部系统来源单据号)
|
||||
* @param context 金蝶同步上下文(缓存载体)
|
||||
* @return 付款计划分录 awrp_entry_code_p
|
||||
*/
|
||||
private String apBillPlanEntryCode(PaymentApplication entity, String apBillSrcCode, KingdeeSyncContext context) {
|
||||
return context.apBillPlanCodeCache().computeIfAbsent(apBillSrcCode, code -> {
|
||||
BfiApBillQueryDTO param = new BfiApBillQueryDTO();
|
||||
param.setAwrpSrcCode(List.of(code));
|
||||
param.setAwrpSrcSys(bfiPayApplySrcSys);
|
||||
List<BfiApBillVO> bills = BfiUtil.queryAllPages(pageNo -> {
|
||||
param.setPageNo(pageNo);
|
||||
param.setPageSize(PAY_RESULT_PAGE_SIZE);
|
||||
return bfiService.queryApBill(param);
|
||||
});
|
||||
for (BfiApBillVO apBill : bills) {
|
||||
if (Func.isNotEmpty(apBill.getPlanentity())) {
|
||||
for (BfiApBillPlanVO plan : apBill.getPlanentity()) {
|
||||
if (Func.isNotBlank(plan.getAwrpEntryCodeP())) {
|
||||
return plan.getAwrpEntryCodeP();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
throw new ServiceException("推送金蝶付款申请(应付下推)失败:财务应付单[" + entity.getPaymentNo() + "]付款计划分录 awrp_entry_code_p 未查询到,无法勾稽关联");
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* A103-4 分页查询:按付款人(公司)编码 + 最近1个月业务日期窗口(接口限制不超过1个月),自动翻页
|
||||
*
|
||||
@@ -955,14 +823,14 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl<PaymentApplic
|
||||
}
|
||||
|
||||
/**
|
||||
* 金蝶接口日期文本:yyyy-MM-dd HH:mm:ss(申请日期为空时取当天),A102-2/A103-1/A103-3 各接口统一格式
|
||||
* 金蝶接口日期文本:yyyy-MM-dd HH:mm:ss(申请日期为空时取当天),A103-1/A103-3 接口统一格式
|
||||
*
|
||||
* @param entity 付款申请实体
|
||||
* @return 日期文本
|
||||
*/
|
||||
private String bfiDateTimeText(PaymentApplication entity) {
|
||||
LocalDate applyDate = entity.getApplyDate() == null ? LocalDate.now() : entity.getApplyDate();
|
||||
return applyDate.atStartOfDay().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
|
||||
return kingdeeApBillSyncSupport.bfiDateTimeText(applyDate);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -972,9 +840,9 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl<PaymentApplic
|
||||
* @return 金蝶付款申请单id
|
||||
*/
|
||||
private String pushBfiPayApply(PaymentApplication entity, KingdeeSyncContext context) {
|
||||
ContractManage contract = requireContract(entity, context);
|
||||
String orgNumber = bfiOrgNumber(contract);
|
||||
String currency = bfiCurrency(contract);
|
||||
ContractManage contract = kingdeeApBillSyncSupport.requireContract(entity.getContractId(), context);
|
||||
String orgNumber = kingdeeApBillSyncSupport.bfiOrgNumber(contract);
|
||||
String currency = kingdeeApBillSyncSupport.bfiCurrency(contract);
|
||||
// 申请日期:yyyy-MM-dd HH:mm:ss(与 A102-2/A103-1 统一,金蝶各接口日期格式要求一致)
|
||||
String applyDateText = bfiDateTimeText(entity);
|
||||
BigDecimal applyAmount = money(entity.getAppliedAmount()).setScale(2, RoundingMode.HALF_UP);
|
||||
@@ -1004,9 +872,9 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl<PaymentApplic
|
||||
|
||||
BfiPayApplySaveEntryDTO entry = new BfiPayApplySaveEntryDTO();
|
||||
entry.setEPaymenttypeNumber(TransportBusiConstant.PAYMENT_TYPE_SETTLEMENT.equals(entity.getPaymentType()) ? KingdeeBfiConstant.BFI_PAYMENT_TYPE_FORMAL : KingdeeBfiConstant.BFI_PAYMENT_TYPE_PREPAY);
|
||||
entry.setEAsstacttype(bfiAsstactType(contract));
|
||||
entry.setEAsstacttype(kingdeeApBillSyncSupport.bfiAsstactType(contract));
|
||||
// 往来户.编码:合同乙方关联客商档案客商编码
|
||||
entry.setEAsstactNumber(bfiAsstactNumber(contract, context));
|
||||
entry.setEAsstactNumber(kingdeeApBillSyncSupport.bfiAsstactNumber(contract, context));
|
||||
entry.setEAssacct(entity.getBankAccount());
|
||||
entry.setEBebankNumber(entity.getBankName());
|
||||
// 应付金额
|
||||
@@ -1020,7 +888,7 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl<PaymentApplic
|
||||
param.setEntry(List.of(entry));
|
||||
|
||||
BfiBillSaveResultVO saveResult = bfiService.savePrePayApply(param);
|
||||
return requireBfiBillId(saveResult, "推送金蝶付款申请");
|
||||
return kingdeeApBillSyncSupport.requireBfiBillId(saveResult, "推送金蝶付款申请");
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1041,182 +909,22 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl<PaymentApplic
|
||||
}
|
||||
|
||||
/**
|
||||
* 推送金蝶财务应付单(A102-2 财务应付单保存,正式结算付款第一步)。
|
||||
* 推送前先经 A102-4 按防重键(awrp_src_sys + awrp_src_code)查询:金蝶已存在财务应付单时
|
||||
* 幂等跳过保存并返回已有单据编号(说明上次同步已在后续下推/落库环节中断,应续推而非拦截报错),
|
||||
* 由调用方先经 A103-2 预检付款申请单是否已下推成功,再决定继续下推或直接回填。
|
||||
* 明细分录逐费用行生成(不聚合),含税金额取应付货物费用行调整后总金额、不含税取结算货物费用行不含税;
|
||||
* 付款计划 planentity 暂不传(启用方案见方法内注释,取数方法 approvedSettlementPayments 已保留),
|
||||
* 当前由金蝶按明细自动汇总生成
|
||||
*
|
||||
* @param entity 付款申请实体
|
||||
* @param entryGroups 结算明细解析出的应付单分录
|
||||
* @param documentNos 结算明细单据号(去重),用于拼接外部系统来源单据号
|
||||
* @return 财务应付单推送结果(单据编号 + 是否金蝶已存在幂等跳过 + 付款计划勾稽标识集合)
|
||||
*/
|
||||
private BfiApBillPushOutcome pushBfiApBill(PaymentApplication entity, List<BfiEntryGroup> entryGroups, List<String> documentNos, KingdeeSyncContext context) {
|
||||
// A102-4 幂等预检:按防重键查询金蝶是否已生成财务应付单(批量同步时整批已预热缓存,此处仅懒加载未命中编码),
|
||||
// 已存在则幂等跳过保存,由调用方经 A103-2 预检后续推付款申请(A103-1)或直接回填已有付款申请单
|
||||
String apBillSrcCode = ExternalSrcCodeSupport.buildSrcCode(entity.getPaymentNo(), documentNos);
|
||||
String existBillNo = context.apBillNoCache().computeIfAbsent(apBillSrcCode, code ->
|
||||
queryApBillExistNos(List.of(code)).getOrDefault(code, ""));
|
||||
if (Func.isNotEmpty(existBillNo)) {
|
||||
log.info("付款申请[{}]金蝶财务应付单已生成(单据编号[{}]),幂等跳过 A102-2 保存,继续后续下推流程", entity.getPaymentNo(), existBillNo);
|
||||
return new BfiApBillPushOutcome(existBillNo, true, List.of());
|
||||
}
|
||||
ContractManage contract = requireContract(entity, 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)) {
|
||||
throw new ServiceException("推送金蝶财务应付单失败:单据类型[" + KingdeeBfiConstant.BFI_AP_BILL_BILLTYPE_NAME + "]未在映射字典中维护");
|
||||
}
|
||||
String orgNumber = bfiOrgNumber(contract);
|
||||
// 币种取合同结算币种
|
||||
String currency = bfiCurrency(contract);
|
||||
// 单据日期:yyyy-MM-dd HH:mm:ss(各接口统一格式)
|
||||
String billDateText = bfiDateTimeText(entity);
|
||||
BfiApBillSaveDTO param = new BfiApBillSaveDTO();
|
||||
// 【必填】单据类型.编码
|
||||
param.setBilltypeidNumber(billtypeNumber);
|
||||
// 【必填】结算组织.编码
|
||||
param.setOrgNumber(orgNumber);
|
||||
// 【必填】单据日期
|
||||
param.setBizdate(billDateText);
|
||||
// 【必填】往来类型:合同类别为“客户合同”时为 bd_customer(客户),否则 bd_supplier(供应商)
|
||||
param.setAsstacttype(bfiAsstactType(contract));
|
||||
// 【必填】往来户.编码:合同乙方关联客商档案客商编码
|
||||
param.setAsstactNumber(bfiAsstactNumber(contract, context));
|
||||
// 【必填】付款方式(CASH:现购, CREDIT:赊购)
|
||||
param.setPurmode(KingdeeBfiConstant.BFI_AP_BILL_PUR_MODE_CASH);
|
||||
// 【必填】结算币别.货币代码
|
||||
param.setCurrencyNumber(currency);
|
||||
// 【必填】付款组织.编码
|
||||
param.setPayorgNumber(orgNumber);
|
||||
// 采购组织.编码(非必传)
|
||||
// param.setPurorgNumber(orgNumber);
|
||||
// 【必填】款项性质.编码
|
||||
param.setPaypropertyNumber(KingdeeBfiConstant.BFI_AP_BILL_PAY_PROPERTY);
|
||||
// TODO 待确认:结算方式编码(settlementtype_number)与税率编码(taxrateid_number)需金蝶侧确认后补充,当前不传
|
||||
// 录入含税价(非必传)
|
||||
param.setIsincludetax(true);
|
||||
// 录入总价,为 true 后可不录入单价(非必传)
|
||||
param.setIspricetotal(true);
|
||||
param.setIsintax(false);
|
||||
// 汇率表.编码(非必传)
|
||||
// param.setExratetableNumber(bfiPayApplyExratetable);
|
||||
// 汇率日期(非必传)
|
||||
// param.setExratedate(billDateText);
|
||||
// 往来单位名称(非必传)
|
||||
// param.setAsstactname(entity.getPayeeName());
|
||||
// 备注(非必传)
|
||||
// param.setRemark(entity.getPaymentNo() + (Func.isEmpty(entity.getSettlementNo()) ? "" : " " + entity.getSettlementNo()));
|
||||
// 【必填】外部系统编号(候选键)
|
||||
param.setAwrpSrcSys(bfiPayApplySrcSys);
|
||||
// 【必填】外部系统来源单据号(候选键):付款申请号_结算明细单据号1_单据号2...(复用预检已组装的防重键)
|
||||
param.setAwrpSrcCode(apBillSrcCode);
|
||||
// 来源单据(非必传,有上游合同时传 awrp_contract_info)
|
||||
// param.setAwrpSrcBt(KingdeeBfiConstant.BFI_CONTRACT_SRC_BT);
|
||||
// 上游外部系统单据号/上游合同号(非必传)
|
||||
// if (Func.isNotEmpty(entity.getContractNo())) {
|
||||
// param.setAwrpSrcConNo(entity.getContractNo());
|
||||
// }
|
||||
// 【必填】外部单据类型
|
||||
param.setAwrpSrcType(KingdeeBfiConstant.BFI_AP_BILL_SRC_TYPE);
|
||||
|
||||
// 明细分录:每条应付货物费用行一条(不聚合),无吨位时数量兜底 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);
|
||||
// 明细.单价:该行运输单价(费用明细 unit_price;单据头 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(entity.getRemark());
|
||||
detailentry.add(entry);
|
||||
}
|
||||
param.setDetailentry(detailentry);
|
||||
|
||||
// 付款计划 planentity 暂不传(联调待确认后启用):启用后送正式结算单下绑定、单据状态为审核通过(approved)
|
||||
// 的付款信息,每条付款信息一行,awrp_entry_code_p 取付款单号(A103-1 下推 awrp_finapbill_entryid 勾稽此值,
|
||||
// 取数见 approvedSettlementPayments);当前不传由金蝶按明细自动汇总生成,A103-1 推送前经 A102-4 反查 awrp_entry_code_p
|
||||
// List<FormalSettlementPayment> approvedPayments = approvedSettlementPayments(entity, context);
|
||||
// if (Func.isNotEmpty(approvedPayments)) {
|
||||
// List<BfiApBillSavePlanDTO> planentity = new ArrayList<>();
|
||||
// for (FormalSettlementPayment payment : approvedPayments) {
|
||||
// BfiApBillSavePlanDTO plan = new BfiApBillSavePlanDTO();
|
||||
// // 外部系统来源分录标识(候选键):付款单号
|
||||
// plan.setAwrpEntryCodeP(payment.getPaymentNo());
|
||||
// // 应付金额:付款信息申请付款金额
|
||||
// plan.setPlanpricetax(money(payment.getAppliedAmount()));
|
||||
// // 【必填】项目.项目编码(联调期临时传金蝶测试档案值)
|
||||
// plan.setPlanprojectNumber(KingdeeBfiConstant.BFI_AP_BILL_PROJECT_NUMBER_TEST);
|
||||
// // 到期日:付款信息创建日期(yyyy-MM-dd)
|
||||
// plan.setPlanduedate(payment.getCreateTime() == null ? null
|
||||
// : payment.getCreateTime().toInstant().atZone(ZoneId.systemDefault()).toLocalDate().toString());
|
||||
// planentity.add(plan);
|
||||
// planEntryCodes.add(payment.getPaymentNo());
|
||||
// }
|
||||
// param.setPlanentity(planentity);
|
||||
// }
|
||||
// 付款计划未传时勾稽标识集合保持为空,A103-1 推送前经 A102-4 反查金蝶生成的 awrp_entry_code_p
|
||||
List<String> planEntryCodes = new ArrayList<>();
|
||||
// TODO 待业务确认:存在预付款抵扣时是否需传预付信息 preentry(y_billtype/y_billid/y_settleamt),本期不传、后续迭代
|
||||
|
||||
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, planEntryCodes);
|
||||
}
|
||||
|
||||
/**
|
||||
* 推送金蝶付款申请(A103-1 应付下推付款申请,正式结算付款第二步),返回星瀚付款申请单id。
|
||||
* 推送金蝶付款申请(A103-1 应付下推付款申请,正式结算付款),返回星瀚付款申请单id。
|
||||
* 付款申请分录与财务应付单明细分录一一对应,申请金额取各组含税金额;
|
||||
* awrp_finapbill_entryid 勾稽 A102-2 付款计划分录 awrp_entry_code_p:付款计划暂不传,统一经 A102-4 反查
|
||||
* 金蝶自动生成的值;付款计划启用后优先取审核通过的付款信息单据号。awrp_entry_code 填费用明细行id
|
||||
* awrp_finapbill_entryid 勾稽 A102-2 付款计划分录 awrp_entry_code_p:财务应付单(A102-2)由正式结算单
|
||||
* 「同步金蝶」推送且未传付款计划(planentity),统一经 A102-4 按各分录所属正式结算单的防重键反查
|
||||
* 金蝶自动生成的 awrp_entry_code_p(多结算单付款申请各分录勾稽各自应付单);awrp_entry_code 填费用明细行id
|
||||
*
|
||||
* @param entity 付款申请实体
|
||||
* @param entryGroups 结算明细解析出的应付单分录
|
||||
* @param planEntryCodes A102-2 付款计划分录 awrp_entry_code_p 集合(付款信息单据号),空表示未传付款计划
|
||||
* @param entryGroups 结算明细解析出的应付单分录(含分录所属正式结算单id)
|
||||
* @param context 金蝶同步上下文(缓存载体)
|
||||
* @return 星瀚付款申请单id
|
||||
*/
|
||||
private String pushBfiPayApplyPush(PaymentApplication entity, List<BfiEntryGroup> entryGroups, List<String> planEntryCodes, KingdeeSyncContext context) {
|
||||
ContractManage contract = requireContract(entity, context);
|
||||
String orgNumber = bfiOrgNumber(contract);
|
||||
private String pushBfiPayApplyPush(PaymentApplication entity, List<KingdeeApBillSyncSupport.BfiEntryGroup> entryGroups, KingdeeSyncContext context) {
|
||||
ContractManage contract = kingdeeApBillSyncSupport.requireContract(entity.getContractId(), context);
|
||||
String orgNumber = kingdeeApBillSyncSupport.bfiOrgNumber(contract);
|
||||
// 申请日期:yyyy-MM-dd HH:mm:ss(与 A102-2 单据日期格式统一)
|
||||
String applyDateText = bfiDateTimeText(entity);
|
||||
// 【必填】结算单分录id勾稽值(awrp_finapbill_entryid):优先取审核通过的付款信息单据号(多条下划线拼接),
|
||||
// 无审核通过付款信息时经 A102-4 反查金蝶自动生成的付款计划 awrp_entry_code_p
|
||||
String finapbillEntryid;
|
||||
if (Func.isNotEmpty(planEntryCodes)) {
|
||||
finapbillEntryid = planEntryCodes.size() == 1 ? planEntryCodes.get(0) : String.join("_", planEntryCodes);
|
||||
} else {
|
||||
finapbillEntryid = apBillPlanEntryCode(entity, apBillSrcCode(entity, context), context);
|
||||
}
|
||||
BfiPayApplyPushDTO param = new BfiPayApplyPushDTO();
|
||||
// 【必填】外部系统来源单据号(候选键):付款申请号
|
||||
param.setAwrpSrcCode(entity.getPaymentNo());
|
||||
@@ -1241,8 +949,11 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl<PaymentApplic
|
||||
|
||||
// 付款申请分录与财务应付单明细分录一一对应(awrp_finapbill_entryid 关联应付单付款计划分录 awrp_entry_code_p,A102-4 反查)
|
||||
List<BfiPayApplyPushEntryDTO> entries = new ArrayList<>();
|
||||
for (int index = 0; index < entryGroups.size(); index++) {
|
||||
BfiEntryGroup entryGroup = entryGroups.get(index);
|
||||
for (KingdeeApBillSyncSupport.BfiEntryGroup entryGroup : entryGroups) {
|
||||
// 【必填】结算单分录id勾稽值(awrp_finapbill_entryid):按分录所属正式结算单的财务应付单,
|
||||
// 经 A102-4 反查其付款计划分录 awrp_entry_code_p(按防重键缓存,同一结算单分录只反查一次)
|
||||
String finapbillEntryid = kingdeeApBillSyncSupport.apBillPlanEntryCode(
|
||||
settlementApBillSrcCode(entity, entryGroup.formalSettlementId(), context), entity.getPaymentNo(), context);
|
||||
BfiPayApplyPushEntryDTO entry = new BfiPayApplyPushEntryDTO();
|
||||
// 【必填】付款类型.编码:正式付款固定 201
|
||||
entry.setEPaymenttypeNumber(KingdeeBfiConstant.BFI_PAYMENT_TYPE_FORMAL);
|
||||
@@ -1269,99 +980,7 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl<PaymentApplic
|
||||
param.setEntry(entries);
|
||||
|
||||
BfiBillSaveResultVO saveResult = bfiService.pushPayApply(param);
|
||||
return requireBfiBillId(saveResult, "推送金蝶付款申请(应付下推)");
|
||||
}
|
||||
|
||||
/**
|
||||
* 财务应付单/付款申请分录:每条来源费用行一条(不聚合)
|
||||
*
|
||||
* @param materialNumber 物料编码(货物类型档案货物编码)
|
||||
* @param measureunitNumber 计量单位编码(运费计算单位,档案暂无编码字段暂传单位名称)
|
||||
* @param entryCode 分录唯一标识(awrp_entry_code):费用明细行id,同行多货物类型拆分时追加序号
|
||||
* @param quantity 该行运输数量
|
||||
* @param price 该行运输单价(费用明细 unit_price)
|
||||
* @param amount 该行金额(含税),对应明细应付金额 e_pricetaxtotal
|
||||
* @param amountNoTax 该行金额(不含税),对应明细金额 e_amount
|
||||
*/
|
||||
private record BfiEntryGroup(String materialNumber, String measureunitNumber, String entryCode,
|
||||
BigDecimal quantity, BigDecimal price, BigDecimal amount, BigDecimal amountNoTax) {
|
||||
}
|
||||
|
||||
/**
|
||||
* 应付单分录解析结果
|
||||
*
|
||||
* @param entryGroups 结算明细解析出的应付单分录(不聚合,含税取应付货物费用行调整后总金额、不含税经结算费用行回链)
|
||||
* @param documentNos 结算明细单据号(去重、保持行序),用于拼接外部系统来源单据号
|
||||
*/
|
||||
private record BfiEntryGroupResult(List<BfiEntryGroup> entryGroups, List<String> documentNos) {
|
||||
}
|
||||
|
||||
/**
|
||||
* 财务应付单(A102-2)推送结果
|
||||
*
|
||||
* @param billNo 财务应付单单据编号(金蝶已存在时返回已有单据编号)
|
||||
* @param alreadyExist 金蝶是否已存在该防重键的财务应付单,true 表示本次幂等跳过保存(上次同步已在后续环节中断)
|
||||
* @param planEntryCodes 付款计划分录外部系统来源分录标识(awrp_entry_code_p,取审核通过的付款信息单据号),
|
||||
* 空表示 A102-2 未传付款计划(A103-1 推送前需经 A102-4 反查)
|
||||
*/
|
||||
private record BfiApBillPushOutcome(String billNo, boolean alreadyExist, List<String> planEntryCodes) {
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析付款申请关联的正式结算明细,逐行生成应付单分录(不聚合,每条来源费用行一条分录)。
|
||||
* <p>取数来源:应付明细货物费用行(blade_receivable_payable_cargo_fee,经结算明细 sourceDetailId 关联应付明细,
|
||||
* 每货物一条)——货物类型/计量单位/运输数量/含税金额直接取应付费用行(含税金额取调整后总金额 after_amount);
|
||||
* 应付费用行无不含税字段,不含税金额经结算货物费用行 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>
|
||||
*
|
||||
* @param entity 付款申请实体
|
||||
* @return 应付单分录解析结果(分录 + 结算明细单据号)
|
||||
*/
|
||||
private BfiEntryGroupResult resolveBfiEntryGroups(PaymentApplication entity, KingdeeSyncContext context) {
|
||||
List<FormalSettlementDetail> settlementDetails = settlementDetails(entity, context);
|
||||
if (Func.isEmpty(settlementDetails)) {
|
||||
throw new ServiceException("推送金蝶财务应付单失败:正式结算单无结算明细,无法解析物料与计量单位");
|
||||
}
|
||||
// 去重收集结算明细单据号(保持行序),用于拼接财务应付单外部系统来源单据号
|
||||
List<String> documentNos = settlementDocumentNos(settlementDetails);
|
||||
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<>();
|
||||
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) {
|
||||
appendEntryGroups(entryGroups, cargoCodeByName, payableFee.getCargoType(),
|
||||
resolveMeasureunitNumber(payableFee.getPriceUnit(), payableFee.getQuantityUnit()),
|
||||
payableFee.getId(), money(payableFee.getUnitPrice()),
|
||||
money(payableFee.getAfterAmount()),
|
||||
noTaxByPayableFeeId.getOrDefault(payableFee.getId(), BigDecimal.ZERO),
|
||||
money(payableFee.getTransportQuantity()),
|
||||
"应付货物费用[" + Func.toStr(payableFee.getCargoName()) + " 第" + Func.toStr(payableFee.getLineNo()) + "行]");
|
||||
}
|
||||
}
|
||||
return new BfiEntryGroupResult(entryGroups, documentNos);
|
||||
}
|
||||
|
||||
/** 结算明细单据号去重收集(保持行序),用于拼接财务应付单外部系统来源单据号 */
|
||||
private List<String> settlementDocumentNos(List<FormalSettlementDetail> settlementDetails) {
|
||||
return settlementDetails.stream()
|
||||
.map(FormalSettlementDetail::getDocumentNo)
|
||||
.filter(Func::isNotBlank)
|
||||
.map(String::trim)
|
||||
.distinct()
|
||||
.toList();
|
||||
return kingdeeApBillSyncSupport.requireBfiBillId(saveResult, "推送金蝶付款申请(应付下推)");
|
||||
}
|
||||
|
||||
/** 结算明细(上下文缓存优先):批量预检加载过的明细推送时直接复用,消除逐单重复查询 */
|
||||
@@ -1369,15 +988,48 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl<PaymentApplic
|
||||
return context.settlementDetailCache().computeIfAbsent(entity.getId(), key -> formalSettlementDetails(entity));
|
||||
}
|
||||
|
||||
/** 财务应付单防重键来源单据号:付款申请号 + 结算明细单据号下划线拼接(与 A102-2 保存传参一致) */
|
||||
private String apBillSrcCode(PaymentApplication entity, KingdeeSyncContext context) {
|
||||
return ExternalSrcCodeSupport.buildSrcCode(entity.getPaymentNo(), settlementDocumentNos(settlementDetails(entity, context)));
|
||||
/**
|
||||
* 付款申请关联各正式结算单的财务应付单防重键集合(结算单id -> awrp_src_code),
|
||||
* 防重键规则:正式结算单号_结算明细单据号下划线拼接(与正式结算单「同步金蝶」推送 A102-2 传参一致)
|
||||
*
|
||||
* @param entity 付款申请实体
|
||||
* @param context 金蝶同步上下文(缓存载体)
|
||||
* @return 结算单id -> 财务应付单防重键
|
||||
*/
|
||||
private Map<Long, String> settlementApBillSrcCodes(PaymentApplication entity, KingdeeSyncContext context) {
|
||||
List<Long> settlementIds = settlementDetails(entity, context).stream()
|
||||
.map(FormalSettlementDetail::getFormalSettlementId).filter(Objects::nonNull).distinct().toList();
|
||||
Map<Long, String> srcCodes = new LinkedHashMap<>();
|
||||
for (Long settlementId : settlementIds) {
|
||||
srcCodes.put(settlementId, settlementApBillSrcCode(entity, settlementId, context));
|
||||
}
|
||||
return srcCodes;
|
||||
}
|
||||
|
||||
/**
|
||||
* 单张正式结算单的财务应付单防重键:正式结算单号_结算明细单据号下划线拼接,
|
||||
* 结算单号经上下文缓存复用,结算明细取付款申请关联明细按结算单过滤
|
||||
*
|
||||
* @param entity 付款申请实体
|
||||
* @param formalSettlementId 正式结算单id
|
||||
* @param context 金蝶同步上下文(缓存载体)
|
||||
* @return 财务应付单防重键
|
||||
*/
|
||||
private String settlementApBillSrcCode(PaymentApplication entity, Long formalSettlementId, KingdeeSyncContext context) {
|
||||
FormalSettlement settlement = context.settlementCache().computeIfAbsent(formalSettlementId, id -> {
|
||||
FormalSettlement found = formalSettlementMapper.selectById(id);
|
||||
if (found == null) throw new ServiceException("推送金蝶付款申请失败:正式结算单[" + id + "]不存在");
|
||||
return found;
|
||||
});
|
||||
List<FormalSettlementDetail> ownDetails = settlementDetails(entity, context).stream()
|
||||
.filter(detail -> Objects.equals(formalSettlementId, detail.getFormalSettlementId())).toList();
|
||||
return kingdeeApBillSyncSupport.apBillSrcCode(settlement, ownDetails);
|
||||
}
|
||||
|
||||
/**
|
||||
* 金蝶分录标识:付款申请号-分录序号(1 起)。
|
||||
* 现仅 A103-3 预付款直接保存路径使用(序号固定 1);正式结算链路 A102-2 明细 awrp_entry_code 与
|
||||
* A103-1 awrp_entry_code 已改为费用明细行id(resolveBfiEntryGroups 统一解析),
|
||||
* A103-1 awrp_entry_code 已改为费用明细行id(KingdeeApBillSyncSupport.resolveEntryGroups 统一解析),
|
||||
* A103-1 awrp_finapbill_entryid 改为财务应付单付款计划 awrp_entry_code_p(A102-4 反查),禁止各接口自行拼接
|
||||
*
|
||||
* @param paymentNo 付款申请号
|
||||
@@ -1389,9 +1041,9 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl<PaymentApplic
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量同步前 A102-4 预检:一次批量查询整批付款申请的财务应付单是否已生成,
|
||||
* 结果(含"未生成"标记)写入上下文缓存,供逐单推送时复用,消除逐单远程查询(N+1)。
|
||||
* 仅预检会真正走到推送的正式结算单据(审批通过且未生成/生成失败),已生成/付款中等状态在
|
||||
* 批量同步前 A102-4 预检:一次批量查询整批付款申请关联正式结算单的财务应付单是否已生成,
|
||||
* 结果(含"未生成"标记)写入上下文缓存,供逐单前置校验与 A103-1 反查复用,消除逐单远程查询(N+1)。
|
||||
* 仅正式结算付款会走到前置校验(审批通过且未生成/生成失败),已生成/付款中等状态在
|
||||
* doSyncKingdee 入口即返回,无需预检
|
||||
*
|
||||
* @param ids 待同步付款申请id批次
|
||||
@@ -1409,7 +1061,7 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl<PaymentApplic
|
||||
if (!TransportBusiConstant.PAYMENT_TYPE_SETTLEMENT.equals(entity.getPaymentType())) {
|
||||
continue;
|
||||
}
|
||||
srcCodes.add(apBillSrcCode(entity, context));
|
||||
srcCodes.addAll(settlementApBillSrcCodes(entity, context).values());
|
||||
}
|
||||
// 仅查询缓存未命中的编码,已命中的(同批重复/已预热)直接复用
|
||||
List<String> pendingCodes = srcCodes.stream()
|
||||
@@ -1420,112 +1072,11 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl<PaymentApplic
|
||||
if (pendingCodes.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
context.apBillNoCache().putAll(queryApBillExistNos(pendingCodes));
|
||||
// 未命中编码补"未生成"标记(空串),避免逐单推送时重复远程查询
|
||||
context.apBillNoCache().putAll(kingdeeApBillSyncSupport.queryApBillExistNos(pendingCodes));
|
||||
// 未命中编码补"未生成"标记(空串),避免逐单前置校验时重复远程查询
|
||||
pendingCodes.forEach(code -> context.apBillNoCache().putIfAbsent(code, ""));
|
||||
}
|
||||
|
||||
/**
|
||||
* 单条应付货物费用行生成不聚合分录(每行一条);
|
||||
* 一行含多种货物类型时按类型拆为多条,金额与数量按份数均分(尾差计入最后一份)
|
||||
*
|
||||
* @param entryGroups 分录集合(保持行序)
|
||||
* @param cargoCodeByName 货物类型名称->货物编码映射
|
||||
* @param cargoType 货物类型(支持“、”分隔多类型)
|
||||
* @param measureunitNumber 计量单位编码(运费计算单位,档案暂无编码字段暂传单位名称)
|
||||
* @param sourceRowId 应付货物费用行id,雪花id天然唯一
|
||||
* @param price 该行运输单价(费用明细 unit_price)
|
||||
* @param amount 该行金额(含税)
|
||||
* @param amountNoTax 该行金额(不含税)
|
||||
* @param quantity 该行运输数量
|
||||
* @param sourceDesc 来源行描述,用于异常提示
|
||||
*/
|
||||
private void appendEntryGroups(List<BfiEntryGroup> entryGroups, Map<String, String> cargoCodeByName,
|
||||
String cargoType, String measureunitNumber, Long sourceRowId, BigDecimal price,
|
||||
BigDecimal amount, BigDecimal amountNoTax, BigDecimal quantity, String sourceDesc) {
|
||||
List<String> cargoNames = splitCargoTypes(cargoType);
|
||||
if (Func.isBlank(measureunitNumber)) {
|
||||
throw new ServiceException("推送金蝶财务应付单失败:" + sourceDesc + "计量单位为空,请先维护运费计算单位");
|
||||
}
|
||||
// 分录唯一标识(awrp_entry_code,A103-1 要求对方系统唯一行id不能重复):雪花id天然唯一,同行多类型拆分时追加序号
|
||||
String rowEntryCode = String.valueOf(sourceRowId);
|
||||
List<BigDecimal> amountShares = splitEqually(amount, cargoNames.size());
|
||||
List<BigDecimal> amountNoTaxShares = splitEqually(amountNoTax, 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 + "]未在货物类型档案中维护编码,请补充后重试");
|
||||
}
|
||||
String entryCode = cargoNames.size() > 1 ? rowEntryCode + "-" + (index + 1) : rowEntryCode;
|
||||
entryGroups.add(new BfiEntryGroup(materialNumber, measureunitNumber, entryCode,
|
||||
quantityShares.get(index), price, amountShares.get(index), amountNoTaxShares.get(index)));
|
||||
log.info("金蝶应付单分录生成:来源[{}],分录标识[{}],物料编码[{}],计量单位[{}],数量[{}],单价[{}],含税金额[{}],不含税金额[{}]",
|
||||
sourceDesc, entryCode, materialNumber, measureunitNumber,
|
||||
quantityShares.get(index), price, amountShares.get(index), amountNoTaxShares.get(index));
|
||||
}
|
||||
}
|
||||
|
||||
/** 付款申请关联的正式结算货物费用行(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));
|
||||
}
|
||||
|
||||
/** 应付明细id->应付货物费用行(blade_receivable_payable_cargo_fee,经结算明细 sourceDetailId 关联,每货物一条,分录主取数来源) */
|
||||
private 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();
|
||||
}
|
||||
return receivablePayableCargoFeeMapper.selectList(Wrappers.<ReceivablePayableCargoFee>lambdaQuery()
|
||||
.in(ReceivablePayableCargoFee::getDetailId, sourceDetailIds)
|
||||
.eq(ReceivablePayableCargoFee::getIsDeleted, 0)
|
||||
.orderByAsc(ReceivablePayableCargoFee::getLineNo))
|
||||
.stream().collect(Collectors.groupingBy(ReceivablePayableCargoFee::getDetailId));
|
||||
}
|
||||
|
||||
/**
|
||||
* 应付货物费用行id->结算金额(不含税):应付费用行无不含税字段,经结算货物费用行 sourceFeeId 回链获取。
|
||||
* 直接选应收应付明细路径 sourceFeeId=应付费用行id;预结算合并路径 sourceFeeId=预结算费用行id,
|
||||
* 经预结算费用行 sourceFeeId(指向应付费用行)回溯;无对应结算费用行时无映射,调用方兜底 0
|
||||
*/
|
||||
private 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;
|
||||
}
|
||||
|
||||
/** 付款申请关联的正式结算明细(关联表优先,兼容仅有 settlementId 的存量数据) */
|
||||
private List<FormalSettlementDetail> formalSettlementDetails(PaymentApplication entity) {
|
||||
List<Long> settlementIds = settlementRelationMapper.selectList(Wrappers.<PaymentApplicationSettlement>lambdaQuery()
|
||||
@@ -1544,134 +1095,6 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl<PaymentApplic
|
||||
.orderByAsc(FormalSettlementDetail::getLineNo));
|
||||
}
|
||||
|
||||
/** 货物类型名称->货物编码映射(取自货物类型档案;名称重复且编码不一致时拦截;批量同步时全批共享一次加载结果) */
|
||||
private Map<String, String> cargoCodeByName(KingdeeSyncContext context) {
|
||||
Map<String, String> cache = context.cargoCodeByName();
|
||||
if (!cache.isEmpty()) {
|
||||
return cache;
|
||||
}
|
||||
R<List<CargoType>> response = sysClient.getCargoTypes();
|
||||
if (response == null || !response.isSuccess() || Func.isEmpty(response.getData())) {
|
||||
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())) {
|
||||
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 + "]在货物类型档案中存在多个编码,请检查档案数据");
|
||||
}
|
||||
}
|
||||
cache.putAll(loaded);
|
||||
return cache;
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析计量单位编码:优先取快照运费计算单位(快照表 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 明细抛出)
|
||||
*
|
||||
* @param saveResult 金蝶返回结果
|
||||
* @param action 业务动作描述,用于异常提示
|
||||
*/
|
||||
private void requireBfiSaveSuccess(BfiBillSaveResultVO saveResult, String action) {
|
||||
if (saveResult == null || Func.isEmpty(saveResult.getResult())) {
|
||||
throw new ServiceException(action + "失败,金蝶未返回单据结果");
|
||||
}
|
||||
BfiBillSaveResultItemVO item = saveResult.getResult().get(0);
|
||||
if (!Boolean.TRUE.equals(item.getBillStatus())) {
|
||||
String errorText = Func.isBlank(item.getErrorText()) ? "未知错误" : item.getErrorText();
|
||||
throw new ServiceException(action + "失败:" + errorText);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验金蝶保存/下推返回结果并取星瀚单据id(单据级失败以 errors 明细抛出)
|
||||
*
|
||||
* @param saveResult 金蝶返回结果
|
||||
* @param action 业务动作描述,用于异常提示
|
||||
* @return 星瀚单据id
|
||||
*/
|
||||
private String requireBfiBillId(BfiBillSaveResultVO saveResult, String action) {
|
||||
requireBfiSaveSuccess(saveResult, action);
|
||||
Object billId = saveResult.getResult().get(0).getId();
|
||||
if (billId == null) throw new ServiceException(action + "失败,金蝶未返回单据id");
|
||||
return String.valueOf(billId);
|
||||
}
|
||||
|
||||
private ContractManage requireContract(PaymentApplication entity, KingdeeSyncContext context) {
|
||||
if (entity.getContractId() == null) throw new ServiceException("推送金蝶付款申请失败,付款申请未关联合同");
|
||||
// 批量同步时按合同id缓存复用(同一合同只查一次),单笔同步退化为直接查询
|
||||
ContractManage contract = context.contractCache().computeIfAbsent(entity.getContractId(), contractManageMapper::selectById);
|
||||
if (contract == null || Objects.equals(contract.getIsDeleted(), 1)) throw new ServiceException("推送金蝶付款申请失败,关联合同不存在");
|
||||
return contract;
|
||||
}
|
||||
|
||||
/**
|
||||
* 合同所属组织编码
|
||||
* @param contract
|
||||
* @return
|
||||
*/
|
||||
private String bfiOrgNumber(ContractManage contract) {
|
||||
Dept dept = contract.getOrganizationId() == null ? null : SysCache.getDept(contract.getOrganizationId());
|
||||
// dept为空 或者 deptCode为空,直接报错
|
||||
if (dept == null || Func.isBlank(dept.getDeptCode())) {
|
||||
throw new ServiceException("推送金蝶付款申请失败,合同所属组织编码不能为空,请检查部门ID【"+contract.getOrganizationId()+"】对应的部门是否维护部门编码");
|
||||
}
|
||||
return dept.getDeptCode();
|
||||
}
|
||||
|
||||
private String bfiCurrency(ContractManage contract) {
|
||||
if (Func.isBlank(contract.getSettlementCurrency())) throw new ServiceException("推送金蝶付款申请失败,合同结算币种不能为空");
|
||||
// 金蝶币别代码为 CNY,合同结算币种维护为 RMB 时统一映射
|
||||
String currency = contract.getSettlementCurrency();
|
||||
return "RMB".equalsIgnoreCase(currency) ? "CNY" : currency;
|
||||
}
|
||||
|
||||
private String bfiApplyCause(String paymentType) {
|
||||
return switch (paymentType == null ? "" : paymentType) {
|
||||
case "project_advance" -> "项目预付款申请";
|
||||
@@ -1688,33 +1111,6 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl<PaymentApplic
|
||||
.map(DictBiz::getDictValue).findFirst().orElse(paymentMethod);
|
||||
}
|
||||
|
||||
/** 财务应付单往来类型:合同类别为“客户合同”时为 bd_customer(客户),否则 bd_supplier(供应商) */
|
||||
private String bfiAsstactType(ContractManage contract) {
|
||||
return "客户合同".equals(contract.getContractCategory())
|
||||
? KingdeeBfiConstant.BFI_CUSTOMER_ASSTACT_TYPE : KingdeeBfiConstant.BFI_SUPPLIER_ASSTACT_TYPE;
|
||||
}
|
||||
|
||||
/** 财务应付单往来户.编码:合同乙方关联客商档案取客商编码,未维护时拦截 */
|
||||
private String bfiAsstactNumber(ContractManage contract, KingdeeSyncContext context) {
|
||||
if (Func.isBlank(contract.getPartyB())) {
|
||||
throw new ServiceException("推送金蝶财务应付单失败,合同乙方不能为空,请检查合同[" + Func.toStr(contract.getContractNo()) + "]乙方信息");
|
||||
}
|
||||
// 批量同步时按乙方名称缓存客商编码(同批同一乙方只查一次),单笔同步退化为直接查询
|
||||
String customerCode = context.customerCodeCache().computeIfAbsent(contract.getPartyB(), this::queryCustomerCode);
|
||||
if (Func.isBlank(customerCode)) {
|
||||
throw new ServiceException("推送金蝶财务应付单失败,合同乙方[" + contract.getPartyB() + "]未在客商档案中维护客商编码,请补充后重试");
|
||||
}
|
||||
return customerCode;
|
||||
}
|
||||
|
||||
/** 按乙方名称(全称或简称)查询客商编码,未维护返回 null */
|
||||
private String queryCustomerCode(String partyB) {
|
||||
CustomerArchive customer = customerArchiveMapper.selectOne(Wrappers.<CustomerArchive>lambdaQuery()
|
||||
.and(wrapper -> wrapper.eq(CustomerArchive::getFullName, partyB).or().eq(CustomerArchive::getShortName, partyB))
|
||||
.eq(CustomerArchive::getIsDeleted, 0).last("limit 1"));
|
||||
return customer == null ? null : customer.getCustomerCode();
|
||||
}
|
||||
|
||||
private String bfiProjectNumber(PaymentApplication entity, KingdeeSyncContext context) {
|
||||
if (entity.getProjectId() == null) return KingdeeBfiConstant.BFI_PROJECT_NUMBER_NONE;
|
||||
// 批量同步时按项目id缓存复用(同一项目只查一次),单笔同步退化为直接查询
|
||||
|
||||
+708
@@ -0,0 +1,708 @@
|
||||
/**
|
||||
* BladeX Commercial License Agreement
|
||||
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
|
||||
* <p>
|
||||
* Use of this software is governed by the Commercial License Agreement
|
||||
* obtained after purchasing a license from BladeX.
|
||||
* <p>
|
||||
* 1. This software is for development use only under a valid license
|
||||
* from BladeX.
|
||||
* <p>
|
||||
* 2. Redistribution of this software's source code to any third party
|
||||
* without a commercial license is strictly prohibited.
|
||||
* <p>
|
||||
* 3. Licensees may copyright their own code but cannot use segments
|
||||
* from this software for such purposes. Copyright of this software
|
||||
* remains with BladeX.
|
||||
* <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>
|
||||
* Author: Chill Zhuang (bladejava@qq.com)
|
||||
*/
|
||||
package org.springblade.transport.support;
|
||||
|
||||
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
||||
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.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.thirdparty.bfi.pojo.dto.BfiApBillQueryDTO;
|
||||
import org.springblade.thirdparty.bfi.pojo.dto.BfiApBillSaveDTO;
|
||||
import org.springblade.thirdparty.bfi.pojo.dto.BfiApBillSaveEntryDTO;
|
||||
import org.springblade.thirdparty.bfi.pojo.vo.BfiApBillPlanVO;
|
||||
import org.springblade.thirdparty.bfi.pojo.vo.BfiApBillVO;
|
||||
import org.springblade.thirdparty.bfi.pojo.vo.BfiBillSaveResultItemVO;
|
||||
import org.springblade.thirdparty.bfi.pojo.vo.BfiBillSaveResultVO;
|
||||
import org.springblade.thirdparty.bfi.service.IBfiService;
|
||||
import org.springblade.thirdparty.bfi.util.BfiUtil;
|
||||
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.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.service.KingdeeSyncContext;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
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;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 金蝶财务应付单(A102-2)同步支撑组件。
|
||||
* <p>流程拆分后:财务应付单(A102-2)由正式结算单「同步金蝶」推送并回写结算单;
|
||||
* 付款申请「同步金蝶」仅负责付款申请单(A103-1 应付下推 / A103-3 直接保存),
|
||||
* 推送前经 A102-4 预检应付单已生成、下推时经 A102-4 反查付款计划分录 awrp_entry_code_p。
|
||||
* 两侧共用的防重键组装、A102-4 查询/反查、A102-2 保存、结算明细分录解析与档案取数统一收口在本组件。</p>
|
||||
*
|
||||
* @author Chill
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class KingdeeApBillSyncSupport {
|
||||
|
||||
/** A102-4 防重键批量分批查询单次条数上限(与 A103-2 批量查询一致) */
|
||||
private static final int PAY_RESULT_QUERY_BATCH = 50;
|
||||
/** A102-4 分页查询页大小 */
|
||||
private static final int PAY_RESULT_PAGE_SIZE = 200;
|
||||
|
||||
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 ISysClient sysClient;
|
||||
@Value("${thirdParty.bfi.payApply.srcSys:WLTMS}")
|
||||
private String bfiPayApplySrcSys;
|
||||
|
||||
// ==================== 财务应付单防重键(awrp_src_code) ====================
|
||||
|
||||
/**
|
||||
* 结算明细单据号去重收集(保持行序),用于拼接财务应付单外部系统来源单据号
|
||||
*
|
||||
* @param settlementDetails 结算明细列表
|
||||
* @return 单据号列表(去空白、去重、保持行序)
|
||||
*/
|
||||
public List<String> settlementDocumentNos(List<FormalSettlementDetail> settlementDetails) {
|
||||
return settlementDetails.stream()
|
||||
.map(FormalSettlementDetail::getDocumentNo)
|
||||
.filter(Func::isNotBlank)
|
||||
.map(String::trim)
|
||||
.distinct()
|
||||
.toList();
|
||||
}
|
||||
|
||||
/**
|
||||
* 财务应付单防重键(awrp_src_code):正式结算单号 + 结算明细单据号下划线拼接(与 A102-2 保存传参一致)。
|
||||
* 流程拆分后 A102-2 由正式结算单推送,防重键以正式结算单号为主,一张结算单只生成一张应付单,
|
||||
* 同一结算单下多笔付款申请(A103-1)共享该应付单
|
||||
*
|
||||
* @param settlement 正式结算单
|
||||
* @param settlementDetails 该结算单下的结算明细
|
||||
* @return 防重键
|
||||
*/
|
||||
public String apBillSrcCode(FormalSettlement settlement, List<FormalSettlementDetail> settlementDetails) {
|
||||
return ExternalSrcCodeSupport.buildSrcCode(settlement.getFormalSettlementNo(), settlementDocumentNos(settlementDetails));
|
||||
}
|
||||
|
||||
// ==================== A102-4 财务应付单查询 ====================
|
||||
|
||||
/**
|
||||
* A102-4 批量查询已生成财务应付单:来源单据号批量 + 外部系统编号(防重键),自动翻页,
|
||||
* 用于推送前幂等预检与应付单是否已生成前置校验(awrp_src_code 支持多值,整批一次查询消除逐单远程查询 N+1)
|
||||
*
|
||||
* @param awrpSrcCodes 来源单据号(正式结算单号_结算明细单据号拼接)批次
|
||||
* @return 已生成财务应付单的 来源单据号->单据编号 映射
|
||||
*/
|
||||
public Map<String, String> queryApBillExistNos(List<String> awrpSrcCodes) {
|
||||
Map<String, String> existBillNos = new LinkedHashMap<>();
|
||||
// 防重键批量分批查询,避免单次过滤条件过长
|
||||
batchForeach(awrpSrcCodes, PAY_RESULT_QUERY_BATCH, batch -> {
|
||||
BfiApBillQueryDTO param = new BfiApBillQueryDTO();
|
||||
param.setAwrpSrcCode(batch);
|
||||
param.setAwrpSrcSys(bfiPayApplySrcSys);
|
||||
for (BfiApBillVO apBill : BfiUtil.queryAllPages(pageNo -> {
|
||||
param.setPageNo(pageNo);
|
||||
param.setPageSize(PAY_RESULT_PAGE_SIZE);
|
||||
return bfiService.queryApBill(param);
|
||||
})) {
|
||||
if (Func.isNotEmpty(apBill.getAwrpSrcCode())) {
|
||||
existBillNos.put(apBill.getAwrpSrcCode(), apBill.getBillno());
|
||||
}
|
||||
}
|
||||
});
|
||||
return existBillNos;
|
||||
}
|
||||
|
||||
/**
|
||||
* A102-4 反查财务应付单付款计划分录的外部系统来源分录标识(awrp_entry_code_p)。
|
||||
* A102-2 保存未传 planentity,由金蝶按明细自动汇总生成(awrp_entry_code_p 不传时自动取外部系统单据号),
|
||||
* A103-1 应付下推的 awrp_finapbill_entryid 需填该值勾稽应付单;
|
||||
* 结果按防重键(awrp_src_code)缓存,恢复场景避免重复远程查询
|
||||
*
|
||||
* @param apBillSrcCode 财务应付单防重键(外部系统来源单据号)
|
||||
* @param subject 业务单据号(付款申请号,异常提示用)
|
||||
* @param context 金蝶同步上下文(缓存载体)
|
||||
* @return 付款计划分录 awrp_entry_code_p
|
||||
*/
|
||||
public String apBillPlanEntryCode(String apBillSrcCode, String subject, KingdeeSyncContext context) {
|
||||
return context.apBillPlanCodeCache().computeIfAbsent(apBillSrcCode, code -> {
|
||||
BfiApBillQueryDTO param = new BfiApBillQueryDTO();
|
||||
param.setAwrpSrcCode(List.of(code));
|
||||
param.setAwrpSrcSys(bfiPayApplySrcSys);
|
||||
List<BfiApBillVO> bills = BfiUtil.queryAllPages(pageNo -> {
|
||||
param.setPageNo(pageNo);
|
||||
param.setPageSize(PAY_RESULT_PAGE_SIZE);
|
||||
return bfiService.queryApBill(param);
|
||||
});
|
||||
for (BfiApBillVO apBill : bills) {
|
||||
if (Func.isNotEmpty(apBill.getPlanentity())) {
|
||||
for (BfiApBillPlanVO plan : apBill.getPlanentity()) {
|
||||
if (Func.isNotBlank(plan.getAwrpEntryCodeP())) {
|
||||
return plan.getAwrpEntryCodeP();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
throw new ServiceException("推送金蝶付款申请(应付下推)失败:财务应付单[" + subject + "]付款计划分录 awrp_entry_code_p 未查询到,无法勾稽关联");
|
||||
});
|
||||
}
|
||||
|
||||
// ==================== A102-2 财务应付单保存 ====================
|
||||
|
||||
/**
|
||||
* 推送金蝶财务应付单(A102-2 财务应付单保存,由正式结算单「同步金蝶」触发)。
|
||||
* 推送前先经 A102-4 按防重键(awrp_src_sys + awrp_src_code)查询:金蝶已存在财务应付单时
|
||||
* 幂等跳过保存并返回已有单据编号(说明上次同步已保存成功、在后续环节中断,应续推而非拦截报错)。
|
||||
* 明细分录逐费用行生成(不聚合),含税金额取应付货物费用行调整后总金额、不含税取结算货物费用行不含税;
|
||||
* 付款计划 planentity 暂不传(启用方案见方法内注释),当前由金蝶按明细自动汇总生成
|
||||
*
|
||||
* @param apBillSrcCode 财务应付单防重键(正式结算单号_结算明细单据号拼接)
|
||||
* @param contractId 正式结算单关联合同id
|
||||
* @param entryGroups 结算明细解析出的应付单分录
|
||||
* @param subject 业务单据号(正式结算单号,日志与异常提示用)
|
||||
* @param context 金蝶同步上下文(缓存载体)
|
||||
* @return 财务应付单推送结果(单据编号 + 是否金蝶已存在幂等跳过)
|
||||
*/
|
||||
public BfiApBillPushOutcome pushApBill(String apBillSrcCode, Long contractId, List<BfiEntryGroup> entryGroups,
|
||||
String subject, KingdeeSyncContext context) {
|
||||
// A102-4 幂等预检:按防重键查询金蝶是否已生成财务应付单(批量同步时整批已预热缓存,此处仅懒加载未命中编码),
|
||||
// 已存在则幂等跳过保存,直接返回已有单据编号
|
||||
String existBillNo = context.apBillNoCache().computeIfAbsent(apBillSrcCode, code ->
|
||||
queryApBillExistNos(List.of(code)).getOrDefault(code, ""));
|
||||
if (Func.isNotEmpty(existBillNo)) {
|
||||
log.info("正式结算单[{}]金蝶财务应付单已生成(单据编号[{}]),幂等跳过 A102-2 保存", subject, existBillNo);
|
||||
return new BfiApBillPushOutcome(existBillNo, true);
|
||||
}
|
||||
ContractManage contract = requireContract(contractId, 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)) {
|
||||
throw new ServiceException("推送金蝶财务应付单失败:单据类型[" + KingdeeBfiConstant.BFI_AP_BILL_BILLTYPE_NAME + "]未在映射字典中维护");
|
||||
}
|
||||
String orgNumber = bfiOrgNumber(contract);
|
||||
// 币种取合同结算币种
|
||||
String currency = bfiCurrency(contract);
|
||||
// 单据日期:yyyy-MM-dd HH:mm:ss(各接口统一格式),流程拆分后以同步当日作为应付单据日期
|
||||
String billDateText = bfiDateTimeText(LocalDate.now());
|
||||
BfiApBillSaveDTO param = new BfiApBillSaveDTO();
|
||||
// 【必填】单据类型.编码
|
||||
param.setBilltypeidNumber(billtypeNumber);
|
||||
// 【必填】结算组织.编码
|
||||
param.setOrgNumber(orgNumber);
|
||||
// 【必填】单据日期
|
||||
param.setBizdate(billDateText);
|
||||
// 【必填】往来类型:合同类别为“客户合同”时为 bd_customer(客户),否则 bd_supplier(供应商)
|
||||
param.setAsstacttype(bfiAsstactType(contract));
|
||||
// 【必填】往来户.编码:合同乙方关联客商档案客商编码
|
||||
param.setAsstactNumber(bfiAsstactNumber(contract, context));
|
||||
// 【必填】付款方式(CASH:现购, CREDIT:赊购)
|
||||
param.setPurmode(KingdeeBfiConstant.BFI_AP_BILL_PUR_MODE_CASH);
|
||||
// 【必填】结算币别.货币代码
|
||||
param.setCurrencyNumber(currency);
|
||||
// 【必填】付款组织.编码
|
||||
param.setPayorgNumber(orgNumber);
|
||||
// 采购组织.编码(非必传)
|
||||
// param.setPurorgNumber(orgNumber);
|
||||
// 【必填】款项性质.编码
|
||||
param.setPaypropertyNumber(KingdeeBfiConstant.BFI_AP_BILL_PAY_PROPERTY);
|
||||
// TODO 待确认:结算方式编码(settlementtype_number)与税率编码(taxrateid_number)需金蝶侧确认后补充,当前不传
|
||||
// 录入含税价(非必传)
|
||||
param.setIsincludetax(true);
|
||||
// 录入总价,为 true 后可不录入单价(非必传)
|
||||
param.setIspricetotal(true);
|
||||
param.setIsintax(false);
|
||||
// 汇率表.编码(非必传)
|
||||
// param.setExratetableNumber(bfiPayApplyExratetable);
|
||||
// 汇率日期(非必传)
|
||||
// param.setExratedate(billDateText);
|
||||
// 往来单位名称(非必传)
|
||||
// param.setAsstactname(entity.getPayeeName());
|
||||
// 备注(非必传)
|
||||
// param.setRemark(subject);
|
||||
// 【必填】外部系统编号(候选键)
|
||||
param.setAwrpSrcSys(bfiPayApplySrcSys);
|
||||
// 【必填】外部系统来源单据号(候选键):正式结算单号_结算明细单据号1_单据号2...(复用预检已组装的防重键)
|
||||
param.setAwrpSrcCode(apBillSrcCode);
|
||||
// 来源单据(非必传,有上游合同时传 awrp_contract_info)
|
||||
// param.setAwrpSrcBt(KingdeeBfiConstant.BFI_CONTRACT_SRC_BT);
|
||||
// 上游外部系统单据号/上游合同号(非必传)
|
||||
// if (Func.isNotEmpty(contract.getContractNo())) {
|
||||
// param.setAwrpSrcConNo(contract.getContractNo());
|
||||
// }
|
||||
// 【必填】外部单据类型
|
||||
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,档案暂无编码字段暂传单位名称)
|
||||
entry.setMeasureunitNumber(entryGroup.measureunitNumber());
|
||||
// 【必填】数量:该行运输数量,无吨位数据时兜底 1
|
||||
entry.setQuantity(entryGroup.quantity().compareTo(BigDecimal.ZERO) > 0 ? entryGroup.quantity() : BigDecimal.ONE);
|
||||
// 明细.单价:该行运输单价(费用明细 unit_price;单据头 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);
|
||||
detailentry.add(entry);
|
||||
}
|
||||
param.setDetailentry(detailentry);
|
||||
|
||||
// 付款计划 planentity 暂不传(联调待确认后启用):启用后送正式结算单下绑定、单据状态为审核通过(approved)
|
||||
// 的付款信息,每条付款信息一行,awrp_entry_code_p 取付款单号(A103-1 下推 awrp_finapbill_entryid 勾稽此值);
|
||||
// 当前不传由金蝶按明细自动汇总生成,A103-1 推送前经 A102-4 反查金蝶生成的 awrp_entry_code_p
|
||||
// TODO 待业务确认:存在预付款抵扣时是否需传预付信息 preentry(y_billtype/y_billid/y_settleamt),本期不传、后续迭代
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
// ==================== 结算明细分录解析 ====================
|
||||
|
||||
/**
|
||||
* 解析正式结算明细,逐行生成应付单分录(不聚合,每条来源费用行一条分录),分录携带所属正式结算单id
|
||||
* (多结算单付款申请 A103-1 下推时按结算单分组勾稽各自应付单付款计划分录)。
|
||||
* <p>取数来源:应付明细货物费用行(blade_receivable_payable_cargo_fee,经结算明细 sourceDetailId 关联应付明细,
|
||||
* 每货物一条)——货物类型/计量单位/运输数量/含税金额直接取应付费用行(含税金额取调整后总金额 after_amount);
|
||||
* 应付费用行无不含税字段,不含税金额经结算货物费用行 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>
|
||||
*
|
||||
* @param settlementDetails 正式结算明细列表
|
||||
* @param context 金蝶同步上下文(缓存载体)
|
||||
* @return 应付单分录列表(保持行序)
|
||||
*/
|
||||
public List<BfiEntryGroup> resolveEntryGroups(List<FormalSettlementDetail> settlementDetails, KingdeeSyncContext context) {
|
||||
if (Func.isEmpty(settlementDetails)) {
|
||||
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<>();
|
||||
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) {
|
||||
appendEntryGroups(entryGroups, cargoCodeByName, settlementDetail.getFormalSettlementId(),
|
||||
payableFee.getCargoType(),
|
||||
resolveMeasureunitNumber(payableFee.getPriceUnit(), payableFee.getQuantityUnit()),
|
||||
payableFee.getId(), money(payableFee.getUnitPrice()),
|
||||
money(payableFee.getAfterAmount()),
|
||||
noTaxByPayableFeeId.getOrDefault(payableFee.getId(), BigDecimal.ZERO),
|
||||
money(payableFee.getTransportQuantity()),
|
||||
"应付货物费用[" + Func.toStr(payableFee.getCargoName()) + " 第" + Func.toStr(payableFee.getLineNo()) + "行]");
|
||||
}
|
||||
}
|
||||
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 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) {
|
||||
List<String> cargoNames = splitCargoTypes(cargoType);
|
||||
if (Func.isBlank(measureunitNumber)) {
|
||||
throw new ServiceException("推送金蝶财务应付单失败:" + sourceDesc + "计量单位为空,请先维护运费计算单位");
|
||||
}
|
||||
// 分录唯一标识(awrp_entry_code,A103-1 要求对方系统唯一行id不能重复):雪花id天然唯一,同行多类型拆分时追加序号
|
||||
String rowEntryCode = String.valueOf(sourceRowId);
|
||||
List<BigDecimal> amountShares = splitEqually(amount, cargoNames.size());
|
||||
List<BigDecimal> amountNoTaxShares = splitEqually(amountNoTax, 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 + "]未在货物类型档案中维护编码,请补充后重试");
|
||||
}
|
||||
String entryCode = cargoNames.size() > 1 ? rowEntryCode + "-" + (index + 1) : rowEntryCode;
|
||||
entryGroups.add(new BfiEntryGroup(materialNumber, measureunitNumber, entryCode,
|
||||
quantityShares.get(index), price, amountShares.get(index), amountNoTaxShares.get(index), formalSettlementId));
|
||||
log.info("金蝶应付单分录生成:来源[{}],分录标识[{}],物料编码[{}],计量单位[{}],数量[{}],单价[{}],含税金额[{}],不含税金额[{}]",
|
||||
sourceDesc, entryCode, materialNumber, measureunitNumber,
|
||||
quantityShares.get(index), price, amountShares.get(index), amountNoTaxShares.get(index));
|
||||
}
|
||||
}
|
||||
|
||||
/** 付款申请关联的正式结算货物费用行(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));
|
||||
}
|
||||
|
||||
/** 应付明细id->应付货物费用行(blade_receivable_payable_cargo_fee,经结算明细 sourceDetailId 关联,每货物一条,分录主取数来源) */
|
||||
private 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();
|
||||
}
|
||||
return receivablePayableCargoFeeMapper.selectList(Wrappers.<ReceivablePayableCargoFee>lambdaQuery()
|
||||
.in(ReceivablePayableCargoFee::getDetailId, sourceDetailIds)
|
||||
.eq(ReceivablePayableCargoFee::getIsDeleted, 0)
|
||||
.orderByAsc(ReceivablePayableCargoFee::getLineNo))
|
||||
.stream().collect(Collectors.groupingBy(ReceivablePayableCargoFee::getDetailId));
|
||||
}
|
||||
|
||||
/**
|
||||
* 应付货物费用行id->结算金额(不含税):应付费用行无不含税字段,经结算货物费用行 sourceFeeId 回链获取。
|
||||
* 直接选应收应付明细路径 sourceFeeId=应付费用行id;预结算合并路径 sourceFeeId=预结算费用行id,
|
||||
* 经预结算费用行 sourceFeeId(指向应付费用行)回溯;无对应结算费用行时无映射,调用方兜底 0
|
||||
*/
|
||||
private 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 Map<String, String> cargoCodeByName(KingdeeSyncContext context) {
|
||||
Map<String, String> cache = context.cargoCodeByName();
|
||||
if (!cache.isEmpty()) {
|
||||
return cache;
|
||||
}
|
||||
R<List<CargoType>> response = sysClient.getCargoTypes();
|
||||
if (response == null || !response.isSuccess() || Func.isEmpty(response.getData())) {
|
||||
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())) {
|
||||
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 + "]在货物类型档案中存在多个编码,请检查档案数据");
|
||||
}
|
||||
}
|
||||
cache.putAll(loaded);
|
||||
return cache;
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析计量单位编码:优先取快照运费计算单位(快照表 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 明细抛出)
|
||||
*
|
||||
* @param saveResult 金蝶返回结果
|
||||
* @param action 业务动作描述,用于异常提示
|
||||
*/
|
||||
public void requireBfiSaveSuccess(BfiBillSaveResultVO saveResult, String action) {
|
||||
if (saveResult == null || Func.isEmpty(saveResult.getResult())) {
|
||||
throw new ServiceException(action + "失败,金蝶未返回单据结果");
|
||||
}
|
||||
BfiBillSaveResultItemVO item = saveResult.getResult().get(0);
|
||||
if (!Boolean.TRUE.equals(item.getBillStatus())) {
|
||||
String errorText = Func.isBlank(item.getErrorText()) ? "未知错误" : item.getErrorText();
|
||||
throw new ServiceException(action + "失败:" + errorText);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验金蝶保存/下推返回结果并取星瀚单据id(单据级失败以 errors 明细抛出)
|
||||
*
|
||||
* @param saveResult 金蝶返回结果
|
||||
* @param action 业务动作描述,用于异常提示
|
||||
* @return 星瀚单据id
|
||||
*/
|
||||
public String requireBfiBillId(BfiBillSaveResultVO saveResult, String action) {
|
||||
requireBfiSaveSuccess(saveResult, action);
|
||||
Object billId = saveResult.getResult().get(0).getId();
|
||||
if (billId == null) throw new ServiceException(action + "失败,金蝶未返回单据id");
|
||||
return String.valueOf(billId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验并获取单据关联合同(批量同步时按合同id缓存复用,单笔同步退化为直接查询)
|
||||
*
|
||||
* @param contractId 合同id
|
||||
* @param context 金蝶同步上下文(缓存载体)
|
||||
* @return 合同实体
|
||||
*/
|
||||
public ContractManage requireContract(Long contractId, KingdeeSyncContext context) {
|
||||
if (contractId == null) throw new ServiceException("推送金蝶失败,单据未关联合同");
|
||||
ContractManage contract = context.contractCache().computeIfAbsent(contractId, contractManageMapper::selectById);
|
||||
if (contract == null || Objects.equals(contract.getIsDeleted(), 1)) throw new ServiceException("推送金蝶失败,关联合同不存在");
|
||||
return contract;
|
||||
}
|
||||
|
||||
/**
|
||||
* 金蝶接口日期文本:yyyy-MM-dd HH:mm:ss(A102-2/A103-1/A103-3 各接口统一格式)
|
||||
*
|
||||
* @param date 业务日期
|
||||
* @return 日期文本
|
||||
*/
|
||||
public String bfiDateTimeText(LocalDate date) {
|
||||
LocalDate effectiveDate = date == null ? LocalDate.now() : date;
|
||||
return effectiveDate.atStartOfDay().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
|
||||
}
|
||||
|
||||
/**
|
||||
* 合同所属组织编码
|
||||
*
|
||||
* @param contract 合同实体
|
||||
* @return 组织编码
|
||||
*/
|
||||
public String bfiOrgNumber(ContractManage contract) {
|
||||
Dept dept = contract.getOrganizationId() == null ? null : SysCache.getDept(contract.getOrganizationId());
|
||||
// dept为空 或者 deptCode为空,直接报错
|
||||
if (dept == null || Func.isBlank(dept.getDeptCode())) {
|
||||
throw new ServiceException("推送金蝶失败,合同所属组织编码不能为空,请检查部门ID【" + contract.getOrganizationId() + "】对应的部门是否维护部门编码");
|
||||
}
|
||||
return dept.getDeptCode();
|
||||
}
|
||||
|
||||
/**
|
||||
* 合同结算币种转金蝶币别代码(RMB 统一映射 CNY)
|
||||
*
|
||||
* @param contract 合同实体
|
||||
* @return 币别代码
|
||||
*/
|
||||
public String bfiCurrency(ContractManage contract) {
|
||||
if (Func.isBlank(contract.getSettlementCurrency())) throw new ServiceException("推送金蝶失败,合同结算币种不能为空");
|
||||
String currency = contract.getSettlementCurrency();
|
||||
return "RMB".equalsIgnoreCase(currency) ? "CNY" : currency;
|
||||
}
|
||||
|
||||
/** 财务应付单往来类型:合同类别为“客户合同”时为 bd_customer(客户),否则 bd_supplier(供应商) */
|
||||
public String bfiAsstactType(ContractManage contract) {
|
||||
return "客户合同".equals(contract.getContractCategory())
|
||||
? KingdeeBfiConstant.BFI_CUSTOMER_ASSTACT_TYPE : KingdeeBfiConstant.BFI_SUPPLIER_ASSTACT_TYPE;
|
||||
}
|
||||
|
||||
/** 财务应付单往来户.编码:合同乙方关联客商档案取客商编码,未维护时拦截 */
|
||||
public String bfiAsstactNumber(ContractManage contract, KingdeeSyncContext context) {
|
||||
if (Func.isBlank(contract.getPartyB())) {
|
||||
throw new ServiceException("推送金蝶财务应付单失败,合同乙方不能为空,请检查合同[" + Func.toStr(contract.getContractNo()) + "]乙方信息");
|
||||
}
|
||||
// 批量同步时按乙方名称缓存客商编码(同批同一乙方只查一次),单笔同步退化为直接查询
|
||||
String customerCode = context.customerCodeCache().computeIfAbsent(contract.getPartyB(), this::queryCustomerCode);
|
||||
if (Func.isBlank(customerCode)) {
|
||||
throw new ServiceException("推送金蝶财务应付单失败,合同乙方[" + contract.getPartyB() + "]未在客商档案中维护客商编码,请补充后重试");
|
||||
}
|
||||
return customerCode;
|
||||
}
|
||||
|
||||
/** 按乙方名称(全称或简称)查询客商编码,未维护返回 null */
|
||||
private String queryCustomerCode(String partyB) {
|
||||
CustomerArchive customer = customerArchiveMapper.selectOne(Wrappers.<CustomerArchive>lambdaQuery()
|
||||
.and(wrapper -> wrapper.eq(CustomerArchive::getFullName, partyB).or().eq(CustomerArchive::getShortName, partyB))
|
||||
.eq(CustomerArchive::getIsDeleted, 0).last("limit 1"));
|
||||
return customer == null ? null : customer.getCustomerCode();
|
||||
}
|
||||
|
||||
/**
|
||||
* 列表按批大小切分逐批处理(末批可能不足批大小),远程接口批量入参分批用,避免单次过滤条件过长
|
||||
*
|
||||
* @param items 待处理列表
|
||||
* @param batchSize 批大小
|
||||
* @param batchConsumer 批处理逻辑
|
||||
* @param <T> 元素类型
|
||||
*/
|
||||
private <T> void batchForeach(List<T> items, int batchSize, Consumer<List<T>> batchConsumer) {
|
||||
for (int from = 0; from < items.size(); from += batchSize) {
|
||||
batchConsumer.accept(items.subList(from, Math.min(from + batchSize, items.size())));
|
||||
}
|
||||
}
|
||||
|
||||
/** 金额空值兜底并保留 2 位小数 */
|
||||
private BigDecimal money(BigDecimal value) {
|
||||
return (value == null ? BigDecimal.ZERO : value).setScale(2, RoundingMode.HALF_UP);
|
||||
}
|
||||
|
||||
/**
|
||||
* 财务应付单/付款申请分录:每条来源费用行一条(不聚合)
|
||||
*
|
||||
* @param materialNumber 物料编码(货物类型档案货物编码)
|
||||
* @param measureunitNumber 计量单位编码(运费计算单位,档案暂无编码字段暂传单位名称)
|
||||
* @param entryCode 分录唯一标识(awrp_entry_code):费用明细行id,同行多货物类型拆分时追加序号
|
||||
* @param quantity 该行运输数量
|
||||
* @param price 该行运输单价(费用明细 unit_price)
|
||||
* @param amount 该行金额(含税),对应明细应付金额 e_pricetaxtotal
|
||||
* @param amountNoTax 该行金额(不含税),对应明细金额 e_amount
|
||||
* @param formalSettlementId 分录所属正式结算单id(多结算单付款申请 A103-1 按结算单勾稽各自应付单)
|
||||
*/
|
||||
public record BfiEntryGroup(String materialNumber, String measureunitNumber, String entryCode,
|
||||
BigDecimal quantity, BigDecimal price, BigDecimal amount, BigDecimal amountNoTax,
|
||||
Long formalSettlementId) {
|
||||
}
|
||||
|
||||
/**
|
||||
* 财务应付单(A102-2)推送结果
|
||||
*
|
||||
* @param billNo 财务应付单单据编号(金蝶已存在时返回已有单据编号)
|
||||
* @param alreadyExist 金蝶是否已存在该防重键的财务应付单,true 表示本次幂等跳过保存(上次同步已在后续环节中断)
|
||||
*/
|
||||
public record BfiApBillPushOutcome(String billNo, boolean alreadyExist) {
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user