feat: 收票单分录改为与应付单同源结算费用行,防重键改用收票登记id

This commit is contained in:
刘泉佳
2026-09-23 16:01:17 +08:00
parent cb0d9a0602
commit 0c064f06c9
2 changed files with 51 additions and 90 deletions
@@ -136,7 +136,7 @@ public class KingdeeInvoicePool extends TenantEntity {
*/
private String attachmentsJson;
/**
* A801-1 发票明细行JSON(BfiInvoicePoolItemVO 列表,A303-2 收票单分录取数来源)
* A801-1 发票明细行JSON(BfiInvoicePoolItemVO 列表,票池存档;收票单分录已改与应付单同源结算费用行)
*/
private String itemsJson;
/**
@@ -28,7 +28,6 @@ package org.springblade.transport.support;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springblade.core.log.exception.ServiceException;
import org.springblade.core.tool.jackson.JsonUtil;
import org.springblade.core.tool.utils.Func;
import org.springblade.thirdparty.bfi.constant.BfiConstant;
import org.springblade.thirdparty.bfi.pojo.dto.BfiInvoiceAssignItemDTO;
@@ -37,7 +36,6 @@ import org.springblade.thirdparty.bfi.pojo.dto.BfiReceivedInvoiceQueryDTO;
import org.springblade.thirdparty.bfi.pojo.dto.BfiReceivedInvoiceSaveDTO;
import org.springblade.thirdparty.bfi.pojo.dto.BfiReceivedInvoiceSaveEntryDTO;
import org.springblade.thirdparty.bfi.pojo.vo.BfiBillSaveResultVO;
import org.springblade.thirdparty.bfi.pojo.vo.BfiInvoicePoolItemVO;
import org.springblade.thirdparty.bfi.pojo.vo.BfiReceivedInvoiceVO;
import org.springblade.thirdparty.bfi.service.IBfiService;
import org.springblade.thirdparty.bfi.util.BfiUtil;
@@ -54,7 +52,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.Collection;
import java.util.LinkedHashMap;
import java.util.List;
@@ -64,9 +61,11 @@ import java.util.Map;
* 金蝶收票单同步支撑组件(图05 环节②③:A303-2 收票单批量保存 + A302-6 收票指定应付 + A801-3 收票单查询)。
* <p>收票登记「推送金蝶」由 {@link org.springblade.transport.service.IInvoiceReceiptService} 编排前置校验
* (审批通过、已绑定镜像、一票一应付、A102-4 应付单已生成预检、A801-3 幂等预检)后调用本组件;
* 报文组装取数:主表组织/币种/往来户复用应付支撑组件的合同派生方法,票面字段与分录取自票池镜像
* (invoice_type_code / items_json,A801-1 落库)。防重键:收票单 awrp_src_code=发票号码(一票一登记天然唯一),
* awrp_entry_code=发票号码-明细行号;A302-6 按「收票单防重键 ↔ 财务应付单防重键」一票一应付指定。
* 报文组装取数:主表组织/币种/往来户复用应付支撑组件的合同派生方法,票面字段取自票池镜像
* (invoice_type_code 等,A801-1 落库),明细分录与财务应付单 A102-2 同源同口径
* (逐结算货物费用行一条,{@link KingdeeApBillSyncSupport.BfiEntryGroup},分录标识同取费用行id)。
* 防重键:收票单 awrp_src_code=收票登记id,awrp_entry_code=应付货物费用行id(与应付单分录同键,
* 便于金蝶侧逐行勾稽);A302-6 按「收票单防重键 ↔ 财务应付单防重键」一票一应付指定。
* 财务应付单侧(A102-2/A102-4)取数与校验复用 {@link KingdeeApBillSyncSupport}。</p>
*
* @author Chill
@@ -149,27 +148,28 @@ public class KingdeeReceivedInvoiceSyncSupport {
/**
* 推送金蝶收票单(A303-2 批量保存,调用方已完成 A801-3 幂等预检)。
* 主表:组织/币种/往来户编码按分摊结算单关联合同派生(与应付单同源),销方=开票单位、购方=受票单位;
* 分录逐票池明细行展开(items_json),物料编码为联调期临时配置值。
* 联调口径沿用应付/应收侧:先只送 DTO 标注【必填】字段与票面核心字段(发票号码、税额金额),
* 非必填的地址/电话/开户行等 setter 注释保留,联调确认后放开
* 明细分录与财务应付单 A102-2 同源同口径:逐结算货物费用行一条(不聚合),物料/数量/单价/金额取自
* 与应付单分录同一解析结果({@link KingdeeApBillSyncSupport.BfiEntryGroup}),分录标识同取费用行id。
* 联调口径沿用应付侧:先只送 DTO 标注【必填】字段与票面核心字段(发票号码),
* 非必填的税号/地址/电话/开户行等 setter 注释保留,联调确认后放开
*
* @param receipt 收票登记(审批通过、已绑定镜像)
* @param mirror 票池镜像(invoice_type_code/items_json 取数来源)
* @param contract 分摊应付结算单关联合同(组织/币种/往来户派生)
* @param context 金蝶同步上下文(缓存载体)
* @param receipt 收票登记(审批通过、已绑定镜像)
* @param mirror 票池镜像(票面主表字段取数来源)
* @param contract 分摊应付结算单关联合同(组织/币种/往来户派生)
* @param entryGroups 分摊结算单解析出的货物费用行分录(与应付单 A102-2 同一解析结果)
* @param context 金蝶同步上下文(缓存载体)
* @return 金蝶收票单单据编号
*/
public String pushReceivedInvoice(InvoiceReceipt receipt, KingdeeInvoicePool mirror, ContractManage contract,
KingdeeSyncContext context) {
List<KingdeeApBillSyncSupport.BfiEntryGroup> entryGroups, KingdeeSyncContext context) {
if (Func.isBlank(mirror.getInvoiceTypeCode())) {
throw new ServiceException("推送金蝶收票单失败:镜像缺少票易通发票类型编码,请先重新绑定票池发票");
}
List<BfiInvoicePoolItemVO> items = parseItems(mirror);
BfiReceivedInvoiceSaveDTO param = new BfiReceivedInvoiceSaveDTO();
// 【必填】外部系统编号(候选键)
param.setAwrpSrcSys(bfiProperties.getSrcSys());
// 【必填】外部系统来源单据号(候选键):发票号码(一票一登记天然唯一,重推同键幂等)
param.setAwrpSrcCode(receipt.getInvoiceNo());
param.setAwrpSrcCode(String.valueOf(receipt.getId()));
// 发票号码(非必填但为收票单核心业务字段,必送)
param.setInvoiceno(receipt.getInvoiceNo());
// 【必填】收票日期:以推送当日为收票日期
@@ -199,20 +199,20 @@ public class KingdeeReceivedInvoiceSyncSupport {
// 录入价税合计(非必传,与应付/应收侧口径一致传 true)
param.setIsincludetax(true);
// 销方税号(非必传,联调确认后放开)
// param.setSellertin(mirror.getSalerTaxNo());
param.setSellertin(mirror.getSalerTaxNo());
// 购方税号(非必传,联调确认后放开)
// param.setBuyertin(mirror.getBuyerTaxNo());
param.setBuyertin(mirror.getBuyerTaxNo());
// 销方地址/电话/开户行/账号(非必传,被金蝶强校验时从镜像 bank_name/bank_account/issuing_bank/phone 补齐)
// param.setSelleraddr(null);
// param.setSellertel(mirror.getPhone());
// param.setSellerbank(mirror.getBankName());
// param.setSellracct(mirror.getBankAccount());
param.setSellertel(mirror.getPhone());
param.setSellerbank(mirror.getBankName());
param.setSelleracct(mirror.getBankAccount());
// 备注(非必传)
// param.setRemark(receipt.getRemark());
// 【必填】外部单据类型
param.setAwrpSrcType(KingdeeBfiConstant.BFI_RECEIVED_INVOICE_SRC_TYPE);
// 【必填】明细分录:逐票池明细行展开
param.setEntry(buildEntries(receipt.getInvoiceNo(), items));
// 【必填】明细分录:与应付单 A102-2 同源同口径,逐结算货物费用行一条(不聚合)
param.setEntry(buildEntries(entryGroups));
BfiBillSaveResultVO saveResult = bfiService.saveReceivedInvoice(param);
kingdeeApBillSyncSupport.requireBfiSaveSuccess(saveResult, "推送金蝶收票单");
@@ -220,85 +220,46 @@ public class KingdeeReceivedInvoiceSyncSupport {
}
/**
* 票池明细行 → A303-2 分录:金额/税额/数量/单价直取票面明细,税收分类编码取 goodsCode,
* 税率换算为金蝶税率编码(0.13→V13),物料编码为联调期临时配置值(发票明细无金蝶物料概念)
* 分录组装与应付单 A102-2 明细同源同口径:逐结算货物费用行一条(不聚合),物料/数量/单价/含税/不含税金额
* 取自 {@link KingdeeApBillSyncSupport.BfiEntryGroup}(与应付单分录同一解析结果),分录标识同取费用行id,
* 便于金蝶侧收票单明细与应付单明细逐行勾稽;数量无吨位时兜底 1;
* 票面明细行(items_json)不再作为分录取数来源,税收分类/税率/税额随应付口径暂不送(联调强校验时再补)
*
* @param invoiceNo 发票号码(分录防重键前缀)
* @param items 票池明细行
* @param entryGroups 分摊结算单解析出的货物费用行分录(与应付单 A102-2 同一解析结果)
* @return A303-2 明细分录
*/
private List<BfiReceivedInvoiceSaveEntryDTO> buildEntries(String invoiceNo, List<BfiInvoicePoolItemVO> items) {
private List<BfiReceivedInvoiceSaveEntryDTO> buildEntries(List<KingdeeApBillSyncSupport.BfiEntryGroup> entryGroups) {
List<BfiReceivedInvoiceSaveEntryDTO> entries = new ArrayList<>();
int index = 1;
for (BfiInvoicePoolItemVO item : items) {
BigDecimal amount = parseItemAmount(invoiceNo, item.getDetailAmount(), "金额");
BigDecimal tax = parseItemAmount(invoiceNo, item.getTaxAmount(), "税额");
for (KingdeeApBillSyncSupport.BfiEntryGroup entryGroup : entryGroups) {
BfiReceivedInvoiceSaveEntryDTO entry = new BfiReceivedInvoiceSaveEntryDTO();
// 【必填】价税合计:明细行金额+税额
entry.setEPricetaxtotal(kingdeeApBillSyncSupport.money(amount.add(tax)));
// 【必填】数量:票面数量,无数量兜底 1
BigDecimal quantity = parseItemAmount(invoiceNo, item.getNum(), "数量");
entry.setQuantity(quantity.compareTo(BigDecimal.ZERO) > 0 ? quantity : BigDecimal.ONE);
// 【必填】单价:票面单价(金额口径为价税合计录入,单价仅作参考)
entry.setPrice(parseItemAmount(invoiceNo, item.getUnitPrice(), "单价"));
// 【必填】物料编码.编码:联调期临时值,真实口径与金蝶对齐后替换
entry.setMaterialNumber(bfiProperties.getReceivedInvoice().getMaterialNumber());
// 【必填】税收分类.编码:票面明细税收分类编码
if (Func.isBlank(item.getGoodsCode())) {
throw new ServiceException("推送金蝶收票单失败:发票" + invoiceNo + "第" + index + "行明细无税收分类编码");
// 【必填】价税合计:该行含税金额(与应付单 e_pricetaxtotal 同源取数)
entry.setEPricetaxtotal(kingdeeApBillSyncSupport.money(entryGroup.amount()));
// 【必填】数量:该行运输数量,无吨位数据时兜底 1(与应付单口径一致)
entry.setQuantity(entryGroup.quantity().compareTo(BigDecimal.ZERO) > 0 ? entryGroup.quantity() : BigDecimal.ONE);
// 【必填】单价:该行运输单价(与应付单同源;单据头 isincludetax=true 录入价税合计,单价仅作参考)
entry.setPrice(entryGroup.price());
// 【必填】物料编码.编码:结算明细货物类型匹配货物类型档案货物编码
entry.setMaterialNumber(entryGroup.materialNumber());
// 金额(不含税):该行结算金额(不含税)(与应付单 e_amount 同源取数)
entry.setEAmount(kingdeeApBillSyncSupport.money(entryGroup.amountNoTax()));
// 【必填】外部系统来源分录标识(候选键):应付货物费用行id(与应付单分录同键,便于逐行勾稽)
entry.setAwrpEntryCode(entryGroup.entryCode());
// 调试(与应付单 A102-2 保持一致:联调期物料编码临时传金蝶测试档案值,联调通过后与应付侧同步移除)
if (true) {
// 【必填】物料编码.编码:结算明细货物类型匹配货物类型档案货物编码
entry.setMaterialNumber(KingdeeBfiConstant.BFI_AP_BILL_MATERIAL_NUMBER_TEST);
}
entry.setETaxclassNumber(item.getGoodsCode().trim());
// 【必填】税率.编码:票面税率(小数)换算金蝶税率编码(0.13→V13)
entry.setTaxrateidNumber(taxRateCode(invoiceNo, index, item.getTaxRate()));
// 金额(不含税)/税额(非必传,票面直取)
entry.setEAmount(kingdeeApBillSyncSupport.money(amount));
entry.setETax(kingdeeApBillSyncSupport.money(tax));
// 【必填】外部系统来源分录标识(候选键):发票号码-行号
entry.setAwrpEntryCode(invoiceNo + "-" + index);
entries.add(entry);
index++;
}
if (entries.isEmpty()) {
throw new ServiceException("推送金蝶收票单失败:发票" + invoiceNo + "票池明细为空,请先补查票池发票明细");
throw new ServiceException("推送金蝶收票单失败:分摊结算单无货物费用行,无法组装收票单明细");
}
return entries;
}
/**
* 票池明细行 JSON 解析(items 为空时返回空列表,由调用方拦截提示补查)
*/
public List<BfiInvoicePoolItemVO> parseItems(KingdeeInvoicePool mirror) {
if (Func.isBlank(mirror.getItemsJson())) {
return List.of();
}
BfiInvoicePoolItemVO[] items = JsonUtil.parse(mirror.getItemsJson(), BfiInvoicePoolItemVO[].class);
return items == null ? List.of() : Arrays.asList(items);
}
/** 票面税率(小数文本)换算金蝶税率编码:0.13→V13、0.01→V1、0→V0 */
private String taxRateCode(String invoiceNo, int lineNo, String taxRateText) {
if (Func.isBlank(taxRateText)) {
throw new ServiceException("推送金蝶收票单失败:发票" + invoiceNo + "第" + lineNo + "行明细税率为空");
}
try {
BigDecimal percent = new BigDecimal(taxRateText.trim()).multiply(BigDecimal.valueOf(100)).stripTrailingZeros();
return "V" + percent.toPlainString();
} catch (NumberFormatException exception) {
throw new ServiceException("推送金蝶收票单失败:发票" + invoiceNo + "第" + lineNo + "行明细税率[" + taxRateText + "]无法解析");
}
}
private BigDecimal parseItemAmount(String invoiceNo, String value, String name) {
if (Func.isBlank(value)) {
return BigDecimal.ZERO;
}
try {
return new BigDecimal(value.trim());
} catch (NumberFormatException exception) {
throw new ServiceException("推送金蝶收票单失败:发票" + invoiceNo + "明细" + name + "[" + value + "]无法解析");
}
}
// ==================== A302-6 收票指定应付 ====================
/**