diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/FormalSettlementPayment.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/FormalSettlementPayment.java index 962f7ad4..fdfb94eb 100644 --- a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/FormalSettlementPayment.java +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/FormalSettlementPayment.java @@ -26,12 +26,20 @@ import java.math.BigDecimal; @TableName("blade_formal_settlement_payment") public class FormalSettlementPayment extends TenantEntity { @Serial private static final long serialVersionUID = 1L; + /** 正式结算单id */ private Long formalSettlementId; + /** 付款单号(租户内唯一) */ private String paymentNo; + /** 付款类型(final:结算尾款) */ private String paymentType; + /** 申请付款金额 */ private BigDecimal appliedAmount; + /** 已付金额 */ private BigDecimal paidAmount; + /** 单据状态(reviewing:审核中,approved:审核通过,returned:已退回,voided:已作废,paid:已付款) */ private String billStatus; + /** 金蝶(星瀚)单据编号 */ private String kingdeeBillNo; + /** 备注 */ private String remark; } diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/constant/KingdeeBfiConstant.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/constant/KingdeeBfiConstant.java index 46a6751a..f219bf07 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/constant/KingdeeBfiConstant.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/constant/KingdeeBfiConstant.java @@ -52,6 +52,8 @@ public final class KingdeeBfiConstant { /** 联调默认计量单位编码:金蝶测试档案值,档案维护计量单位编码后替换。 */ public static final String BFI_AP_BILL_MEASUREUNIT_NUMBER_TEST = "TON"; + /** 联调默认银行行号:金蝶测试档案值,档案维护银行行号后替换。 */ + public static final String BFI_AP_BILL_BANKACCOUNT_NUMBER_TEST = "001110012009"; /** 联调默认项目编码:金蝶测试档案值,项目档案与金蝶对齐后替换。 */ public static final String BFI_AP_BILL_PROJECT_NUMBER_TEST = "999"; diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/KingdeeSyncContext.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/KingdeeSyncContext.java index 5b689078..f2cbde55 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/KingdeeSyncContext.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/KingdeeSyncContext.java @@ -41,10 +41,11 @@ import java.util.Map; public record KingdeeSyncContext(Map cargoCodeByName, Map contractCache, Map customerCodeCache, Map projectCache, Map apBillNoCache, - Map> settlementDetailCache) { + Map> settlementDetailCache, + Map apBillPlanCodeCache) { /** 空上下文:缓存可写但初始为空,首条记录命中后全批复用 */ public static KingdeeSyncContext empty() { - return new KingdeeSyncContext(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<>()); } } diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/PaymentApplicationServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/PaymentApplicationServiceImpl.java index 38fb5d01..de02aaa1 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/PaymentApplicationServiceImpl.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/PaymentApplicationServiceImpl.java @@ -41,12 +41,14 @@ 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.BfiApBillSavePlanDTO; 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; @@ -110,6 +112,7 @@ import java.math.BigDecimal; import java.math.RoundingMode; import java.time.LocalDate; import java.time.LocalDateTime; +import java.time.ZoneId; import java.time.format.DateTimeFormatter; import java.util.ArrayList; import java.util.Arrays; @@ -422,7 +425,7 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl approvedSettlementPayments(PaymentApplication entity, KingdeeSyncContext context) { + List settlementIds = settlementDetails(entity, context).stream() + .map(FormalSettlementDetail::getFormalSettlementId) + .filter(Objects::nonNull).distinct().toList(); + if (Func.isEmpty(settlementIds)) { + return List.of(); + } + return formalSettlementPaymentMapper.selectList(Wrappers.lambdaQuery() + .in(FormalSettlementPayment::getFormalSettlementId, settlementIds) + .eq(FormalSettlementPayment::getIsDeleted, 0) + .eq(FormalSettlementPayment::getBillStatus, APPROVED) + .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 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个月),自动翻页 * @@ -948,12 +1007,14 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl entryGroups, List documentNos, KingdeeSyncContext context) { // A102-4 幂等预检:按防重键查询金蝶是否已生成财务应付单(批量同步时整批已预热缓存,此处仅懒加载未命中编码), @@ -963,7 +1024,7 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl approvedPayments = approvedSettlementPayments(entity, context); + List planEntryCodes = new ArrayList<>(); + if (Func.isNotEmpty(approvedPayments)) { + List 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); + } // 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); + return new BfiApBillPushOutcome(saveResult.getResult().get(0).getNumber(), false, planEntryCodes); } /** * 推送金蝶付款申请(A103-1 应付下推付款申请,正式结算付款第二步),返回星瀚付款申请单id。 - * 付款申请分录与财务应付单明细分录一一对应,申请金额取各组结算金额(含税)。 + * 付款申请分录与财务应付单明细分录一一对应,申请金额取各组结算金额(含税); + * awrp_finapbill_entryid 勾稽 A102-2 付款计划分录 awrp_entry_code_p(审核通过的付款信息单据号, + * 无审核通过付款信息时经 A102-4 反查金蝶自动生成的值);awrp_entry_code 填结算明细单据号 * - * @param entity 付款申请实体 - * @param entryGroups 结算明细聚合出的应付单分录组 + * @param entity 付款申请实体 + * @param entryGroups 结算明细聚合出的应付单分录组 + * @param planEntryCodes A102-2 付款计划分录 awrp_entry_code_p 集合(付款信息单据号),空表示未传付款计划 + * @param context 金蝶同步上下文(缓存载体) * @return 星瀚付款申请单id */ - private String pushBfiPayApplyPush(PaymentApplication entity, List entryGroups, KingdeeSyncContext context) { + private String pushBfiPayApplyPush(PaymentApplication entity, List entryGroups, List planEntryCodes, KingdeeSyncContext context) { ContractManage contract = requireContract(entity, context); String orgNumber = 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()); + // 【必填】外部系统编号 param.setAwrpSrcSys(bfiPayApplySrcSys); + // 【必填】申请日期:yyyy-MM-dd HH:mm:ss(各接口统一格式) param.setApplydate(applyDateText); - param.setPayorgNumber(orgNumber); - param.setPurorgNumber(orgNumber); - param.setApplycause(bfiApplyCause(entity.getPaymentType())); - param.setExratetableNumber(bfiPayApplyExratetable); - param.setExratedate(applyDateText); - param.setAwrpTextfield1(bfiPaymentMethodLabel(entity.getPaymentMethod())); - param.setCreatorName(entity.getApplicantName()); + // 付款组织.编码(非必传,不传时根据应付单自动带出) + // param.setPayorgNumber(orgNumber); + // 采购组织.编码(非必传,不传时根据应付单自动带出) + // param.setPurorgNumber(orgNumber); + // 请款事由(非必传) + // param.setApplycause(bfiApplyCause(entity.getPaymentType())); + // 汇率表.编码(非必传,不传时根据应付单自动带出) + // param.setExratetableNumber(bfiPayApplyExratetable); + // 汇率日期(非必传,不传时根据应付单自动带出) + // param.setExratedate(applyDateText); + // 付款渠道(非必传) + // param.setAwrpTextfield1(bfiPaymentMethodLabel(entity.getPaymentMethod())); + // 申请人(非必传) + // param.setCreatorName(entity.getApplicantName()); // 付款申请分录与财务应付单明细分录一一对应(awrp_finapbill_entryid 关联应付单明细 awrp_entry_code) List entries = new ArrayList<>(); for (int index = 0; index < entryGroups.size(); index++) { BfiEntryGroup entryGroup = entryGroups.get(index); BfiPayApplyPushEntryDTO entry = new BfiPayApplyPushEntryDTO(); - // 正式付款固定 201 + // 【必填】付款类型.编码:正式付款固定 201 entry.setEPaymenttypeNumber(KingdeeBfiConstant.BFI_PAYMENT_TYPE_FORMAL); - // 关联 A102-2 财务应付单分录,对应应付单明细的 awrp_entry_code - entry.setAwrpFinapbillEntryid(bfiEntryCode(entity.getPaymentNo(), index + 1)); - entry.setAwrpEntryCode(bfiEntryCode(entity.getPaymentNo(), index + 1)); + // 【必填】结算单分录id:A102-2 财务应付单付款计划分录的外部系统来源分录标识(awrp_entry_code_p) + entry.setAwrpFinapbillEntryid(finapbillEntryid); + // 【必填】对方系统唯一行id(不能重复):结算明细单据号 + entry.setAwrpEntryCode(entryGroup.documentNo()); + // 【必填】往来账户:乙方(收款方)银行账号 entry.setEAssacct(entity.getBankAccount()); - // TODO 待确认:往来银行为必填编码(e_bebank_number),当前暂传银行名称,待建立银行档案编码映射后调整 - entry.setEBebankNumber(entity.getBankName()); - // 申请金额:与对应应付单分录结算金额(含税)保持一致 + // 【必填】往来银行.编码(联调期临时传金蝶测试档案值,待建立银行档案编码映射后调整)todo + entry.setEBebankNumber(KingdeeBfiConstant.BFI_AP_BILL_BANKACCOUNT_NUMBER_TEST); + // 【必填】申请金额:与对应应付单分录结算金额(含税)保持一致 entry.setEApplyamount(entryGroup.amount()); - entry.setEExpaydate(applyDateText); - // entry.setProjectNumber(bfiProjectNumber(entity)); // 【必填】项目.项目编码(联调期临时传金蝶测试档案值) entry.setProjectNumber(KingdeeBfiConstant.BFI_AP_BILL_PROJECT_NUMBER_TEST); - entry.setERemark(entity.getRemark()); + // 期望付款日(非必传,不传时根据应付单自动带出) + // entry.setEExpaydate(applyDateText); + // 备注(非必传) + // entry.setERemark(entity.getRemark()); + // 【必填】外部单据类型 todo entry.setAwrpSrcType(bfiPayApplySrcType); entries.add(entry); } @@ -1129,12 +1236,13 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl planEntryCodes) { } /** * 解析付款申请关联的正式结算明细,按货物编码+计量单位聚合为应付单分录组。 *

物料编码:货物费用行/结算明细货物类型(支持“、”分隔多类型)匹配货物类型档案货物编码; - * 计量单位:快照运费计算单位(历史快照为空时兜底数量单位),档案暂无编码字段暂以单位名称作为编码。 + * 计量单位:快照运费计算单位(历史快照为空时兜底数量单位),档案暂无编码字段暂以单位名称作为编码; + * 分录唯一标识(awrp_entry_code)取所属结算明细单据号,A103-1 下推时以 awrp_finapbill_entryid 关联此行; * 分录金额直接取结算金额(含税/不含税),不按申请付款金额分摊。

* * @param entity 付款申请实体 @@ -1176,9 +1287,15 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl detailFees = formalSettlementDetailFees(settlementDetails); if (Func.isNotEmpty(detailFees)) { + // 结算明细id->单据号映射:货物费用行经此回溯所属结算明细单据号(作为分录唯一标识) + Map detailDocumentNoMap = new LinkedHashMap<>(); + for (FormalSettlementDetail settlementDetail : settlementDetails) { + detailDocumentNoMap.putIfAbsent(settlementDetail.getId(), settlementDetail.getDocumentNo()); + } for (FormalSettlementDetailFee detailFee : detailFees) { collectEntryGroups(groupMap, cargoCodeByName, detailFee.getCargoType(), resolveMeasureunitNumber(null, detailFee.getQuantityUnit()), + detailDocumentNoMap.get(detailFee.getFormalSettlementDetailId()), money(detailFee.getSettlementAmountTax()), money(detailFee.getSettlementAmountNoTax()), money(detailFee.getTransportQuantity()), "货物费用[" + Func.toStr(detailFee.getCargoName()) + " 第" + Func.toStr(detailFee.getLineNo()) + "行]"); @@ -1187,6 +1304,7 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl货物编码映射 * @param cargoType 货物类型(支持“、”分隔多类型) * @param measureunitNumber 计量单位编码(运费计算单位,档案暂无编码字段暂传单位名称) + * @param documentNo 结算明细单据号,作为分录唯一标识(awrp_entry_code) * @param amount 结算金额(含税) * @param amountNoTax 结算金额(不含税) * @param quantity 运输数量 * @param sourceDesc 来源行描述,用于异常提示 */ private void collectEntryGroups(LinkedHashMap groupMap, Map cargoCodeByName, - String cargoType, String measureunitNumber, BigDecimal amount, BigDecimal amountNoTax, + String cargoType, String measureunitNumber, String documentNo, BigDecimal amount, BigDecimal amountNoTax, BigDecimal quantity, String sourceDesc) { List cargoNames = splitCargoTypes(cargoType); if (Func.isBlank(measureunitNumber)) { throw new ServiceException("推送金蝶财务应付单失败:" + sourceDesc + "计量单位为空,请先维护运费计算单位"); } + if (Func.isBlank(documentNo)) { + throw new ServiceException("推送金蝶财务应付单失败:" + sourceDesc + "单据号为空,无法生成金蝶分录标识(awrp_entry_code)"); + } List amountShares = splitEqually(amount, cargoNames.size()); List amountNoTaxShares = splitEqually(amountNoTax, cargoNames.size()); List quantityShares = splitEqually(quantity, cargoNames.size()); @@ -1297,12 +1420,24 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl new BfiEntryGroup(left.materialNumber(), left.measureunitNumber(), + joinEntryDocumentNos(left.documentNo(), right.documentNo()), left.quantity().add(right.quantity()), left.amount().add(right.amount()), left.amountNoTax().add(right.amountNoTax()))); } } + /** 分录组合并时结算明细单据号去重下划线拼接(保持先后顺序),与外部系统来源单据号拼接规则一致 */ + private String joinEntryDocumentNos(String left, String right) { + if (Func.isBlank(left)) { + return right; + } + if (Func.isBlank(right) || left.equals(right)) { + return left; + } + return left.contains(right) ? left : right.contains(left) ? right : left + "_" + right; + } + /** 付款申请关联的正式结算货物费用行(blade_formal_settlement_detail_fee,每货物一条,金额为结算调整后精确值) */ private List formalSettlementDetailFees(List settlementDetails) { List settlementDetailIds = settlementDetails.stream().map(FormalSettlementDetail::getId)