feat: 金蝶收票单A303-2按先票后款组装报文(分录取发票明细行/往来户销方供应商代码/币别CNY)

This commit is contained in:
刘泉佳
2026-10-31 16:06:32 +08:00
parent 9e3240712c
commit bbf819d1b3
2 changed files with 180 additions and 53 deletions
@@ -186,6 +186,9 @@ public final class KingdeeBfiConstant {
/** A303-2 业务类型(PUR:采购 / FEE:费用,TMS 服务费进项票暂按采购)。待确认:取值需金蝶侧最终确认。 */ /** A303-2 业务类型(PUR:采购 / FEE:费用,TMS 服务费进项票暂按采购)。待确认:取值需金蝶侧最终确认。 */
public static final String BFI_RECEIVED_BIZ_TYPE = "PUR"; public static final String BFI_RECEIVED_BIZ_TYPE = "PUR";
/** A303-2 结算币别:收票登记不挂合同(先票后款),数电票固定人民币。 */
public static final String BFI_RECEIVED_CURRENCY_NUMBER = "CNY";
/** A303-2 必填:外部单据类型(收票单来源标识)。待确认:取值需金蝶侧确认,确认后如有调整直接改此常量。 */ /** A303-2 必填:外部单据类型(收票单来源标识)。待确认:取值需金蝶侧确认,确认后如有调整直接改此常量。 */
public static final String BFI_RECEIVED_INVOICE_SRC_TYPE = "TMS收票登记"; public static final String BFI_RECEIVED_INVOICE_SRC_TYPE = "TMS收票登记";
@@ -28,6 +28,7 @@ package org.springblade.transport.support;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springblade.core.log.exception.ServiceException; import org.springblade.core.log.exception.ServiceException;
import org.springblade.core.tool.jackson.JsonUtil;
import org.springblade.core.tool.utils.Func; import org.springblade.core.tool.utils.Func;
import org.springblade.thirdparty.bfi.constant.BfiConstant; import org.springblade.thirdparty.bfi.constant.BfiConstant;
import org.springblade.thirdparty.bfi.pojo.dto.BfiInvoiceAssignItemDTO; import org.springblade.thirdparty.bfi.pojo.dto.BfiInvoiceAssignItemDTO;
@@ -37,22 +38,24 @@ import org.springblade.thirdparty.bfi.pojo.dto.BfiReceivedInvoiceSaveDTO;
import org.springblade.thirdparty.bfi.pojo.dto.BfiReceivedInvoiceSaveEntryDTO; import org.springblade.thirdparty.bfi.pojo.dto.BfiReceivedInvoiceSaveEntryDTO;
import org.springblade.thirdparty.bfi.pojo.vo.BfiBillSaveResultItemVO; import org.springblade.thirdparty.bfi.pojo.vo.BfiBillSaveResultItemVO;
import org.springblade.thirdparty.bfi.pojo.vo.BfiBillSaveResultVO; 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.pojo.vo.BfiReceivedInvoiceVO;
import org.springblade.thirdparty.bfi.service.IBfiService; import org.springblade.thirdparty.bfi.service.IBfiService;
import org.springblade.thirdparty.bfi.util.BfiUtil; import org.springblade.thirdparty.bfi.util.BfiUtil;
import org.springblade.transport.config.BfiProperties; import org.springblade.transport.config.BfiProperties;
import org.springblade.transport.constant.KingdeeBfiConstant; import org.springblade.transport.constant.KingdeeBfiConstant;
import org.springblade.transport.pojo.entity.ContractManage;
import org.springblade.transport.pojo.entity.InvoiceReceipt; import org.springblade.transport.pojo.entity.InvoiceReceipt;
import org.springblade.transport.pojo.entity.KingdeeInvoicePool; import org.springblade.transport.pojo.entity.KingdeeInvoicePool;
import org.springblade.transport.service.KingdeeSyncContext; import org.springblade.transport.service.KingdeeSyncContext;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.math.RoundingMode;
import java.time.LocalDate; import java.time.LocalDate;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter; import java.time.format.DateTimeFormatter;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection; import java.util.Collection;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
import java.util.List; import java.util.List;
@@ -61,12 +64,11 @@ import java.util.Map;
/** /**
* 金蝶收票单同步支撑组件(图05 环节②③:A303-2 收票单批量保存 + A302-6 收票指定应付 + A801-3 收票单查询)。 * 金蝶收票单同步支撑组件(图05 环节②③:A303-2 收票单批量保存 + A302-6 收票指定应付 + A801-3 收票单查询)。
* <p>收票登记「推送金蝶」由 {@link org.springblade.transport.service.IInvoiceReceiptService} 编排前置校验 * <p>收票登记「推送金蝶」由 {@link org.springblade.transport.service.IInvoiceReceiptService} 编排前置校验
* (审批通过、已绑定镜像、一票一应付、A102-4 应付单已生成预检、A801-3 幂等预检)后调用本组件; * (审批通过、已绑定镜像、A801-3 幂等预检)后调用本组件;先票后款链路:A303-2 推送即终态(pushed),
* 报文组装取数:主表组织/币种/往来户复用应付支撑组件的合同派生方法,票面字段取自票池镜像 * A302-6 指定应付移交付款结算单侧(应付单 A102-2 生成后按认领关系逐单指定)。
* (invoice_type_code 等,A801-1 落库),明细分录与财务应付单 A102-2 同源同口径 * 报文组装取数:往来户=发票销售方金蝶供应商代码(A901-1),结算组织=页面手选部门沿部门树上溯平台公司,
* (结算明细行+结算合计费用项各一条,{@link KingdeeApBillSyncSupport.BfiEntryGroup},分录标识同键)。 * 币别固定 CNY;票面字段取自票池镜像(A801-1 落库),明细分录逐行取发票明细(镜像 items_json),
* 防重键:收票单 awrp_src_code=收票登记id,awrp_entry_code=结算明细行单据号/结算合计费用项拼接 * 分录标识 awrp_entry_code=发票号码_seq(重推幂等候选键)。
* (与应付单分录同键,便于金蝶侧逐行勾稽);A302-6 按「收票单防重键 ↔ 财务应付单防重键」一票一应付指定。
* 财务应付单侧(A102-2/A102-4)取数与校验复用 {@link KingdeeApBillSyncSupport}。</p> * 财务应付单侧(A102-2/A102-4)取数与校验复用 {@link KingdeeApBillSyncSupport}。</p>
* *
* @author Chill * @author Chill
@@ -148,21 +150,19 @@ public class KingdeeReceivedInvoiceSyncSupport {
/** /**
* 推送金蝶收票单(A303-2 批量保存,调用方已完成 A801-3 幂等预检)。 * 推送金蝶收票单(A303-2 批量保存,调用方已完成 A801-3 幂等预检)。
* 主表:组织/币种/往来户编码按分摊结算单关联合同派生(与应付单同源),销方=开票单位、购方=受票单位; * 先票后款链路:收票登记不挂结算单/合同——主表往来户=发票销售方金蝶供应商代码(receipt.supplierCode,
* 明细分录与财务应付单 A102-2 同源同口径:结算明细按 货物→费用大类→税率 拆分行+手动添加结算合计费用项每行一条,物料/数量/单价/金额取自 * 缺失实时查 A901-1 一次,仍缺失报错阻断),结算组织=页面手选部门(receipt.deptId)沿部门树反查平台公司,
* 与应付单分录同一解析结果({@link KingdeeApBillSyncSupport.BfiEntryGroup}),分录标识同键。 * 币别固定 CNY,往来类型取固定常量;销方=开票单位、购方=受票单位。
* 明细分录逐行取发票明细行(镜像 items_json,{@link BfiInvoicePoolItemVO}),不与结算明细同源。
* 联调口径沿用应付侧:先只送 DTO 标注【必填】字段与票面核心字段(发票号码), * 联调口径沿用应付侧:先只送 DTO 标注【必填】字段与票面核心字段(发票号码),
* 非必填的税号/地址/电话/开户行等 setter 注释保留,联调确认后放开 * 非必填的税号/地址/电话/开户行等 setter 注释保留,联调确认后放开
* *
* @param receipt 收票登记(审批通过、已绑定镜像) * @param receipt 收票登记(审批通过、已绑定镜像)
* @param mirror 票池镜像(票面主表字段取数来源) * @param mirror 票池镜像(票面主表字段+明细行取数来源)
* @param contract 分摊应付结算单关联合同(组织/币种/往来户派生) * @param context 金蝶同步上下文(缓存载体)
* @param entryGroups 分摊结算单解析出的分录(与应付单 A102-2 同一解析结果)
* @param context 金蝶同步上下文(缓存载体)
* @return 推送结果(单据编号 + 星瀚收票单内码,A902 附件上传定位用) * @return 推送结果(单据编号 + 星瀚收票单内码,A902 附件上传定位用)
*/ */
public ReceivedInvoicePushOutcome pushReceivedInvoice(InvoiceReceipt receipt, KingdeeInvoicePool mirror, ContractManage contract, public ReceivedInvoicePushOutcome pushReceivedInvoice(InvoiceReceipt receipt, KingdeeInvoicePool mirror, KingdeeSyncContext context) {
List<KingdeeApBillSyncSupport.BfiEntryGroup> entryGroups, KingdeeSyncContext context) {
if (Func.isBlank(mirror.getInvoiceTypeCode())) { if (Func.isBlank(mirror.getInvoiceTypeCode())) {
throw new ServiceException("推送金蝶收票单失败:镜像缺少票易通发票类型编码,请先重新绑定票池发票"); throw new ServiceException("推送金蝶收票单失败:镜像缺少票易通发票类型编码,请先重新绑定票池发票");
} }
@@ -183,14 +183,17 @@ public class KingdeeReceivedInvoiceSyncSupport {
param.setInvoicetypef7Number(mirror.getInvoiceTypeCode()); param.setInvoicetypef7Number(mirror.getInvoiceTypeCode());
// 【必填】付款方式:(与应付单一致,不允许金蝶自动推付款) // 【必填】付款方式:(与应付单一致,不允许金蝶自动推付款)
param.setPaymenttype(BfiConstant.PAY_MODE_CASH); param.setPaymenttype(BfiConstant.PAY_MODE_CASH);
// 【必填】往来类型:统一固定客户(KingdeeApBillSyncSupport#bfiAsstactType,与应付单/付款申请同口径) // 【必填】往来类型:固定常量(收票登记不挂合同,直接取常量)
param.setAsstacttype(kingdeeApBillSyncSupport.bfiAsstactType(contract)); param.setAsstacttype(KingdeeBfiConstant.BFI_CUSTOMER_ASSTACT_TYPE);
// 【必填】结算组织.编码:合同关联项目业务部门反查平台公司(与应付单同源) // 【必填】结算组织.编码:页面手选部门沿部门树反查平台公司(收票登记 deptId)
param.setOrgNumber(kingdeeApBillSyncSupport.bfiOrgNumber(contract, context)); param.setOrgNumber(kingdeeApBillSyncSupport.kingdeeOrgNumberByDept(receipt.getDeptId(), context));
// 【必填】结算币别:合同结算币种 // 【必填】结算币别:固定 CNY(收票登记不挂合同,数电票人民币)
param.setCurrencyNumber(kingdeeApBillSyncSupport.bfiCurrency(contract)); param.setCurrencyNumber(KingdeeBfiConstant.BFI_RECEIVED_CURRENCY_NUMBER);
// 【必填】往来户.编码:合同乙方(开票供应商)客商编码 // 【必填】往来户.编码:发票销售方金蝶供应商代码(缺失实时查 A901-1,仍缺失报错阻断)
param.setReceivablessuppNumber(kingdeeApBillSyncSupport.bfiAsstactNumber(contract, context)); String supplierNumber = this.requireSupplierNumber(receipt, mirror);
param.setReceivablessuppNumber(supplierNumber);
// 销售方名称.编码(非必填):同往来户供应商代码
param.setAsstactNumber(supplierNumber);
// 【必填】汇率表.编码 // 【必填】汇率表.编码
param.setExratetableNumber(BfiConstant.EXRATE_TABLE_DEFAULT); param.setExratetableNumber(BfiConstant.EXRATE_TABLE_DEFAULT);
// 【必填】销方名称:镜像开票单位 // 【必填】销方名称:镜像开票单位
@@ -212,8 +215,8 @@ public class KingdeeReceivedInvoiceSyncSupport {
// param.setRemark(receipt.getRemark()); // param.setRemark(receipt.getRemark());
// 【必填】外部单据类型 // 【必填】外部单据类型
param.setAwrpSrcType(KingdeeBfiConstant.BFI_RECEIVED_INVOICE_SRC_TYPE); param.setAwrpSrcType(KingdeeBfiConstant.BFI_RECEIVED_INVOICE_SRC_TYPE);
// 【必填】明细分录:与应付单 A102-2 同源同口径,结算明细行+结算合计费用项各一条(不聚合) // 【必填】明细分录:逐行取发票明细行(镜像 items_json),不与结算明细同源
param.setEntry(buildEntries(entryGroups)); param.setEntry(this.buildEntries(receipt, mirror));
BfiBillSaveResultVO saveResult = bfiService.saveReceivedInvoice(param); BfiBillSaveResultVO saveResult = bfiService.saveReceivedInvoice(param);
kingdeeApBillSyncSupport.requireBfiSaveSuccess(saveResult, "推送金蝶收票单"); kingdeeApBillSyncSupport.requireBfiSaveSuccess(saveResult, "推送金蝶收票单");
@@ -222,6 +225,30 @@ public class KingdeeReceivedInvoiceSyncSupport {
savedItem.getId() == null ? null : String.valueOf(savedItem.getId())); savedItem.getId() == null ? null : String.valueOf(savedItem.getId()));
} }
/**
* 往来户供应商编码:优先取登记上已回填的 supplierCode;缺失时按销方名称实时查一次 A901-1 并回填实体
* (调用方推送成功后随推送结果落库),仍缺失报错阻断
*
* @param receipt 收票登记
* @param mirror 票池镜像(销方名称=开票单位)
* @return 金蝶供应商编码
*/
private String requireSupplierNumber(InvoiceReceipt receipt, KingdeeInvoicePool mirror) {
String supplierCode = receipt.getSupplierCode();
if (Func.isBlank(supplierCode)) {
log.info("收票登记[{}]往来户供应商代码缺失,按销方[{}]实时查询 A901-1", receipt.getInvoiceNo(), mirror.getIssuerName());
supplierCode = kingdeeApBillSyncSupport.queryCustomerCode(mirror.getIssuerName());
if (Func.isNotBlank(supplierCode)) {
receipt.setSupplierCode(supplierCode);
}
}
if (Func.isBlank(supplierCode)) {
throw new ServiceException("推送金蝶收票单失败:发票销售方[" + Func.toStr(mirror.getIssuerName())
+ "]未建档金蝶供应商编码,请先维护客商档案后再推送");
}
return supplierCode;
}
/** /**
* A303-2 收票单推送结果 * A303-2 收票单推送结果
* *
@@ -232,42 +259,139 @@ public class KingdeeReceivedInvoiceSyncSupport {
} }
/** /**
* 分录组装与应付单 A102-2 明细同源同口径:结算明细按 货物→费用大类→税率 拆分行 + 手动添加的结算合计费用项每行一条, * 分录组装:逐行取发票明细行(镜像 items_json 反序列化为 {@link BfiInvoicePoolItemVO}),一条明细行一条分录:
* 物料/数量/单价/含税/不含税金额 * 价税合计=明细金额+税额(合计为 0 拦截);数量取 num(≤0/空/不可解析兜底 1);单价取 unitPrice(空按价税合计/数量反算);
* 取自 {@link KingdeeApBillSyncSupport.BfiEntryGroup}(与应付单分录同一解析结果),分录标识同键, * 物料编码与税收分类编码取行商品编码 goodsCode(缺失拦截);税率表编码="V"+整数税率(13/13%/0.13→V13,
* 便于金蝶侧收票单明细与应付单明细逐行勾稽;数量无效时兜底 1; * 缺失、0/免税行拦截提示人工);不含税金额/税额取明细金额/税额(空不送);分录标识=发票号码_seq(重推幂等候选键)
* 票面明细行(items_json)不再作为分录取数来源,税收分类/税率/税额随应付口径暂不送(联调强校验时再补)
* *
* @param entryGroups 分摊结算单解析出的分录(与应付单 A102-2 同一解析结果) * @param receipt 收票登记(发票号码,分录标识前缀)
* @param mirror 票池镜像(明细行 items_json)
* @return A303-2 明细分录 * @return A303-2 明细分录
*/ */
private List<BfiReceivedInvoiceSaveEntryDTO> buildEntries(List<KingdeeApBillSyncSupport.BfiEntryGroup> entryGroups) { private List<BfiReceivedInvoiceSaveEntryDTO> buildEntries(InvoiceReceipt receipt, KingdeeInvoicePool mirror) {
if (Func.isBlank(mirror.getItemsJson())) {
throw new ServiceException("推送金蝶收票单失败:发票[" + receipt.getInvoiceNo() + "]票池镜像无明细行,请先按票号补查票池明细");
}
List<BfiInvoicePoolItemVO> items = this.parseMirrorItems(receipt.getInvoiceNo(), mirror.getItemsJson());
if (Func.isEmpty(items)) {
throw new ServiceException("推送金蝶收票单失败:发票[" + receipt.getInvoiceNo() + "]票池镜像明细行为空,请先按票号补查票池明细");
}
List<BfiReceivedInvoiceSaveEntryDTO> entries = new ArrayList<>(); List<BfiReceivedInvoiceSaveEntryDTO> entries = new ArrayList<>();
for (KingdeeApBillSyncSupport.BfiEntryGroup entryGroup : entryGroups) { int rowNo = 0;
BfiReceivedInvoiceSaveEntryDTO entry = new BfiReceivedInvoiceSaveEntryDTO(); for (BfiInvoicePoolItemVO item : items) {
// 【必填】价税合计:该行含税金额(与应付单 e_pricetaxtotal 同源取数) rowNo++;
entry.setEPricetaxtotal(kingdeeApBillSyncSupport.money(entryGroup.amount())); // 分录序号:优先取明细行 seq,缺失按行号兜底
// 【必填】数量:该行运输数量,无吨位数据时兜底 1(与应付单口径一致) String seq = Func.isBlank(item.getSeq()) ? String.valueOf(rowNo) : item.getSeq().trim();
entry.setQuantity(entryGroup.quantity().compareTo(BigDecimal.ZERO) > 0 ? entryGroup.quantity() : BigDecimal.ONE); BigDecimal detailAmount = this.parseMirrorAmount(receipt.getInvoiceNo(), seq, "明细金额", item.getDetailAmount());
// 【必填】单价:该行运输单价(与应付单同源;单据头 isincludetax=true 录入价税合计,单价仅作参考) BigDecimal taxAmount = this.parseMirrorAmount(receipt.getInvoiceNo(), seq, "税额", item.getTaxAmount());
entry.setPrice(entryGroup.price()); // 【必填】价税合计:明细金额+税额(合计为 0 拦截)
// 【必填】物料编码.编码:按装卸大类/项目业务模式/运单运输方式映射(thirdparty.bfi.material.* 配置,与应付单同源) BigDecimal priceTaxTotal = kingdeeApBillSyncSupport.money(detailAmount == null ? BigDecimal.ZERO : detailAmount)
entry.setMaterialNumber(entryGroup.materialNumber()); .add(kingdeeApBillSyncSupport.money(taxAmount == null ? BigDecimal.ZERO : taxAmount));
// 金额(不含税):该行结算金额(不含税)(与应付单 e_amount 同源取数;结算合计费用项行无不含税金额不传) if (priceTaxTotal.compareTo(BigDecimal.ZERO) == 0) {
if (entryGroup.amountNoTax() != null) { throw new ServiceException("推送金蝶收票单失败:发票[" + receipt.getInvoiceNo() + "]分录[" + seq + "]价税合计为 0,无法推送");
entry.setEAmount(kingdeeApBillSyncSupport.money(entryGroup.amountNoTax()));
} }
// 【必填】外部系统来源分录标识(候选键):与应付单分录同键(结算明细行单据号/结算合计费用项拼接),便于逐行勾稽 if (Func.isBlank(item.getGoodsCode())) {
entry.setAwrpEntryCode(entryGroup.entryCode()); throw new ServiceException("推送金蝶收票单失败:发票[" + receipt.getInvoiceNo() + "]分录[" + seq + "]缺少行商品编码,无法确定物料与税收分类编码");
}
BfiReceivedInvoiceSaveEntryDTO entry = new BfiReceivedInvoiceSaveEntryDTO();
entry.setEPricetaxtotal(priceTaxTotal);
// 【必填】数量:明细行数量(≤0/空/不可解析兜底 1)
entry.setQuantity(this.mirrorQuantity(item.getNum()));
// 【必填】单价:明细行单价(空按价税合计/数量反算;单据头 isincludetax=true,单价仅作参考)
entry.setPrice(this.mirrorPrice(item.getUnitPrice(), priceTaxTotal, entry.getQuantity()));
// 【必填】物料编码.编码:行商品编码
entry.setMaterialNumber(item.getGoodsCode().trim());
// 税收分类编码.编码:数电票行商品编码即税收分类编码
entry.setETaxclassNumber(item.getGoodsCode().trim());
// 【必填】税率表.编码:"V"+整数税率(缺失、0/免税行拦截提示人工)
entry.setTaxrateidNumber(this.taxRateCode(receipt.getInvoiceNo(), seq, item.getTaxRate()));
// 金额(不含税)/税额:明细金额/税额(空不送)
if (detailAmount != null) {
entry.setEAmount(kingdeeApBillSyncSupport.money(detailAmount));
}
if (taxAmount != null) {
entry.setETax(kingdeeApBillSyncSupport.money(taxAmount));
}
// 【必填】外部系统来源分录标识(候选键):发票号码_seq,重推同键幂等
entry.setAwrpEntryCode(receipt.getInvoiceNo() + "_" + seq);
entries.add(entry); entries.add(entry);
} }
if (entries.isEmpty()) {
throw new ServiceException("推送金蝶收票单失败:分摊结算单无结算明细与结算合计费用项,无法组装收票单明细");
}
return entries; return entries;
} }
/** 镜像明细行反序列化:items_json 解析异常按脏数据拦截(A801-1 原样落库,理论上可解析) */
private List<BfiInvoicePoolItemVO> parseMirrorItems(String invoiceNo, String itemsJson) {
try {
return Arrays.asList(JsonUtil.parse(itemsJson, BfiInvoicePoolItemVO[].class));
} catch (RuntimeException exception) {
log.warn("收票登记[{}]票池镜像明细行反序列化失败:{}", invoiceNo, exception.getMessage());
throw new ServiceException("推送金蝶收票单失败:发票[" + invoiceNo + "]票池镜像明细行数据异常,请先按票号补查票池明细");
}
}
/** 明细行数量:num ≤0/空/不可解析兜底 1 */
private BigDecimal mirrorQuantity(String num) {
BigDecimal quantity = this.parseMirrorAmountQuietly(num);
if (quantity == null || quantity.compareTo(BigDecimal.ZERO) <= 0) {
return BigDecimal.ONE;
}
return quantity;
}
/** 明细行单价:unitPrice 空/不可解析按 价税合计/数量 反算(2 位小数) */
private BigDecimal mirrorPrice(String unitPrice, BigDecimal priceTaxTotal, BigDecimal quantity) {
BigDecimal price = this.parseMirrorAmountQuietly(unitPrice);
if (price != null && price.compareTo(BigDecimal.ZERO) > 0) {
return price;
}
return priceTaxTotal.divide(quantity, 2, RoundingMode.HALF_UP);
}
/** 镜像税率转税率表编码:"V"+整数税率(13/13%/0.13→V13);缺失、零/免税行拦截提示人工 */
private String taxRateCode(String invoiceNo, String seq, String taxRateText) {
if (Func.isBlank(taxRateText)) {
throw new ServiceException("推送金蝶收票单失败:发票[" + invoiceNo + "]分录[" + seq + "]税率缺失,无法映射税率表,请人工核实");
}
BigDecimal rate;
try {
rate = new BigDecimal(taxRateText.trim().replace("%", ""));
} catch (NumberFormatException exception) {
throw new ServiceException("推送金蝶收票单失败:发票[" + invoiceNo + "]分录[" + seq + "]税率[" + taxRateText.trim() + "]无法识别,请人工核实");
}
if (rate.compareTo(BigDecimal.ZERO) <= 0) {
throw new ServiceException("推送金蝶收票单失败:发票[" + invoiceNo + "]分录[" + seq + "]为免税/零税率行,无法映射税率表,请人工核实");
}
// 小于 1 的小数按比率换算百分比(0.13→13),"1" 按百分比(1%→V1)
if (rate.compareTo(BigDecimal.ONE) < 0) {
rate = rate.multiply(BigDecimal.valueOf(100));
}
return "V" + rate.stripTrailingZeros().toPlainString();
}
/** 镜像明细数值解析:空返回 null,不可解析返回 null(数量/单价有兜底,金额空不送) */
private BigDecimal parseMirrorAmountQuietly(String value) {
if (Func.isBlank(value)) {
return null;
}
try {
return new BigDecimal(value.trim());
} catch (NumberFormatException exception) {
return null;
}
}
/** 镜像明细金额解析:空返回 null,不可解析按脏数据拦截(金额直接进分录,不能静默丢弃) */
private BigDecimal parseMirrorAmount(String invoiceNo, String seq, String fieldLabel, String value) {
if (Func.isBlank(value)) {
return null;
}
try {
return new BigDecimal(value.trim());
} catch (NumberFormatException exception) {
throw new ServiceException("推送金蝶收票单失败:发票[" + invoiceNo + "]分录[" + seq + "]" + fieldLabel + "[" + value.trim() + "]无法解析,请先按票号补查票池明细");
}
}
// ==================== A302-6 收票指定应付 ==================== // ==================== A302-6 收票指定应付 ====================
/** /**