From 3edefa9b4b2e61d26f33094856aa916e6b0af84e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=98=E6=B3=89=E4=BD=B3?= <1107186916@qq.com> Date: Tue, 22 Sep 2026 10:51:49 +0800 Subject: [PATCH] =?UTF-8?q?:recycle:=20=E5=BC=80=E7=A5=A8=E7=94=B3?= =?UTF-8?q?=E8=AF=B7=E5=90=8C=E6=AD=A5=E9=87=91=E8=9D=B6=E5=88=86=E5=BD=95?= =?UTF-8?q?=E7=BB=84=E8=A3=85=E6=94=B9=E4=B8=BA=E7=BA=AF=E9=A1=B5=E9=9D=A2?= =?UTF-8?q?=E5=95=86=E5=93=81=E8=A1=8C=E9=80=8F=E4=BC=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit resolveInvoiceApplyEntries 去掉开票项目主数据全量拉取(ISysClient.getInvoiceItems)与合同计费规则链路(开票明细→正式结算明细→应收费用行 billing_rules_json)两处回查,分录的分类/简称/税收分类编码/税率/金额/税额/单价/备注全部取商品行落库值,行缺编码或缺税率直接报错;取数收敛为张次+商品行两条查询,避免逐张 N+1。 --- .../pojo/entity/InvoiceApplicationLine.java | 2 +- .../impl/InvoiceApplicationServiceImpl.java | 200 +++++++----------- 2 files changed, 81 insertions(+), 121 deletions(-) diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/InvoiceApplicationLine.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/InvoiceApplicationLine.java index a158df88..60f04c39 100644 --- a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/InvoiceApplicationLine.java +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/InvoiceApplicationLine.java @@ -64,7 +64,7 @@ public class InvoiceApplicationLine extends TenantEntity { */ private String goodsName; /** - * 税收分类编码(选择开票内容时快照,仅供展示与追溯;推送金蝶按分类+简称实时匹配开票项目主数据取数) + * 税收分类编码(选择开票内容时快照,推送金蝶 A303-1 分录直接取该值) */ private String taxClassificationCode; /** 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 864b2f6f..654dd137 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 @@ -34,14 +34,11 @@ import lombok.extern.slf4j.Slf4j; import org.springblade.core.log.exception.ServiceException; import org.springblade.core.mp.base.BaseServiceImpl; import org.springblade.core.secure.utils.AuthUtil; -import org.springblade.core.tool.api.R; import org.springblade.core.tool.jackson.JsonUtil; import org.springblade.core.tool.utils.BeanUtil; import org.springblade.core.tool.utils.Func; import org.springblade.system.cache.SysCache; -import org.springblade.system.feign.ISysClient; import org.springblade.system.pojo.entity.Dept; -import org.springblade.system.pojo.entity.InvoiceItem; import org.springblade.thirdparty.bfi.pojo.dto.BfiInvoiceApplyOpenApiQueryDTO; import org.springblade.thirdparty.bfi.pojo.dto.BfiInvoiceApplyQueryDTO; import org.springblade.thirdparty.bfi.pojo.dto.BfiInvoiceApplySaveDTO; @@ -53,11 +50,11 @@ import org.springblade.thirdparty.bfi.pojo.vo.BfiPageVO; import org.springblade.thirdparty.bfi.service.IBfiService; import org.springblade.transport.config.BfiProperties; import org.springblade.transport.constant.KingdeeBfiConstant; +import org.springblade.transport.mapper.ContractManageMapper; import org.springblade.transport.mapper.CustomerArchiveMapper; import org.springblade.transport.mapper.CustomerContactMapper; import org.springblade.transport.mapper.CustomerInvoiceContactMapper; import org.springblade.transport.mapper.CustomerInvoiceInfoMapper; -import org.springblade.transport.mapper.ContractManageMapper; import org.springblade.transport.mapper.FormalSettlementDetailMapper; import org.springblade.transport.mapper.FormalSettlementMapper; import org.springblade.transport.mapper.InvoiceApplicationDetailMapper; @@ -70,11 +67,11 @@ import org.springblade.transport.mapper.InvoiceApplicationSheetMapper; import org.springblade.transport.mapper.WaybillMapper; import org.springblade.transport.pojo.dto.InvoiceApplicationSaveRequest; import org.springblade.transport.pojo.dto.InvoiceApplicationStatusRequest; +import org.springblade.transport.pojo.entity.ContractManage; import org.springblade.transport.pojo.entity.CustomerArchive; import org.springblade.transport.pojo.entity.CustomerContact; import org.springblade.transport.pojo.entity.CustomerInvoiceContact; import org.springblade.transport.pojo.entity.CustomerInvoiceInfo; -import org.springblade.transport.pojo.entity.ContractManage; import org.springblade.transport.pojo.entity.FormalSettlement; import org.springblade.transport.pojo.entity.FormalSettlementDetail; import org.springblade.transport.pojo.entity.InvoiceApplication; @@ -85,14 +82,13 @@ import org.springblade.transport.pojo.entity.InvoiceApplicationRecord; import org.springblade.transport.pojo.entity.InvoiceApplicationSettlement; import org.springblade.transport.pojo.entity.InvoiceApplicationSheet; import org.springblade.transport.pojo.entity.Waybill; -import org.springblade.transport.support.KingdeeApBillSyncSupport; -import org.springblade.transport.support.KingdeeArBillSyncSupport; import org.springblade.transport.pojo.vo.CustomerInvoiceContactVO; import org.springblade.transport.pojo.vo.CustomerInvoiceInfoVO; import org.springblade.transport.pojo.vo.InvoiceApplicationSheetVO; import org.springblade.transport.pojo.vo.InvoiceApplicationVO; import org.springblade.transport.service.IInvoiceApplicationService; -import org.springblade.transport.service.KingdeeSyncContext; +import org.springblade.transport.support.KingdeeApBillSyncSupport; +import org.springblade.transport.support.KingdeeArBillSyncSupport; import org.springblade.transport.wrapper.InvoiceApplicationWrapper; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -104,6 +100,7 @@ import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; import java.util.ArrayList; import java.util.Arrays; +import java.util.Comparator; import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.List; @@ -131,8 +128,6 @@ public class InvoiceApplicationServiceImpl extends BaseServiceImpl selectPage(IPage page, InvoiceApplicationVO query) { @@ -571,8 +565,7 @@ public class InvoiceApplicationServiceImpl extends BaseServiceImpl items = this.resolveInvoiceApplyEntries(entity); BfiInvoiceApplySaveDTO param = new BfiInvoiceApplySaveDTO(); // ==================== 以下为【必填】字段(BfiInvoiceApplySaveDTO 标注),非必填字段先注释保留 ==================== @@ -628,7 +621,7 @@ public class InvoiceApplicationServiceImpl extends BaseServiceImpl resolveInvoiceApplyEntries(InvoiceApplication entity) { - List applicationDetails = applicationDetailMapper.selectList(Wrappers.lambdaQuery() - .eq(InvoiceApplicationDetail::getInvoiceApplicationId, entity.getId()) - .orderByAsc(InvoiceApplicationDetail::getLineNo)); - if (Func.isEmpty(applicationDetails)) { - throw new ServiceException("推送金蝶开票申请失败:开票申请无开票明细,无法按费用明细组装分录"); - } - List settlementDetailIds = applicationDetails.stream().map(InvoiceApplicationDetail::getFormalSettlementDetailId) - .filter(Objects::nonNull).distinct().toList(); - if (Func.isEmpty(settlementDetailIds)) { - throw new ServiceException("推送金蝶开票申请失败:开票明细缺少来源正式结算明细,请重新选择开票明细"); - } - List settlementDetails = formalSettlementDetailMapper.selectBatchIds(settlementDetailIds).stream() - .filter(detail -> !Objects.equals(detail.getIsDeleted(), 1)).toList(); - // 明细分录:每条应收货物费用行一条(不聚合,无吨位时数量兜底 1),与财务应收单(A302-2)分录同一取数 - KingdeeSyncContext context = KingdeeSyncContext.empty(); - List entryGroups = kingdeeArBillSyncSupport.resolveArEntryGroups(settlementDetails, context); - if (Func.isEmpty(entryGroups)) { - throw new ServiceException("推送金蝶开票申请失败:开票明细费用行均为空,无法组装分录"); - } - InvoiceItem freightItem = this.invoiceItemByShortName().get(INVOICE_FEE_NAME_FREIGHT); - if (freightItem == null || Func.isBlank(freightItem.getTaxClassificationCode())) { - throw new ServiceException("推送金蝶开票申请失败:开票项目页面未维护简称[" + INVOICE_FEE_NAME_FREIGHT - + "]的启用的开票项目(或税收分类编码为空),请补充后重试"); - } - BigDecimal taxRate = money(freightItem.getDefaultTaxRate()); - BigDecimal targetTotal = money(entity.getInvoiceAmount()); - BigDecimal sourceTotal = entryGroups.stream().map(KingdeeApBillSyncSupport.BfiEntryGroup::amount).reduce(BigDecimal.ZERO, BigDecimal::add); + 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<>(); - BigDecimal allocated = BigDecimal.ZERO; - for (int index = 0; index < entryGroups.size(); index++) { - KingdeeApBillSyncSupport.BfiEntryGroup entryGroup = entryGroups.get(index); - // 含税金额:以页面开票金额合计为准按费用行金额占比分摊,尾差由最后一行兜底 - BigDecimal taxAmount = index == entryGroups.size() - 1 ? targetTotal.subtract(allocated) - : sourceTotal.compareTo(BigDecimal.ZERO) == 0 ? BigDecimal.ZERO - : money(entryGroup.amount()).multiply(targetTotal).divide(sourceTotal, 2, RoundingMode.HALF_UP); - allocated = allocated.add(taxAmount); - // 不含税金额 = 价税合计 ÷ (1 + 税率),税额 = 差额(税率为小数,直接参与换算) - BigDecimal amountNoTax = taxAmount.divide(BigDecimal.ONE.add(taxRate), 2, RoundingMode.HALF_UP); - BigDecimal quantity = entryGroup.quantity() != null && entryGroup.quantity().compareTo(BigDecimal.ZERO) > 0 - ? entryGroup.quantity() : BigDecimal.ONE; - BfiInvoiceApplySaveEntryDTO item = new BfiInvoiceApplySaveEntryDTO(); - // 【必填】行性质:2 商品行(固定;联调校准项) - item.setRowtype("2"); - // 【必填】基准类型:0 数量基准(固定,按数量开票) - item.setBenchmark("0"); - // 【必填】商品名称:开票项目「货物或服务简称」(运费) - item.setGoodsname(freightItem.getShortName()); - // 【必填】货物或服务简称(商品和服务分类):开票项目「商品和服务分类名称」 - item.setGoodssimplename(freightItem.getCategoryName()); - // 【必填】税收分类编码.编码:开票项目「税收分类编码」 - String taxratecodeidNumber = freightItem.getTaxClassificationCode().trim(); - // todo test - taxratecodeidNumber = "304060299"; - item.setTaxratecodeidNumber(taxratecodeidNumber); - // 税收分类合并编码(联调确认必传):开票项目「税收分类编码」,如有补位规则联调后调整 - item.setGoodscode(taxratecodeidNumber); - // 数量:应收货物费用行运输数量(与财务应收单分录同源,无吨位兜底 1;折扣行才填 0) - item.setNum(quantity); - // 计量单位:费用行运费计算单位(兜底数量单位;档案暂无编码传名称,空兜底「次」) - item.setUnit(Func.isBlank(entryGroup.measureunitNumber()) ? "次" : entryGroup.measureunitNumber()); - // 规格型号:费用行无稳定规格来源,不传 - // 费用项目.编码:金蝶侧无对应档案映射,不传 - // 单价(含税):分摊后含税金额 ÷ 数量(6 位小数) - item.setTaxunitprice(taxAmount.divide(quantity, 6, RoundingMode.HALF_UP)); - // 金额(含税):费用行调整后总金额按占比分摊到开票金额合计(Σ分录 = 申请价税合计,尾差最后一行) - item.setTaxamount(taxAmount); - // 金额(不含税):含税金额 ÷ (1 + 税率)(2 位小数) - item.setAmount(amountNoTax); - // 单价(不含税):不含税金额 ÷ 数量(6 位小数) - item.setUnitprice(amountNoTax.divide(quantity, 6, RoundingMode.HALF_UP)); - // 税额:含税 − 不含税差额 - item.setTax(taxAmount.subtract(amountNoTax)); - // 税率:开票项目「默认税率」直接透传(库中即小数值:0.13/0.06,money 后两位文本 0 -> "0.00") - item.setTaxrate(taxRate.toPlainString()); - // 优惠政策标识/内容:本期无优惠场景,不传 - // 剩余可申请数量/金额/税额:差额征税抵扣场景使用,不传 - // 商品编码/物料编码:按商品类型(producttype)判定填写,本期品目走开票项目,不传 - // 明细扣除额:差额征税场景使用,不传 - // 行备注:费用行备注无开票语义,不传 - // 【必填】外部系统来源分录标识(候选键):应收货物费用行id,与财务应收单(A302-2)分录逐行勾稽 - item.setAwrpEntryCode(entryGroup.entryCode()); - // 【必填】分录外部单据类型:固定 0KzXs(配置 thirdParty.bfi.invoiceApply.srcType) - item.setAwrpSrcType(bfiProperties.getInvoiceApply().getSrcType()); - // 上游分录号:指向财务应收单分录行,回填规则待联调确认,本期不传 - items.add(item); + 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; } - /** 开票项目主数据「货物或服务简称 -> 开票项目」映射(每次同步远程加载一次;同名编码不一致时拦截) */ - private Map invoiceItemByShortName() { - R> response = sysClient.getInvoiceItems(); - if (response == null || !response.isSuccess() || Func.isEmpty(response.getData())) { - throw new ServiceException("推送金蝶开票申请失败:开票项目主数据读取失败,请稍后重试"); + /** 单条商品行生成一条开票分录:品目三要素、税率、金额、税额全部透传页面「开票内容」行值 */ + private BfiInvoiceApplySaveEntryDTO buildInvoiceApplyEntry(InvoiceApplicationLine line) { + // 税收分类编码:直接取商品行「开票内容」选择的开票项目快照 + if (Func.isBlank(line.getTaxClassificationCode())) { + throw new ServiceException("推送金蝶开票申请失败:商品行[" + Func.toStr(line.getGoodsCategory()) + "/" + Func.toStr(line.getGoodsName()) + + "]缺少税收分类编码,请编辑开票申请重新选择开票内容"); } - Map itemByShortName = new LinkedHashMap<>(); - for (InvoiceItem invoiceItem : response.getData()) { - if (invoiceItem == null || Func.isBlank(invoiceItem.getShortName())) { - continue; - } - String shortName = invoiceItem.getShortName().trim(); - InvoiceItem existing = itemByShortName.putIfAbsent(shortName, invoiceItem); - if (existing != null && !Func.toStr(existing.getTaxClassificationCode()).trim() - .equals(Func.toStr(invoiceItem.getTaxClassificationCode()).trim())) { - throw new ServiceException("推送金蝶开票申请失败:开票项目[" + shortName - + "]在开票项目页面维护了多个税收分类编码,请检查档案数据"); - } + if (line.getTaxRate() == null) { + throw new ServiceException("推送金蝶开票申请失败:商品行[" + Func.toStr(line.getGoodsName()) + + "]缺少税率,请编辑开票申请补全税率后重试"); } - return itemByShortName; + String taxClassificationCode = line.getTaxClassificationCode().trim(); + // todo test + taxClassificationCode = "304060299"; + + // 税率:页面维护的 0-100 百分数转小数(8 -> "0.08") + BigDecimal taxRate = money(line.getTaxRate()).divide(BigDecimal.valueOf(100), 6, RoundingMode.HALF_UP).stripTrailingZeros(); + String taxRateText = taxRate.compareTo(BigDecimal.ZERO) == 0 ? "0.00" : taxRate.toPlainString(); + // 含税金额/不含税金额/税额/数量/单位:取商品行(数量为0兜底1) + BigDecimal taxAmount = money(line.getTotalAmount()); + BigDecimal amountNoTax = money(line.getAmountNoTax()); + BigDecimal tax = money(line.getTaxAmount()); + BigDecimal quantity = line.getQuantity() != null && line.getQuantity().compareTo(BigDecimal.ZERO) > 0 + ? line.getQuantity() : BigDecimal.ONE; + BfiInvoiceApplySaveEntryDTO item = new BfiInvoiceApplySaveEntryDTO(); + // 【必填】行性质:2 商品行(固定;联调校准项) + item.setRowtype("2"); + // 【必填】基准类型:0 数量基准(固定,按数量开票) + item.setBenchmark("0"); + // 【必填】商品名称:商品行「货物或服务简称」 + item.setGoodsname(line.getGoodsName()); + // 【必填】货物或服务简称(商品和服务分类):商品行「商品和服务分类」 + item.setGoodssimplename(line.getGoodsCategory()); + // 税收分类编码.编码 + 税收分类合并编码(联调确认均必传,同取商品行开票内容快照) + item.setTaxratecodeidNumber(taxClassificationCode); + item.setGoodscode(taxClassificationCode); + // 数量/计量单位:商品行数量(兜底1)与单位(空兜底「个」) + item.setNum(quantity); + item.setUnit(Func.isBlank(line.getUnit()) ? "个" : line.getUnit()); + // 单价(不含税):商品行不含税单价;单价(含税):含税金额 ÷ 数量 + item.setUnitprice(money(line.getUnitPriceNoTax())); + // item.setTaxunitprice(taxAmount.divide(quantity, 6, RoundingMode.HALF_UP)); + // 金额(含税)/金额(不含税)/税额:商品行价税合计、不含税金额、税额 + // item.setTaxamount(taxAmount); + item.setAmount(amountNoTax); + item.setTax(tax); + // 税率:商品行税率(0-100 百分数转小数) 蝶税率枚举只有 0.13/0.09/0.06/0.05/0.03/0.01/0 + item.setTaxrate(taxRateText); + // 备注:商品行备注 + item.setRemark(line.getRemark()); + // 规格型号/费用项目.编码/优惠政策/剩余可申请/商品编码/物料编码/明细扣除额/上游分录号:本期不传 + // 【必填】外部系统来源分录标识(候选键):商品行id(雪花id天然唯一) + item.setAwrpEntryCode(String.valueOf(line.getId())); + // 【必填】分录外部单据类型:固定 0KzXs(配置 thirdParty.bfi.invoiceApply.srcType) + item.setAwrpSrcType(bfiProperties.getInvoiceApply().getSrcType()); + log.info("金蝶开票申请分录生成:商品行[{}],分类[{}],简称[{}],税收分类编码[{}],税率[{}],数量[{}],含税金额[{}],不含税金额[{}],税额[{}],分录标识[{}]", + line.getId(), item.getGoodssimplename(), item.getGoodsname(), item.getTaxratecodeidNumber(), taxRateText, + quantity, taxAmount, amountNoTax, tax, item.getAwrpEntryCode()); + return item; } @Override