feat: 开票申请金蝶推送支持发票张次拆分与幂等重推,回写金蝶单号
This commit is contained in:
+111
-57
@@ -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<>();
|
||||
|
||||
Reference in New Issue
Block a user