diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/InvoiceApplicationServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/InvoiceApplicationServiceImpl.java index 40c18b2d..e27d0c3c 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/InvoiceApplicationServiceImpl.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/InvoiceApplicationServiceImpl.java @@ -509,25 +509,31 @@ public class InvoiceApplicationServiceImpl extends BaseServiceImpl sheets = sheetMapper.selectList(Wrappers.lambdaQuery() + .eq(InvoiceApplicationSheet::getInvoiceApplicationId, id).orderByAsc(InvoiceApplicationSheet::getSheetNo)); + if (Func.isEmpty(sheets)) { + throw new ServiceException("推送金蝶开票申请失败:开票申请无发票张次"); + } + Map> linesBySheetId = lineMapper.selectList(Wrappers.lambdaQuery() + .eq(InvoiceApplicationLine::getInvoiceApplicationId, id)).stream() + .collect(Collectors.groupingBy(InvoiceApplicationLine::getInvoiceSheetId)); + // 每张发票的金蝶业务单据编号(A303-1 billno 候选键,须唯一):单张=开票申请单号(兼容存量),多张=开票申请单号-张次序号 + List 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 existApplies = List.of(); + idempotentQuery.setPageSize(Math.max(sheets.size(), 10)); + idempotentQuery.setBillno(sheetBillNos); + Set existSheetBillNos = new LinkedHashSet<>(); BfiPageVO 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 items = this.resolveInvoiceApplyEntries(entity); + // 逐张发票推送金蝶开票申请单:每张发票张次一张(价税合计=本张金额,分录=本张商品行),已存在的张次幂等跳过 + List savedBillNos = new ArrayList<>(); + List 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 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 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 resolveInvoiceApplyEntries(InvoiceApplication entity) { - List sheets = sheetMapper.selectList(Wrappers.lambdaQuery() - .eq(InvoiceApplicationSheet::getInvoiceApplicationId, entity.getId()).orderByAsc(InvoiceApplicationSheet::getSheetNo)); - Map> linesBySheetId = lineMapper.selectList(Wrappers.lambdaQuery() - .eq(InvoiceApplicationLine::getInvoiceApplicationId, entity.getId())).stream() - .collect(Collectors.groupingBy(InvoiceApplicationLine::getInvoiceSheetId)); - List 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 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 sellerCache = new LinkedHashMap<>();