feat: 开票申请金蝶推送支持发票张次拆分与幂等重推,回写金蝶单号

This commit is contained in:
刘泉佳
2026-09-24 19:42:48 +08:00
parent e4095c6e60
commit 851deb2959
@@ -509,25 +509,31 @@ public class InvoiceApplicationServiceImpl extends BaseServiceImpl<InvoiceApplic
+ "]财务应收单未在金蝶生成,请先在正式付款单页面同步金蝶");
}
}
// A303-4 幂等预检:按申请单号(billno 候选键)查询金蝶是否已生成开票申请单(防上次推送成功但本地回滚的断链重推)
// 加载发票张次与商品行:每张发票张次对应一张金蝶开票申请单(按张次拆分开票)
List<InvoiceApplicationSheet> sheets = sheetMapper.selectList(Wrappers.<InvoiceApplicationSheet>lambdaQuery()
.eq(InvoiceApplicationSheet::getInvoiceApplicationId, id).orderByAsc(InvoiceApplicationSheet::getSheetNo));
if (Func.isEmpty(sheets)) {
throw new ServiceException("推送金蝶开票申请失败:开票申请无发票张次");
}
Map<Long, List<InvoiceApplicationLine>> linesBySheetId = lineMapper.selectList(Wrappers.<InvoiceApplicationLine>lambdaQuery()
.eq(InvoiceApplicationLine::getInvoiceApplicationId, id)).stream()
.collect(Collectors.groupingBy(InvoiceApplicationLine::getInvoiceSheetId));
// 每张发票的金蝶业务单据编号(A303-1 billno 候选键,须唯一):单张=开票申请单号(兼容存量),多张=开票申请单号-张次序号
List<String> sheetBillNos = sheets.stream().map(sheet -> this.sheetBillNo(entity, sheet, sheets.size())).toList();
// A303-4 幂等预检:按全部候选 billno 查询金蝶是否已生成开票申请单(防上次推送成功但本地回滚的断链重推),
// 已生成的张次幂等跳过,未生成的张次重新推送
BfiInvoiceApplyQueryDTO idempotentQuery = new BfiInvoiceApplyQueryDTO();
idempotentQuery.setPageNo(1);
idempotentQuery.setPageSize(10);
idempotentQuery.setBillno(List.of(entity.getApplicationNo()));
List<BfiInvoiceApplyVO> existApplies = List.of();
idempotentQuery.setPageSize(Math.max(sheets.size(), 10));
idempotentQuery.setBillno(sheetBillNos);
Set<String> existSheetBillNos = new LinkedHashSet<>();
BfiPageVO<BfiInvoiceApplyVO> existPage = bfiService.queryInvoiceApply(idempotentQuery);
if (existPage != null && Func.isNotEmpty(existPage.getRows())) {
existApplies = existPage.getRows();
}
for (BfiInvoiceApplyVO existApply : existApplies) {
if (existApply != null && entity.getApplicationNo().equals(existApply.getBillno())) {
// 金蝶已生成:直接回填视为同步成功(勾稽纠偏)
entity.setKingdeeBillNo(existApply.getBillno());
entity.setKingdeeStatus(KingdeeBfiConstant.KINGDEE_STATUS_SYNCED);
entity.setSyncedTime(LocalDateTime.now());
updateById(entity);
record(entity.getId(), "sync", "同步金蝶(勾稽回填)", TransportBusiConstant.APPROVED, TransportBusiConstant.APPROVED, null, existApply.getBillno());
return existApply.getBillno();
for (BfiInvoiceApplyVO existApply : existPage.getRows()) {
if (existApply != null && Func.isNotBlank(existApply.getBillno())
&& sheetBillNos.contains(existApply.getBillno())) {
existSheetBillNos.add(existApply.getBillno());
}
}
}
// 组装 A303-1 开票申请单(字段映射见设计文档 6.3/6.4)
@@ -564,16 +570,92 @@ public class InvoiceApplicationServiceImpl extends BaseServiceImpl<InvoiceApplic
if (customer == null || Func.isBlank(customer.getCustomerCode())) {
throw new ServiceException("推送金蝶开票申请失败:受票方客户未在客商档案中维护客商编码,请补充后重试");
}
// 单据体分录:逐「开票内容」商品行一条,取数口径即页面开票内容行(金额/税率透传行值)
List<BfiInvoiceApplySaveEntryDTO> items = this.resolveInvoiceApplyEntries(entity);
// 逐张发票推送金蝶开票申请单:每张发票张次一张(价税合计=本张金额,分录=本张商品行),已存在的张次幂等跳过
List<String> savedBillNos = new ArrayList<>();
List<InvoiceApplicationSheet> sheetsToUpdate = new ArrayList<>();
boolean pushedNew = false;
for (InvoiceApplicationSheet sheet : sheets) {
String sheetBillNo = this.sheetBillNo(entity, sheet, sheets.size());
if (existSheetBillNos.contains(sheetBillNo)) {
// 金蝶已生成:本张幂等回填(勾稽纠偏),不重复推送
sheet.setKingdeeBillNo(sheetBillNo);
savedBillNos.add(sheetBillNo);
log.info("金蝶开票申请已生成:开票申请[{}]发票张[{}],billno=[{}],幂等跳过推送",
entity.getApplicationNo(), sheet.getSheetNo(), sheetBillNo);
sheetsToUpdate.add(sheet);
continue;
}
// 本张商品行分录:逐「开票内容」商品行一条(金额/税率透传行值,张内行号升序)
List<BfiInvoiceApplySaveEntryDTO> items = linesBySheetId.getOrDefault(sheet.getId(), List.of()).stream()
.sorted(Comparator.comparing(InvoiceApplicationLine::getLineNo,
Comparator.nullsLast(Comparator.naturalOrder())))
.map(this::buildInvoiceApplyEntry)
.toList();
if (Func.isEmpty(items)) {
throw new ServiceException("推送金蝶开票申请失败:发票张[" + Func.toStr(sheet.getSheetNo())
+ "]无商品行,无法组装分录");
}
BfiInvoiceApplySaveDTO param = this.buildInvoiceApplyParam(entity, sheetBillNo, seller, salerAddr, salerBank,
dept, customer, money(sheet.getInvoiceAmount()), items);
log.info("金蝶开票申请推送:开票申请[{}]发票张[{}],billno=[{}],价税合计=[{}],分录{}条",
entity.getApplicationNo(), sheet.getSheetNo(), sheetBillNo, sheet.getInvoiceAmount(), items.size());
BfiBillSaveResultVO saveResult = bfiService.saveInvoiceApply(param);
kingdeeApBillSyncSupport.requireBfiSaveSuccess(saveResult,
"推送金蝶开票申请发票张[" + Func.toStr(sheet.getSheetNo()) + "]");
String kingdeeNo = saveResult.getResult().get(0).getNumber();
sheet.setKingdeeBillNo(kingdeeNo);
savedBillNos.add(kingdeeNo);
pushedNew = true;
sheetsToUpdate.add(sheet);
}
if (Func.isEmpty(savedBillNos)) {
throw new ServiceException("推送金蝶开票申请失败:无发票张次可推送");
}
// 批量回写每张发票张次的金蝶单号,避免逐张单次更新(N+1)
if (!sheetsToUpdate.isEmpty()) {
Db.updateBatchById(sheetsToUpdate);
}
String joinedBillNos = String.join(",", savedBillNos);
entity.setKingdeeBillNo(joinedBillNos);
entity.setKingdeeStatus(KingdeeBfiConstant.KINGDEE_STATUS_SYNCED);
entity.setSyncedTime(LocalDateTime.now());
updateById(entity);
record(entity.getId(), "sync", pushedNew ? "同步金蝶" : "同步金蝶(勾稽回填)",
TransportBusiConstant.APPROVED, TransportBusiConstant.APPROVED, null, joinedBillNos);
return joinedBillNos;
}
/**
* 单张发票的金蝶业务单据编号(A303-1 billno 候选键,须唯一):
* 单张发票=开票申请单号(兼容存量推送口径),多张发票=开票申请单号-张次序号
*
* @param entity 开票申请
* @param sheet 发票张次
* @param sheetCount 发票张次数
* @return 金蝶业务单据编号
*/
private String sheetBillNo(InvoiceApplication entity, InvoiceApplicationSheet sheet, int sheetCount) {
if (sheetCount <= 1) {
return entity.getApplicationNo();
}
return entity.getApplicationNo() + "-" + sheet.getSheetNo();
}
/**
* 组装 A303-1 单张开票申请单入参(字段映射见设计文档 6.3/6.4),
* 购销方等主表公共字段随开票申请取数,价税合计与单据体分录取本张发票
*/
private BfiInvoiceApplySaveDTO buildInvoiceApplyParam(InvoiceApplication entity, String billNo,
CustomerArchive seller, String salerAddr, String salerBank, Dept dept, CustomerArchive customer,
BigDecimal sheetAmount, List<BfiInvoiceApplySaveEntryDTO> items) {
BfiInvoiceApplySaveDTO param = new BfiInvoiceApplySaveDTO();
// ==================== 以下为【必填】字段(BfiInvoiceApplySaveDTO 标注),非必填字段先注释保留 ====================
// 【必填】组织.编码:申请部门 deptId → 部门编码
param.setOrgidNumber(dept.getDeptCode());
// 【必填】申请部门.编码:同上
param.setAwrpOrgfieldNumber(dept.getDeptCode());
// 【必填】业务单据编号(候选键):开票申请单号
param.setBillno(entity.getApplicationNo());
// 【必填】业务单据编号(候选键):单张发票候选 billno(多张发票时开票申请单号-张次序号)
param.setBillno(billNo);
// 【必填】单据日期:申请日期 yyyy-MM-dd HH:mm:ss
LocalDate billDate = entity.getApplicationDate() == null ? LocalDate.now() : entity.getApplicationDate();
param.setBilldate(billDate.atStartOfDay().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
@@ -608,8 +690,8 @@ public class InvoiceApplicationServiceImpl extends BaseServiceImpl<InvoiceApplic
// 销方开户行及账号:数电专票强制校验(联调确认),客商开票信息开户行+银行账号;同步打开票面展示开关
param.setSalerbank(salerBank);
param.setShowsalerbank(true);
// 【必填】申请价税合计:开票金额
param.setTotalamount(money(entity.getInvoiceAmount()));
// 【必填】申请价税合计:本张发票开票金额
param.setTotalamount(sheetAmount);
// 是否含税:数电普票传1(含税),其他传0(不含税)
param.setHsbz("electronic_general".equals(entity.getInvoiceType()) ? "1" : "0");
// 【必填】客户.编码:受票方客商档案客商编码
@@ -630,42 +712,9 @@ public class InvoiceApplicationServiceImpl extends BaseServiceImpl<InvoiceApplic
// 数据来源系统
param.setSystemsource(bfiProperties.getSrcSys());
// 单据体分录:逐「开票内容」商品行一条(联调确认金蝶强校验「单据体分录不能为空」,实际必传)
// 单据体分录:本张发票「开票内容」商品行(联调确认金蝶强校验「单据体分录不能为空」,实际必传)
param.setSimOriginalBillItem(items);
BfiBillSaveResultVO saveResult = bfiService.saveInvoiceApply(param);
kingdeeApBillSyncSupport.requireBfiSaveSuccess(saveResult, "推送金蝶开票申请");
String kingdeeNo = saveResult.getResult().get(0).getNumber();
entity.setKingdeeBillNo(kingdeeNo);
entity.setKingdeeStatus(KingdeeBfiConstant.KINGDEE_STATUS_SYNCED);
entity.setSyncedTime(LocalDateTime.now());
updateById(entity);
record(entity.getId(), "sync", "同步金蝶", TransportBusiConstant.APPROVED, TransportBusiConstant.APPROVED, null, kingdeeNo);
return kingdeeNo;
}
/**
* A303-1 单据体分录组装:逐「开票内容」商品行一条(按发票张次、张内行号平铺),
* 商品和服务分类、货物或服务简称、税收分类编码、数量、单位、税率、金额、税额、备注全部取商品行落库值,
* 不再回查开票项目主数据与合同计费规则
*/
private List<BfiInvoiceApplySaveEntryDTO> resolveInvoiceApplyEntries(InvoiceApplication entity) {
List<InvoiceApplicationSheet> sheets = sheetMapper.selectList(Wrappers.<InvoiceApplicationSheet>lambdaQuery()
.eq(InvoiceApplicationSheet::getInvoiceApplicationId, entity.getId()).orderByAsc(InvoiceApplicationSheet::getSheetNo));
Map<Long, List<InvoiceApplicationLine>> linesBySheetId = lineMapper.selectList(Wrappers.<InvoiceApplicationLine>lambdaQuery()
.eq(InvoiceApplicationLine::getInvoiceApplicationId, entity.getId())).stream()
.collect(Collectors.groupingBy(InvoiceApplicationLine::getInvoiceSheetId));
List<BfiInvoiceApplySaveEntryDTO> items = new ArrayList<>();
for (InvoiceApplicationSheet sheet : sheets) {
linesBySheetId.getOrDefault(sheet.getId(), List.of()).stream()
.sorted(Comparator.comparing(InvoiceApplicationLine::getLineNo,
Comparator.nullsLast(Comparator.naturalOrder())))
.forEach(line -> items.add(buildInvoiceApplyEntry(line)));
}
if (Func.isEmpty(items)) {
throw new ServiceException("推送金蝶开票申请失败:开票申请无商品行,无法组装分录");
}
return items;
return param;
}
/** 单条商品行生成一条开票分录:品目三要素、税率、金额、税额全部透传页面「开票内容」行值 */
@@ -746,7 +795,12 @@ public class InvoiceApplicationServiceImpl extends BaseServiceImpl<InvoiceApplic
Map<String, InvoiceApplication> applicationBySerialNo = new LinkedHashMap<>();
applications.forEach(application -> {
if (Func.isNotBlank(application.getKingdeeBillNo())) {
applicationBySerialNo.putIfAbsent(application.getKingdeeBillNo(), application);
// 多张发票时申请单金蝶单号逗号拼接存储,拆分为逐张单号分别归集回写
for (String serialNo : application.getKingdeeBillNo().split(",")) {
if (Func.isNotBlank(serialNo)) {
applicationBySerialNo.putIfAbsent(serialNo.trim(), application);
}
}
}
});
Map<String, CustomerArchive> sellerCache = new LinkedHashMap<>();