feat: 金蝶票池选票仅出可用票并改造有界循环补页游标分页
- 选票过滤:剩余可用金额availableAmount>0且金额使用状态amountUseStatus=0(未使用),保存落镜像前按同口径复核,防选票到保存之间票被他人使用造成超用 - 单据使用状态不再作为前端条件,后端固定下推未用(expenseStatus=1) - 分页改造:单请求最多3次远程调用循环补页凑满一页,返回nextCursor(远程页码:已消费过滤行数)供下一页续扫,防过滤后翻页漏行重行,游标解析失败降级页码锚定 - 新增剩余可用金额availableTaxAmount展示字段(金蝶可用税额口径,不改可认领金额的价税合计口径)
This commit is contained in:
+9
-9
@@ -73,12 +73,6 @@ public class KingdeeInvoicePoolQueryDTO implements Serializable {
|
||||
@Schema(description = "开票日期止 yyyy-MM-dd")
|
||||
private String invoiceDateEnd;
|
||||
|
||||
/**
|
||||
* 发票类型编码(票易通字典,如 26/27)
|
||||
*/
|
||||
@Schema(description = "发票类型编码(票易通字典,如 26/27)")
|
||||
private String invoiceType;
|
||||
|
||||
/**
|
||||
* 销方税号,精确
|
||||
*/
|
||||
@@ -92,8 +86,14 @@ public class KingdeeInvoicePoolQueryDTO implements Serializable {
|
||||
private String buyerTaxNo;
|
||||
|
||||
/**
|
||||
* 单据使用状态[1未用,30在用,60已用,65已入账],空不限(A801-1 原生条件)
|
||||
* 发票类型编码(票易通字典,如 26/27)
|
||||
*/
|
||||
@Schema(description = "单据使用状态[1未用,30在用,60已用,65已入账]")
|
||||
private String expenseStatus;
|
||||
@Schema(description = "发票类型编码(票易通字典,如 26/27)")
|
||||
private String invoiceType;
|
||||
|
||||
/**
|
||||
* 续扫游标:下一页翻页时原样回传上页返回的 nextCursor,空表示按页码锚定重新扫描
|
||||
*/
|
||||
@Schema(description = "续扫游标(下一页请求回传上页 nextCursor)")
|
||||
private String cursor;
|
||||
}
|
||||
|
||||
+5
-1
@@ -56,7 +56,11 @@ public class KingdeeInvoicePoolPageVO implements Serializable {
|
||||
@Schema(description = "每页条数")
|
||||
private Long size;
|
||||
|
||||
/** 是否还有下一页(服务端多取 1 条探测) */
|
||||
/** 是否还有下一页(服务端有界补页扫描判断) */
|
||||
@Schema(description = "是否还有下一页")
|
||||
private Boolean hasMore;
|
||||
|
||||
/** 下一页续扫游标("远程页码:已消费过滤行数"),null 表示没有更多;翻下一页时原样回传 */
|
||||
@Schema(description = "下一页续扫游标,null 表示没有更多")
|
||||
private String nextCursor;
|
||||
}
|
||||
|
||||
+4
@@ -139,6 +139,10 @@ public class KingdeeInvoicePoolVO implements Serializable {
|
||||
@Schema(description = "可认领金额")
|
||||
private BigDecimal availableInvoiceAmount;
|
||||
|
||||
/** 剩余可用金额(金蝶可用税额口径,与可认领金额的价税合计口径不同) */
|
||||
@Schema(description = "剩余可用金额")
|
||||
private BigDecimal availableTaxAmount;
|
||||
|
||||
/** 默认认领金额,口径同发票金额 */
|
||||
@Schema(description = "默认认领金额")
|
||||
private BigDecimal matchedAmount;
|
||||
|
||||
+12
@@ -182,6 +182,18 @@ public final class KingdeeBfiConstant {
|
||||
/** A103-2 单据状态:已关闭。 */
|
||||
public static final String BFI_BILL_STATUS_CLOSED = "D";
|
||||
|
||||
/** A801-1 查询限定发票状态:0 正常(票易通字典),作废/异常票不进镜像、不可补查 */
|
||||
public static final String POOL_INVOICE_STATUS_NORMAL = "0";
|
||||
|
||||
/** 选票金额使用状态可选值:仅保留剩余可用金额>0 且金额使用状态为 0 的可用票,2-全部使用 */
|
||||
public static final String POOL_AMOUNT_USE_STATUS_SELECTABLE = "0";
|
||||
|
||||
/** 单请求远程补页调用上限:金额可用过滤在拉取后进行,过滤率过低时以此封顶防请求无限拉长 */
|
||||
public static final int POOL_MAX_REMOTE_CALLS = 3;
|
||||
|
||||
/** 选票单据使用状态固定为未用(1,票易通字典):仅未使用的票可被认领/登记,前端不提供该筛选条件 */
|
||||
public static final String POOL_EXPENSE_STATUS_UNUSED = "1";
|
||||
|
||||
/**
|
||||
* 按中文名称从映射字典转换单据类型编码。
|
||||
*
|
||||
|
||||
+137
-46
@@ -110,9 +110,6 @@ public class InvoiceReceiptServiceImpl extends BaseServiceImpl<InvoiceReceiptMap
|
||||
/** A801-1 rim 契约单页最大条数 */
|
||||
private static final int POOL_PAGE_SIZE = 500;
|
||||
|
||||
/** A801-1 查询限定发票状态:0 正常(票易通字典),作废/异常票不进镜像、不可补查 */
|
||||
private static final String POOL_INVOICE_STATUS_NORMAL = "0";
|
||||
|
||||
private final KingdeeInvoicePoolMapper invoicePoolMapper;
|
||||
private final InvoiceReceiptSettlementMapper settlementRelationMapper;
|
||||
private final InvoiceReceiptRecordMapper recordMapper;
|
||||
@@ -177,44 +174,127 @@ public class InvoiceReceiptServiceImpl extends BaseServiceImpl<InvoiceReceiptMap
|
||||
long size = query.getSize() == null ? 10L : Math.min(Math.max(query.getSize(), 1L), 100L);
|
||||
// 查询票号(已 trim 归一,方法级可见):用于对远程模糊命中做全等防御
|
||||
String queryInvoiceNo = trimOrNull(query.getInvoiceNo());
|
||||
BfiInvoicePoolQueryDataDTO data = new BfiInvoicePoolQueryDataDTO();
|
||||
data.setPageNo((int) current);
|
||||
// A801-1 无分页总数:多取 1 条探测下一页,返回前截断
|
||||
data.setPageSize((int) size + 1);
|
||||
// 固定只认正常状态票,作废/异常票不进选票结果
|
||||
data.setInvoiceStatus(POOL_INVOICE_STATUS_NORMAL);
|
||||
data.setInvoiceNo(queryInvoiceNo);
|
||||
data.setSalerTaxNo(trimOrNull(query.getSalerTaxNo()));
|
||||
data.setBuyerTaxNo(trimOrNull(query.getBuyerTaxNo()));
|
||||
data.setInvoiceType(trimOrNull(query.getInvoiceType()));
|
||||
data.setExpenseStatus(trimOrNull(query.getExpenseStatus()));
|
||||
data.setInvoiceDateStart(normalizeQueryDate(query.getInvoiceDateStart()));
|
||||
data.setInvoiceDateEnd(normalizeQueryDate(query.getInvoiceDateEnd()));
|
||||
BfiInvoicePoolQueryDTO param = new BfiInvoicePoolQueryDTO();
|
||||
param.setQueryParam(data);
|
||||
BfiPageVO<BfiInvoicePoolVO> poolPage = bfiService.queryInvoicePool(param);
|
||||
// WHY: hasMore 以远程原始行数为准,防被过滤行(空票号/模糊命中)造成末页假阴性
|
||||
int rawCount = poolPage.getRows() == null ? 0 : poolPage.getRows().size();
|
||||
boolean hasMore = rawCount > size;
|
||||
List<BfiInvoicePoolVO> rows = Optional.ofNullable(poolPage.getRows()).orElse(List.of()).stream()
|
||||
.filter(row -> StringUtil.isNotBlank(row.getInvoiceNo()))
|
||||
// 按票号查询时全等防御:远程为模糊匹配,命中"包含"关系的其他票号不得混入选票结果
|
||||
.filter(row -> queryInvoiceNo == null || queryInvoiceNo.equals(row.getInvoiceNo().trim()))
|
||||
.toList();
|
||||
// 过滤后行数可能小于 size(空票号/模糊命中剔除),截断下标须夹取到过滤后列表长度,防 toIndex 越界
|
||||
int end = (int) Math.min(size, rows.size());
|
||||
List<BfiInvoicePoolVO> pageRows = hasMore ? rows.subList(0, end) : rows;
|
||||
// WHY: 金额可用过滤发生在拉取之后,下一页必须按游标续扫才能跨请求对齐过滤进度,按页码锚定会漏行/重行
|
||||
PoolScanCursor cursor = PoolScanCursor.parse(query.getCursor());
|
||||
List<BfiInvoicePoolVO> window = new ArrayList<>();
|
||||
// scanPageNo 指向待拉取的远程页;游标续扫固定按 500 行窗口取数,保证"页码:过滤偏移"语义稳定
|
||||
long scanPageNo = cursor == null ? current : cursor.pageNo();
|
||||
int skip = cursor == null ? 0 : cursor.skip();
|
||||
boolean exhausted = false;
|
||||
// 末次拉取前的过滤累计行数与末次拉取页码:未消费的过滤行只可能落在末次拉取页内
|
||||
int filteredBeforeLastFetch = 0;
|
||||
long lastFetchPageNo = scanPageNo;
|
||||
int remoteCalls = 0;
|
||||
// 有界循环补页:单请求最多 POOL_MAX_REMOTE_CALLS 次远程调用凑满一页,仍不足则返回稀疏页并给出游标
|
||||
for (int call = 0; call < KingdeeBfiConstant.POOL_MAX_REMOTE_CALLS && window.size() - skip < size; call++) {
|
||||
// 首拉按 size+1 轻量探测,补拉按契约上限 500 行以减少调用次数
|
||||
int fetchSize = call == 0 && cursor == null ? (int) size + 1 : POOL_PAGE_SIZE;
|
||||
List<BfiInvoicePoolVO> rawRows = this.queryPoolPage(scanPageNo, fetchSize, query);
|
||||
remoteCalls++;
|
||||
List<BfiInvoicePoolVO> usableRows = rawRows.stream()
|
||||
.filter(this::isUsablePoolRow)
|
||||
// 按票号查询时全等防御:远程为模糊匹配,命中"包含"关系的其他票号不得混入选票结果
|
||||
.filter(row -> queryInvoiceNo == null || queryInvoiceNo.equals(row.getInvoiceNo().trim()))
|
||||
.toList();
|
||||
filteredBeforeLastFetch = window.size();
|
||||
window.addAll(usableRows);
|
||||
lastFetchPageNo = scanPageNo;
|
||||
// WHY: A801-1 无总数,远程短页是唯一耗尽信号
|
||||
if (rawRows.size() < fetchSize) {
|
||||
exhausted = true;
|
||||
break;
|
||||
}
|
||||
scanPageNo++;
|
||||
}
|
||||
// 按游标偏移切页:skip 为游标窗口内已消费的过滤行数
|
||||
int fromIndex = Math.min(skip, window.size());
|
||||
int end = Math.min(fromIndex + (int) size, window.size());
|
||||
List<BfiInvoicePoolVO> pageRows = window.subList(fromIndex, end);
|
||||
// 末次拉取页内已消费的过滤行数:翻页游标锚定在该页,页内剩余行下页续用
|
||||
int consumedInLastPage = Math.max(0, end - Math.max(fromIndex, filteredBeforeLastFetch));
|
||||
boolean hasMore = !exhausted || end < window.size();
|
||||
List<KingdeeInvoicePoolVO> records = pageRows.stream().map(this::toPoolViewVO).toList();
|
||||
decorateUseFlags(records);
|
||||
this.decorateUseFlags(records);
|
||||
KingdeeInvoicePoolPageVO result = new KingdeeInvoicePoolPageVO();
|
||||
result.setCurrent(current);
|
||||
result.setSize(size);
|
||||
result.setHasMore(hasMore);
|
||||
result.setRecords(records);
|
||||
log.info("金蝶票池实时查询 第{}页 {}条/页 返回{}条 hasMore:{}", current, size, records.size(), hasMore);
|
||||
result.setNextCursor(hasMore ? PoolScanCursor.encode(lastFetchPageNo, consumedInLastPage) : null);
|
||||
log.info("金蝶票池实时查询 第{}页 {}条/页 返回{}条 hasMore:{} 远程调用{}次 游标:{}", current, size,
|
||||
records.size(), hasMore, remoteCalls, result.getNextCursor());
|
||||
return result;
|
||||
}
|
||||
|
||||
/** A801-1 按页拉取原始行:查询条件清洗与选票口径集中在此,循环补页多次复用 */
|
||||
private List<BfiInvoicePoolVO> queryPoolPage(long pageNo, int pageSize, KingdeeInvoicePoolQueryDTO query) {
|
||||
BfiInvoicePoolQueryDataDTO data = new BfiInvoicePoolQueryDataDTO();
|
||||
data.setPageNo((int) pageNo);
|
||||
data.setPageSize(pageSize);
|
||||
// 固定只认正常状态票,作废/异常票不进选票结果
|
||||
data.setInvoiceStatus(KingdeeBfiConstant.POOL_INVOICE_STATUS_NORMAL);
|
||||
data.setInvoiceNo(trimOrNull(query.getInvoiceNo()));
|
||||
data.setSalerTaxNo(trimOrNull(query.getSalerTaxNo()));
|
||||
data.setBuyerTaxNo(trimOrNull(query.getBuyerTaxNo()));
|
||||
data.setInvoiceType(trimOrNull(query.getInvoiceType()));
|
||||
// 单据使用状态固定查未用,不走前端条件
|
||||
data.setExpenseStatus(KingdeeBfiConstant.POOL_EXPENSE_STATUS_UNUSED);
|
||||
data.setInvoiceDateStart(normalizeQueryDate(query.getInvoiceDateStart()));
|
||||
data.setInvoiceDateEnd(normalizeQueryDate(query.getInvoiceDateEnd()));
|
||||
BfiInvoicePoolQueryDTO param = new BfiInvoicePoolQueryDTO();
|
||||
param.setQueryParam(data);
|
||||
BfiPageVO<BfiInvoicePoolVO> poolPage = bfiService.queryInvoicePool(param);
|
||||
return Optional.ofNullable(poolPage.getRows()).orElse(List.of());
|
||||
}
|
||||
|
||||
/**
|
||||
* 选票可用过滤(选票弹窗与保存落镜像同口径):票号非空 + 剩余可用金额>0 + 金额使用状态为 0(未使用);
|
||||
* 可用金额空白/解析失败由 parseAmount 按 0 兜底,即无法证明可用视为不可用
|
||||
*/
|
||||
private boolean isUsablePoolRow(BfiInvoicePoolVO row) {
|
||||
if (row == null || StringUtil.isBlank(row.getInvoiceNo())) {
|
||||
return false;
|
||||
}
|
||||
String amountUseStatus = row.getAmountUseStatus();
|
||||
if (!KingdeeBfiConstant.POOL_AMOUNT_USE_STATUS_SELECTABLE.equals(amountUseStatus == null ? null : amountUseStatus.trim())) {
|
||||
return false;
|
||||
}
|
||||
return parseAmount(row.getInvoiceNo(), row.getAvailableAmount()).compareTo(BigDecimal.ZERO) > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 票池扫描游标("远程页码:已消费过滤行数"):过滤发生在拉取之后,翻页按游标续扫才能跨请求对齐过滤进度;
|
||||
* 游标页码统一按 500 行窗口定位,解析失败降级为按页码锚定(仅存在翻页错位风险,不阻断查询)
|
||||
*/
|
||||
private record PoolScanCursor(long pageNo, int skip) {
|
||||
|
||||
static PoolScanCursor parse(String text) {
|
||||
if (StringUtil.isBlank(text)) {
|
||||
return null;
|
||||
}
|
||||
String[] parts = text.trim().split(":");
|
||||
if (parts.length != 2) {
|
||||
log.warn("金蝶票池续扫游标格式非法,忽略后按页码锚定,游标={}", text);
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
long pageNo = Long.parseLong(parts[0].trim());
|
||||
int skip = Integer.parseInt(parts[1].trim());
|
||||
if (pageNo < 1 || skip < 0) {
|
||||
throw new NumberFormatException("游标数值越界");
|
||||
}
|
||||
return new PoolScanCursor(pageNo, skip);
|
||||
} catch (NumberFormatException exception) {
|
||||
log.warn("金蝶票池续扫游标解析失败,忽略后按页码锚定,游标={}", text);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
static String encode(long pageNo, int skip) {
|
||||
return pageNo + ":" + skip;
|
||||
}
|
||||
}
|
||||
|
||||
/** 空白串归一为 null,便于远程查询条件与 MyBatis-Plus 判空 */
|
||||
private String trimOrNull(String text) {
|
||||
return StringUtil.isBlank(text) ? null : text.trim();
|
||||
@@ -248,6 +328,8 @@ public class InvoiceReceiptServiceImpl extends BaseServiceImpl<InvoiceReceiptMap
|
||||
vo.setInvoiceAmount(totalAmount);
|
||||
vo.setAvailableInvoiceAmount(totalAmount);
|
||||
vo.setMatchedAmount(totalAmount);
|
||||
// 剩余可用金额为金蝶可用税额口径,仅用于选票展示,与可认领金额的价税合计口径不同
|
||||
vo.setAvailableTaxAmount(parseAmount(row.getInvoiceNo(), row.getAvailableAmount()));
|
||||
vo.setReceiverName(row.getBuyerName());
|
||||
vo.setBuyerTaxNo(row.getBuyerTaxNo());
|
||||
vo.setIssuerName(row.getSalerName());
|
||||
@@ -629,14 +711,18 @@ public class InvoiceReceiptServiceImpl extends BaseServiceImpl<InvoiceReceiptMap
|
||||
log.info("收票登记[{}]金蝶收票单已生成(单据编号[{}]),幂等跳过 A303-2 保存续推指定", receipt.getInvoiceNo(), billNo);
|
||||
} else {
|
||||
try {
|
||||
// 镜像缺明细行/票种编码(入镜像早于字段上线或滑出增量窗口):按票号窄查 A801-1 回填补写,镜像入池时已过白名单,回填不再复检
|
||||
if (Func.isBlank(plan.mirror().getItemsJson()) || Func.isBlank(plan.mirror().getInvoiceTypeCode())) {
|
||||
log.info("收票登记[{}]镜像缺明细/票种编码,按票号回填 A801-1", receipt.getInvoiceNo());
|
||||
// 镜像缺票种编码(入镜像早于字段上线或滑出增量窗口):按票号窄查 A801-1 回填补写;
|
||||
// 明细分录已与应付单同源取自结算货物费用行,票池明细行仅入池存档、不再作为分录取数来源
|
||||
if (Func.isBlank(plan.mirror().getInvoiceTypeCode())) {
|
||||
log.info("收票登记[{}]镜像缺票种编码,按票号回填 A801-1", receipt.getInvoiceNo());
|
||||
fetchMirrorItemsFromPool(plan.mirror());
|
||||
}
|
||||
log.info("收票登记[{}]开始 A303-2 推送金蝶收票单", receipt.getInvoiceNo());
|
||||
ContractManage contract = kingdeeApBillSyncSupport.requireContract(plan.settlement().getContractId(), context);
|
||||
billNo = kingdeeReceivedInvoiceSyncSupport.pushReceivedInvoice(receipt, plan.mirror(), contract, context);
|
||||
// 明细分录与应付单 A102-2 同源同口径:复用同一结算明细解析结果,逐结算货物费用行一条
|
||||
List<KingdeeApBillSyncSupport.BfiEntryGroup> entryGroups =
|
||||
kingdeeApBillSyncSupport.resolveEntryGroups(plan.details(), context);
|
||||
billNo = kingdeeReceivedInvoiceSyncSupport.pushReceivedInvoice(receipt, plan.mirror(), contract, entryGroups, context);
|
||||
log.info("收票登记[{}]A303-2 推送成功:金蝶收票单[{}]", receipt.getInvoiceNo(), billNo);
|
||||
} catch (RuntimeException pushException) {
|
||||
log.warn("收票登记[{}]A303-2 推送失败:{}", receipt.getInvoiceNo(), pushException.getMessage());
|
||||
@@ -672,21 +758,21 @@ public class InvoiceReceiptServiceImpl extends BaseServiceImpl<InvoiceReceiptMap
|
||||
Map<Long, List<FormalSettlementDetail>> detailsBySettlement) {
|
||||
String pushStatus = Func.toStrWithEmpty(receipt.getKingdeePushStatus(), KingdeeBfiConstant.RECEIVED_PUSH_UNSYNCED);
|
||||
if (!TransportBusiConstant.APPROVED.equals(receipt.getApprovalStatus())) {
|
||||
return new ReceivedInvoicePushPlan(receipt, null, null, null, "仅审批通过的收票登记允许推送金蝶");
|
||||
return new ReceivedInvoicePushPlan(receipt, null, null, null, null, "仅审批通过的收票登记允许推送金蝶");
|
||||
}
|
||||
if (KingdeeBfiConstant.RECEIVED_PUSH_ASSIGNED.equals(pushStatus)) {
|
||||
return new ReceivedInvoicePushPlan(receipt, null, null, null, "收票单已指定应付,无需重复推送");
|
||||
return new ReceivedInvoicePushPlan(receipt, null, null, null, null, "收票单已指定应付,无需重复推送");
|
||||
}
|
||||
KingdeeInvoicePool mirror = receipt.getKingdeeInvoicePoolId() == null ? null
|
||||
: mirrorById.get(receipt.getKingdeeInvoicePoolId());
|
||||
if (mirror == null) {
|
||||
return new ReceivedInvoicePushPlan(receipt, null, null, null, "收票登记未绑定票池镜像,请先完成同步绑定");
|
||||
return new ReceivedInvoicePushPlan(receipt, null, null, null, null, "收票登记未绑定票池镜像,请先完成同步绑定");
|
||||
}
|
||||
if ("2".equals(mirror.getCheckStatus())) {
|
||||
return new ReceivedInvoicePushPlan(receipt, mirror, null, null, "金蝶发票查验不通过,请先人工核实票池发票");
|
||||
return new ReceivedInvoicePushPlan(receipt, mirror, null, null, null, "金蝶发票查验不通过,请先人工核实票池发票");
|
||||
}
|
||||
if (relations.size() != 1 || relations.get(0).getFormalSettlementId() == null) {
|
||||
return new ReceivedInvoicePushPlan(receipt, mirror, null, null,
|
||||
return new ReceivedInvoicePushPlan(receipt, mirror, null, null, null,
|
||||
"金蝶不支持一张发票指定多张应付单,收票登记须恰好分摊一张应付正式结算单");
|
||||
}
|
||||
FormalSettlement settlement = settlementById.get(relations.get(0).getFormalSettlementId());
|
||||
@@ -694,14 +780,14 @@ public class InvoiceReceiptServiceImpl extends BaseServiceImpl<InvoiceReceiptMap
|
||||
|| !Objects.equals(settlement.getStatus(), 1)
|
||||
|| !TransportBusiConstant.APPROVED.equals(settlement.getApprovalStatus())
|
||||
|| !"payable".equals(settlement.getSettlementType())) {
|
||||
return new ReceivedInvoicePushPlan(receipt, mirror, settlement, null, "分摊结算单不是审批通过的应付正式结算单");
|
||||
return new ReceivedInvoicePushPlan(receipt, mirror, settlement, null, null, "分摊结算单不是审批通过的应付正式结算单");
|
||||
}
|
||||
List<FormalSettlementDetail> details = detailsBySettlement.getOrDefault(settlement.getId(), List.of());
|
||||
if (details.isEmpty()) {
|
||||
return new ReceivedInvoicePushPlan(receipt, mirror, settlement, null,
|
||||
return new ReceivedInvoicePushPlan(receipt, mirror, settlement, null, null,
|
||||
"结算单" + settlement.getFormalSettlementNo() + "无结算明细,无法组装应付单防重键");
|
||||
}
|
||||
return new ReceivedInvoicePushPlan(receipt, mirror, settlement,
|
||||
return new ReceivedInvoicePushPlan(receipt, mirror, settlement, details,
|
||||
kingdeeApBillSyncSupport.apBillSrcCode(settlement, details), null);
|
||||
}
|
||||
|
||||
@@ -771,11 +857,12 @@ public class InvoiceReceiptServiceImpl extends BaseServiceImpl<InvoiceReceiptMap
|
||||
* @param receipt 收票登记
|
||||
* @param mirror 绑定的票池镜像(可为 null)
|
||||
* @param settlement 唯一分摊的应付正式结算单(可为 null)
|
||||
* @param details 结算单结算明细(防重键组装与 A303-2 分录解析共用,可为 null)
|
||||
* @param apBillSrcCode 财务应付单防重键(无法组装时为 null)
|
||||
* @param blockedReason 前置拦截原因
|
||||
*/
|
||||
private record ReceivedInvoicePushPlan(InvoiceReceipt receipt, KingdeeInvoicePool mirror,
|
||||
FormalSettlement settlement, String apBillSrcCode, String blockedReason) {
|
||||
FormalSettlement settlement, List<FormalSettlementDetail> details, String apBillSrcCode, String blockedReason) {
|
||||
}
|
||||
|
||||
// ==================== A801-3 收票结果定时回写(异常回收/指定补偿/单号补登) ====================
|
||||
@@ -923,7 +1010,7 @@ public class InvoiceReceiptServiceImpl extends BaseServiceImpl<InvoiceReceiptMap
|
||||
data.setPageSize(POOL_PAGE_SIZE);
|
||||
data.setInvoiceNo(invoiceNo.trim());
|
||||
// 只认发票状态正常的票,作废/异常票不再进入镜像
|
||||
data.setInvoiceStatus(POOL_INVOICE_STATUS_NORMAL);
|
||||
data.setInvoiceStatus(KingdeeBfiConstant.POOL_INVOICE_STATUS_NORMAL);
|
||||
BfiInvoicePoolQueryDTO param = new BfiInvoicePoolQueryDTO();
|
||||
param.setQueryParam(data);
|
||||
BfiPageVO<BfiInvoicePoolVO> page = bfiService.queryInvoicePool(param);
|
||||
@@ -950,6 +1037,10 @@ public class InvoiceReceiptServiceImpl extends BaseServiceImpl<InvoiceReceiptMap
|
||||
if (poolRow == null) {
|
||||
throw new ServiceException("金蝶票池未查询到发票 " + trimmedInvoiceNo + ",不能保存");
|
||||
}
|
||||
// 保存防御:选票到保存之间票可能被他人认领/使用,落镜像前按选票同口径复核可用性
|
||||
if (!this.isUsablePoolRow(poolRow)) {
|
||||
throw new ServiceException("发票 " + trimmedInvoiceNo + " 剩余可用金额为0或金额使用状态已变更,不能保存");
|
||||
}
|
||||
return upsertInvoicePool(existing, poolRow);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user