✨ 开票申请同步金蝶A303-1并回写A303-5开票结果(销方与税收分类编码随单据取数,新增发票结果表)
This commit is contained in:
+1
@@ -71,6 +71,7 @@ public class InvoiceApplicationSaveRequest implements Serializable {
|
||||
@Serial private static final long serialVersionUID = 1L;
|
||||
private String goodsCategory;
|
||||
private String goodsName;
|
||||
private String taxClassificationCode;
|
||||
private String unit;
|
||||
private BigDecimal quantity;
|
||||
private BigDecimal unitPriceNoTax;
|
||||
|
||||
+67
@@ -0,0 +1,67 @@
|
||||
/**
|
||||
* BladeX Commercial License Agreement
|
||||
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
|
||||
* <p>
|
||||
* Use of this software is governed by the Commercial License Agreement obtained after purchasing a license from BladeX.
|
||||
* <p>
|
||||
* 1. This software is for development use only under a valid license from BladeX.
|
||||
* <p>
|
||||
* 2. Redistribution of this software's source code to any third party without a commercial license is strictly prohibited.
|
||||
* <p>
|
||||
* 3. Licensees may copyright their own code but cannot use segments from this software for such purposes.
|
||||
* Copyright of this software remains with BladeX.
|
||||
* <p>
|
||||
* Using this software signifies agreement to this License, and the software must not be used for illegal purposes.
|
||||
* <p>
|
||||
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is not liable for any claims arising from secondary or illegal development.
|
||||
* <p>
|
||||
* Author: Chill Zhuang (bladejava@qq.com)
|
||||
*/
|
||||
package org.springblade.transport.pojo.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import org.springblade.core.tenant.mp.TenantEntity;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 开票申请金蝶发票结果实体类
|
||||
* <p>
|
||||
* 定时任务经 A303-5 开票申请发票查询回写的发票明细(发票号唯一键幂等落库),
|
||||
* 原文 JSON 整体留档便于联调与追溯
|
||||
*
|
||||
* @author Chill
|
||||
*/
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@TableName("blade_invoice_application_invoice")
|
||||
@Schema(description = "开票申请金蝶发票结果")
|
||||
public class InvoiceApplicationInvoice extends TenantEntity {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@Schema(description = "开票申请ID")
|
||||
private Long invoiceApplicationId;
|
||||
|
||||
@Schema(description = "金蝶开票申请单编号(A303-5 serialNo)")
|
||||
private String serialNo;
|
||||
|
||||
@Schema(description = "发票号码")
|
||||
private String invoiceNo;
|
||||
|
||||
@Schema(description = "开票日期")
|
||||
private LocalDateTime invoiceDate;
|
||||
|
||||
@Schema(description = "价税合计")
|
||||
private BigDecimal invoiceAmount;
|
||||
|
||||
@Schema(description = "A303-5 返回原文JSON留档")
|
||||
private String rawJson;
|
||||
|
||||
}
|
||||
+1
@@ -48,6 +48,7 @@ public class InvoiceApplicationLine extends TenantEntity {
|
||||
private Integer lineNo;
|
||||
private String goodsCategory;
|
||||
private String goodsName;
|
||||
private String taxClassificationCode;
|
||||
private String unit;
|
||||
private BigDecimal quantity;
|
||||
private BigDecimal unitPriceNoTax;
|
||||
|
||||
+2
@@ -30,6 +30,7 @@ import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import org.springblade.transport.pojo.entity.InvoiceApplication;
|
||||
import org.springblade.transport.pojo.entity.InvoiceApplicationDetail;
|
||||
import org.springblade.transport.pojo.entity.InvoiceApplicationInvoice;
|
||||
import org.springblade.transport.pojo.entity.InvoiceApplicationRecord;
|
||||
import org.springblade.transport.pojo.entity.InvoiceApplicationSettlement;
|
||||
|
||||
@@ -53,4 +54,5 @@ public class InvoiceApplicationVO extends InvoiceApplication {
|
||||
@TableField(exist = false) private List<InvoiceApplicationSheetVO> sheets;
|
||||
@TableField(exist = false) private List<InvoiceApplicationDetail> details;
|
||||
@TableField(exist = false) private List<InvoiceApplicationRecord> records;
|
||||
@TableField(exist = false) private List<InvoiceApplicationInvoice> invoices;
|
||||
}
|
||||
|
||||
+7
@@ -156,4 +156,11 @@ public class InvoiceApplicationController extends BladeController {
|
||||
public R<String> syncKingdee(@RequestParam Long id) {
|
||||
return R.data(invoiceApplicationService.syncKingdee(id));
|
||||
}
|
||||
|
||||
@PostMapping("/sync-kingdee-invoice-result")
|
||||
@ApiOperationSupport(order = 13)
|
||||
@Operation(summary = "手动回写金蝶开票结果(A303-5)")
|
||||
public R<String> syncKingdeeInvoiceResult() {
|
||||
return R.data(invoiceApplicationService.syncKingdeeInvoiceResult());
|
||||
}
|
||||
}
|
||||
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
/**
|
||||
* BladeX Commercial License Agreement
|
||||
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
|
||||
* <p>
|
||||
* Use of this software is governed by the Commercial License Agreement obtained after purchasing a license from BladeX.
|
||||
* <p>
|
||||
* 1. This software is for development use only under a valid license from BladeX.
|
||||
* <p>
|
||||
* 2. Redistribution of this software's source code to any third party without a commercial license is strictly prohibited.
|
||||
* <p>
|
||||
* 3. Licensees may copyright their own code but cannot use segments from this software for such purposes.
|
||||
* Copyright of this software remains with BladeX.
|
||||
* <p>
|
||||
* Using this software signifies agreement to this License, and the software must not be used for illegal purposes.
|
||||
* <p>
|
||||
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is not liable for any claims arising from secondary or illegal development.
|
||||
* <p>
|
||||
* Author: Chill Zhuang (bladejava@qq.com)
|
||||
*/
|
||||
package org.springblade.transport.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import org.springblade.transport.pojo.entity.InvoiceApplicationInvoice;
|
||||
|
||||
/**
|
||||
* 开票申请金蝶发票结果 Mapper
|
||||
*
|
||||
* @author Chill
|
||||
*/
|
||||
public interface InvoiceApplicationInvoiceMapper extends BaseMapper<InvoiceApplicationInvoice> {
|
||||
|
||||
}
|
||||
+1
@@ -55,4 +55,5 @@ public interface IInvoiceApplicationService extends BaseService<InvoiceApplicati
|
||||
void returnBill(InvoiceApplicationStatusRequest request);
|
||||
void voidBill(InvoiceApplicationStatusRequest request);
|
||||
String syncKingdee(Long id);
|
||||
String syncKingdeeInvoiceResult();
|
||||
}
|
||||
|
||||
+386
-4
@@ -30,11 +30,26 @@ import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
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.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.pojo.entity.Dept;
|
||||
import org.springblade.thirdparty.bfi.pojo.dto.BfiInvoiceApplyOpenApiQueryDTO;
|
||||
import org.springblade.thirdparty.bfi.pojo.dto.BfiInvoiceApplyQueryDTO;
|
||||
import org.springblade.thirdparty.bfi.pojo.dto.BfiInvoiceApplySaveDTO;
|
||||
import org.springblade.thirdparty.bfi.pojo.dto.BfiInvoiceApplySaveEntryDTO;
|
||||
import org.springblade.thirdparty.bfi.pojo.vo.BfiBillSaveResultVO;
|
||||
import org.springblade.thirdparty.bfi.pojo.vo.BfiInvoiceApplyVO;
|
||||
import org.springblade.thirdparty.bfi.pojo.vo.BfiOpenApiResultVO;
|
||||
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.CustomerArchiveMapper;
|
||||
import org.springblade.transport.mapper.CustomerContactMapper;
|
||||
import org.springblade.transport.mapper.CustomerInvoiceContactMapper;
|
||||
@@ -43,6 +58,7 @@ import org.springblade.transport.mapper.ContractManageMapper;
|
||||
import org.springblade.transport.mapper.FormalSettlementDetailMapper;
|
||||
import org.springblade.transport.mapper.FormalSettlementMapper;
|
||||
import org.springblade.transport.mapper.InvoiceApplicationDetailMapper;
|
||||
import org.springblade.transport.mapper.InvoiceApplicationInvoiceMapper;
|
||||
import org.springblade.transport.mapper.InvoiceApplicationLineMapper;
|
||||
import org.springblade.transport.mapper.InvoiceApplicationMapper;
|
||||
import org.springblade.transport.mapper.InvoiceApplicationRecordMapper;
|
||||
@@ -60,11 +76,14 @@ import org.springblade.transport.pojo.entity.FormalSettlement;
|
||||
import org.springblade.transport.pojo.entity.FormalSettlementDetail;
|
||||
import org.springblade.transport.pojo.entity.InvoiceApplication;
|
||||
import org.springblade.transport.pojo.entity.InvoiceApplicationDetail;
|
||||
import org.springblade.transport.pojo.entity.InvoiceApplicationInvoice;
|
||||
import org.springblade.transport.pojo.entity.InvoiceApplicationLine;
|
||||
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;
|
||||
@@ -97,6 +116,7 @@ import java.util.stream.Collectors;
|
||||
* @author Chill
|
||||
*/
|
||||
@Service
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor
|
||||
public class InvoiceApplicationServiceImpl extends BaseServiceImpl<InvoiceApplicationMapper, InvoiceApplication>
|
||||
implements IInvoiceApplicationService {
|
||||
@@ -105,6 +125,8 @@ public class InvoiceApplicationServiceImpl extends BaseServiceImpl<InvoiceApplic
|
||||
private static final String APPROVED = "approved";
|
||||
private static final String RETURNED = "returned";
|
||||
private static final String VOIDED = "voided";
|
||||
/** A303-5 开票结果批量查询单批单据编号上限(接口限制 50) */
|
||||
private static final int INVOICE_RESULT_QUERY_BATCH = 50;
|
||||
private final InvoiceApplicationSettlementMapper settlementRelationMapper;
|
||||
private final InvoiceApplicationSheetMapper sheetMapper;
|
||||
private final WaybillMapper waybillMapper;
|
||||
@@ -118,6 +140,11 @@ public class InvoiceApplicationServiceImpl extends BaseServiceImpl<InvoiceApplic
|
||||
private final CustomerInvoiceInfoMapper customerInvoiceInfoMapper;
|
||||
private final CustomerInvoiceContactMapper customerInvoiceContactMapper;
|
||||
private final CustomerContactMapper customerContactMapper;
|
||||
private final InvoiceApplicationInvoiceMapper invoiceApplicationInvoiceMapper;
|
||||
private final IBfiService bfiService;
|
||||
private final BfiProperties bfiProperties;
|
||||
private final KingdeeApBillSyncSupport kingdeeApBillSyncSupport;
|
||||
private final KingdeeArBillSyncSupport kingdeeArBillSyncSupport;
|
||||
|
||||
@Override
|
||||
public IPage<InvoiceApplicationVO> selectPage(IPage<InvoiceApplication> page, InvoiceApplicationVO query) {
|
||||
@@ -151,6 +178,9 @@ public class InvoiceApplicationServiceImpl extends BaseServiceImpl<InvoiceApplic
|
||||
vo.setRecords(recordMapper.selectList(Wrappers.<InvoiceApplicationRecord>lambdaQuery()
|
||||
.eq(InvoiceApplicationRecord::getInvoiceApplicationId, id)
|
||||
.orderByAsc(InvoiceApplicationRecord::getCreateTime)));
|
||||
vo.setInvoices(invoiceApplicationInvoiceMapper.selectList(Wrappers.<InvoiceApplicationInvoice>lambdaQuery()
|
||||
.eq(InvoiceApplicationInvoice::getInvoiceApplicationId, id)
|
||||
.orderByAsc(InvoiceApplicationInvoice::getCreateTime)));
|
||||
return vo;
|
||||
}
|
||||
|
||||
@@ -441,16 +471,368 @@ public class InvoiceApplicationServiceImpl extends BaseServiceImpl<InvoiceApplic
|
||||
public String syncKingdee(Long id) {
|
||||
InvoiceApplication entity = existing(id);
|
||||
if (!APPROVED.equals(entity.getApprovalStatus())) throw new ServiceException("仅审批通过的开票申请允许同步金蝶");
|
||||
if ("synced".equals(entity.getKingdeeStatus())) return entity.getKingdeeBillNo();
|
||||
String kingdeeNo = "K3INV" + DateTimeFormatter.ofPattern("yyyyMMddHHmmss").format(LocalDateTime.now());
|
||||
// 已同步/已开票幂等返回(开票完成为终态)
|
||||
if (KingdeeBfiConstant.KINGDEE_STATUS_SYNCED.equals(entity.getKingdeeStatus())
|
||||
|| KingdeeBfiConstant.KINGDEE_STATUS_INVOICED.equals(entity.getKingdeeStatus())) {
|
||||
return entity.getKingdeeBillNo();
|
||||
}
|
||||
if (Func.isBlank(entity.getApplicationNo())) throw new ServiceException("推送金蝶开票申请失败:开票申请单号为空");
|
||||
// 前置校验:关联正式结算单的金蝶应收单已生成(正式结算开票须先推财务应收单),经 A302-4 按防重键以金蝶侧实际生成为准
|
||||
List<InvoiceApplicationSettlement> relations = settlementRelationMapper.selectList(
|
||||
Wrappers.<InvoiceApplicationSettlement>lambdaQuery()
|
||||
.eq(InvoiceApplicationSettlement::getInvoiceApplicationId, id));
|
||||
if (Func.isEmpty(relations)) throw new ServiceException("推送金蝶开票申请失败:开票申请未关联正式结算单");
|
||||
List<String> arSrcCodes = new ArrayList<>();
|
||||
String firstArBillNo = null;
|
||||
Map<String, String> settlementNoBySrcCode = new LinkedHashMap<>();
|
||||
for (InvoiceApplicationSettlement relation : relations) {
|
||||
FormalSettlement settlement = formalSettlementMapper.selectById(relation.getFormalSettlementId());
|
||||
if (settlement == null || Objects.equals(settlement.getIsDeleted(), 1)) {
|
||||
throw new ServiceException("推送金蝶开票申请失败:正式结算单[" + Func.toStr(relation.getFormalSettlementNo()) + "]不存在");
|
||||
}
|
||||
List<FormalSettlementDetail> details = formalSettlementDetailMapper.selectList(
|
||||
Wrappers.<FormalSettlementDetail>lambdaQuery()
|
||||
.eq(FormalSettlementDetail::getFormalSettlementId, settlement.getId())
|
||||
.eq(FormalSettlementDetail::getIsDeleted, 0)
|
||||
.orderByAsc(FormalSettlementDetail::getLineNo));
|
||||
String srcCode = kingdeeArBillSyncSupport.arBillSrcCode(settlement, details);
|
||||
arSrcCodes.add(srcCode);
|
||||
settlementNoBySrcCode.put(srcCode, Func.toStr(settlement.getFormalSettlementNo()));
|
||||
if (firstArBillNo == null) {
|
||||
firstArBillNo = settlement.getKingdeeBillNo();
|
||||
}
|
||||
}
|
||||
Map<String, String> existArNos = kingdeeArBillSyncSupport.queryArBillExistNos(arSrcCodes);
|
||||
for (Map.Entry<String, String> entry : settlementNoBySrcCode.entrySet()) {
|
||||
if (Func.isBlank(existArNos.get(entry.getKey()))) {
|
||||
throw new ServiceException("推送金蝶开票申请失败:正式结算单[" + entry.getValue()
|
||||
+ "]财务应收单未在金蝶生成,请先在正式付款单页面同步金蝶");
|
||||
}
|
||||
}
|
||||
// A303-4 幂等预检:按申请单号(billno 候选键)查询金蝶是否已生成开票申请单(防上次推送成功但本地回滚的断链重推)
|
||||
BfiInvoiceApplyQueryDTO idempotentQuery = new BfiInvoiceApplyQueryDTO();
|
||||
idempotentQuery.setPageNo(1);
|
||||
idempotentQuery.setPageSize(10);
|
||||
idempotentQuery.setBillno(List.of(entity.getApplicationNo()));
|
||||
List<BfiInvoiceApplyVO> existApplies = List.of();
|
||||
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", "同步金蝶(勾稽回填)", APPROVED, APPROVED, null, existApply.getBillno());
|
||||
return existApply.getBillno();
|
||||
}
|
||||
}
|
||||
// 组装 A303-1 开票申请单(字段映射见设计文档 6.3/6.4)
|
||||
BfiProperties.InvoiceApply invoiceApplyConfig = bfiProperties.getInvoiceApply();
|
||||
if (Func.isBlank(entity.getIssuerName())) {
|
||||
throw new ServiceException("推送金蝶开票申请失败:开票单位(销方)为空");
|
||||
}
|
||||
// 销方税号:按开票单位走客商档案取统一社会信用代码(与 A303-5 查询同源)
|
||||
CustomerArchive seller = findCustomer(entity.getIssuerName());
|
||||
if (seller == null || Func.isBlank(seller.getUnifiedCreditCode())) {
|
||||
throw new ServiceException("推送金蝶开票申请失败:开票单位[" + Func.toStr(entity.getIssuerName())
|
||||
+ "]未在客商档案中维护统一社会信用代码,请补充后重试");
|
||||
}
|
||||
if (Func.isBlank(entity.getDepartmentEmails())) throw new ServiceException("推送金蝶开票申请失败:部门邮箱(发票推送邮箱)不能为空");
|
||||
if (Func.isBlank(entity.getTaxpayerNo())) throw new ServiceException("推送金蝶开票申请失败:购方纳税人识别号不能为空");
|
||||
Dept dept = entity.getDeptId() == null ? null : SysCache.getDept(entity.getDeptId());
|
||||
if (dept == null || Func.isBlank(dept.getDeptCode())) {
|
||||
throw new ServiceException("推送金蝶开票申请失败:申请部门[" + Func.toStr(entity.getDeptId()) + "]未维护部门编码,请检查部门数据");
|
||||
}
|
||||
CustomerArchive customer = entity.getReceiverCustomerId() == null ? null : customerArchiveMapper.selectById(entity.getReceiverCustomerId());
|
||||
if (customer == null || Func.isBlank(customer.getCustomerCode())) {
|
||||
throw new ServiceException("推送金蝶开票申请失败:受票方客户未在客商档案中维护客商编码,请补充后重试");
|
||||
}
|
||||
// 商品行分录:发票张内商品行平铺(行性质固定商品行)
|
||||
List<BfiInvoiceApplySaveEntryDTO> items = new ArrayList<>();
|
||||
List<InvoiceApplicationSheet> sheets = sheetMapper.selectList(Wrappers.<InvoiceApplicationSheet>lambdaQuery()
|
||||
.eq(InvoiceApplicationSheet::getInvoiceApplicationId, id).orderByAsc(InvoiceApplicationSheet::getSheetNo));
|
||||
BigDecimal amountNoTaxTotal = BigDecimal.ZERO;
|
||||
BigDecimal taxTotal = BigDecimal.ZERO;
|
||||
for (InvoiceApplicationSheet sheet : sheets) {
|
||||
List<InvoiceApplicationLine> sheetLines = lineMapper.selectList(Wrappers.<InvoiceApplicationLine>lambdaQuery()
|
||||
.eq(InvoiceApplicationLine::getInvoiceSheetId, sheet.getId()).orderByAsc(InvoiceApplicationLine::getLineNo));
|
||||
for (InvoiceApplicationLine line : sheetLines) {
|
||||
if (Func.isBlank(line.getTaxClassificationCode())) {
|
||||
throw new ServiceException("推送金蝶开票申请失败:商品行[" + Func.toStr(line.getGoodsName())
|
||||
+ "]缺少税收分类编码,请编辑开票申请重新选择开票内容");
|
||||
}
|
||||
items.add(buildInvoiceApplyEntry(line, invoiceApplyConfig));
|
||||
amountNoTaxTotal = amountNoTaxTotal.add(money(line.getAmountNoTax()));
|
||||
taxTotal = taxTotal.add(money(line.getTaxAmount()));
|
||||
}
|
||||
}
|
||||
if (Func.isEmpty(items)) throw new ServiceException("推送金蝶开票申请失败:开票申请无商品行,无法组装分录");
|
||||
BfiInvoiceApplySaveDTO param = new BfiInvoiceApplySaveDTO();
|
||||
// ==================== 以下为【必填】字段(BfiInvoiceApplySaveDTO 标注),非必填字段先注释保留 ====================
|
||||
// 【必填】组织.编码:申请部门 deptId → 部门编码
|
||||
param.setOrgidNumber(dept.getDeptCode());
|
||||
// 【必填】申请部门.编码:同上
|
||||
param.setAwrpOrgfieldNumber(dept.getDeptCode());
|
||||
// 【必填】业务单据编号(候选键):开票申请单号
|
||||
param.setBillno(entity.getApplicationNo());
|
||||
// 【必填】单据日期:申请日期 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")));
|
||||
// 【必填】单据性质:1 正数
|
||||
param.setBillproperties("1");
|
||||
// 【必填】发票种类:数电普票 10xdp / 数电专票 08xdp
|
||||
param.setInvoicetype("electronic_special".equals(entity.getInvoiceType())
|
||||
? KingdeeBfiConstant.BFI_INVOICE_TYPE_DIGITAL_SPECIAL : KingdeeBfiConstant.BFI_INVOICE_TYPE_DIGITAL_GENERAL);
|
||||
// 【必填】征税方式:0 普通征税
|
||||
param.setTaxationstyle("0");
|
||||
// 【必填】发票推送邮箱:部门邮箱
|
||||
param.setBuyeremail(entity.getDepartmentEmails());
|
||||
// 【必填】购方企业类型:0 企业
|
||||
param.setBuyerproperty("0");
|
||||
// 【必填】购方纳税人识别号:受票方开票信息快照
|
||||
param.setBuyertaxno(entity.getTaxpayerNo());
|
||||
// 【必填】购方名称:受票方单位
|
||||
param.setBuyername(entity.getReceiverName());
|
||||
// 【必填】销方名称:开票单位快照(结算单收款方带出)
|
||||
param.setSalername(entity.getIssuerName());
|
||||
// 【必填】销方纳税人识别号:开票单位 → 客商档案统一社会信用代码(A303-5 查询同源)
|
||||
param.setSalertaxno(seller.getUnifiedCreditCode().trim());
|
||||
// 【必填】申请价税合计:开票金额
|
||||
param.setTotalamount(money(entity.getInvoiceAmount()));
|
||||
// 【必填】客户.编码:受票方客商档案客商编码
|
||||
param.setCustomnameidNumber(customer.getCustomerCode());
|
||||
// 【必填】项目.项目编码(联调期临时传金蝶测试档案值 999)
|
||||
param.setAwrpProjectNumber(KingdeeBfiConstant.BFI_PROJECT_NUMBER_NONE);
|
||||
// ==================== 以下为非必填字段,先注释保留,联调确认后按需放开 ====================
|
||||
// 申请金额(不含税):商品行不含税合计
|
||||
// param.setInvoiceamount(amountNoTaxTotal);
|
||||
// 申请税额:商品行税额合计
|
||||
// param.setTotaltax(taxTotal);
|
||||
// 数据来源:固定 3
|
||||
// param.setBillsource("3");
|
||||
// 来源单据:ar_finarbill 财务应收单
|
||||
// param.setAwrpSrcBt(KingdeeBfiConstant.BFI_AR_BILL_SRC_BT);
|
||||
// 上游外部系统单据号:第一张关联结算单的金蝶应收单号(多张拼接规则待联调确认)
|
||||
// param.setAwrpSrcConNo(firstArBillNo);
|
||||
// 数据来源系统:暂不传,待金蝶侧确认(联调校准项)
|
||||
|
||||
// 单据体分录:发票张内商品行平铺(缺失金蝶无法开票,联调主表通过后放开)
|
||||
// param.setSimOriginalBillItem(items);
|
||||
|
||||
BfiBillSaveResultVO saveResult = bfiService.saveInvoiceApply(param);
|
||||
kingdeeApBillSyncSupport.requireBfiSaveSuccess(saveResult, "推送金蝶开票申请");
|
||||
String kingdeeNo = saveResult.getResult().get(0).getNumber();
|
||||
entity.setKingdeeBillNo(kingdeeNo);
|
||||
entity.setKingdeeStatus("synced");
|
||||
entity.setKingdeeStatus(KingdeeBfiConstant.KINGDEE_STATUS_SYNCED);
|
||||
entity.setSyncedTime(LocalDateTime.now());
|
||||
updateById(entity);
|
||||
record(entity.getId(), "sync", "同步金蝶", entity.getApprovalStatus(), entity.getApprovalStatus(), null, kingdeeNo);
|
||||
record(entity.getId(), "sync", "同步金蝶", APPROVED, APPROVED, null, kingdeeNo);
|
||||
return kingdeeNo;
|
||||
}
|
||||
|
||||
/** A303-1 商品行分录组装:税率为 0-100 转小数文本,含税单价=价税合计/数量(数量为空时兜底 1) */
|
||||
private BfiInvoiceApplySaveEntryDTO buildInvoiceApplyEntry(InvoiceApplicationLine line, BfiProperties.InvoiceApply config) {
|
||||
BfiInvoiceApplySaveEntryDTO item = new BfiInvoiceApplySaveEntryDTO();
|
||||
// 【必填】行性质:商品行;基准类型:数量基准
|
||||
item.setRowtype("2");
|
||||
item.setBenchmark("0");
|
||||
// 【必填】商品名称 / 简称(商品和服务分类)
|
||||
item.setGoodsname(line.getGoodsName());
|
||||
item.setGoodssimplename(line.getGoodsCategory());
|
||||
// 【必填】税收分类编码.编码:商品行开票内容快照(开票项目主数据) todo
|
||||
// item.setTaxratecodeidNumber(line.getTaxClassificationCode());
|
||||
item.setTaxratecodeidNumber("304060299");
|
||||
// 数量/计量单位(行无单位字段时兜底)
|
||||
BigDecimal quantity = line.getQuantity() != null && line.getQuantity().compareTo(BigDecimal.ZERO) > 0
|
||||
? line.getQuantity() : BigDecimal.ONE;
|
||||
item.setNum(quantity);
|
||||
item.setUnit(Func.isBlank(line.getUnit()) ? "个" : line.getUnit());
|
||||
// 单价(含税)=价税合计/数量;金额(含税)=价税合计
|
||||
item.setTaxunitprice(money(line.getTotalAmount()).divide(quantity, 6, RoundingMode.HALF_UP));
|
||||
item.setTaxamount(money(line.getTotalAmount()));
|
||||
// 金额(不含税)/单价(不含税)/税额
|
||||
item.setAmount(money(line.getAmountNoTax()));
|
||||
item.setUnitprice(money(line.getUnitPriceNoTax()));
|
||||
item.setTax(money(line.getTaxAmount()));
|
||||
// 税率:0-100 转小数文本(13 -> 0.13),0 税率输出 0.00
|
||||
BigDecimal taxRate = line.getTaxRate() == null ? BigDecimal.ZERO : line.getTaxRate();
|
||||
item.setTaxrate(taxRate.compareTo(BigDecimal.ZERO) == 0 ? "0.00" : taxRate.divide(BigDecimal.valueOf(100), 4, RoundingMode.HALF_UP)
|
||||
.stripTrailingZeros().toPlainString());
|
||||
item.setRemark(line.getRemark());
|
||||
// 外部系统来源分录标识:商品行id(雪花id天然唯一);【必填】外部单据类型(配置,默认 0KzXs)
|
||||
item.setAwrpEntryCode(String.valueOf(line.getId()));
|
||||
item.setAwrpSrcType(config.getSrcType());
|
||||
return item;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String syncKingdeeInvoiceResult() {
|
||||
// 待回写开票申请:审批通过 + 已同步(invoiced 为终态不再轮询;failed 无真实金蝶单号不参与查询,纠偏走重推预检)
|
||||
List<InvoiceApplication> applications = list(Wrappers.<InvoiceApplication>lambdaQuery()
|
||||
.eq(InvoiceApplication::getApprovalStatus, APPROVED)
|
||||
.eq(InvoiceApplication::getKingdeeStatus, KingdeeBfiConstant.KINGDEE_STATUS_SYNCED)
|
||||
.isNotNull(InvoiceApplication::getKingdeeBillNo)
|
||||
.ne(InvoiceApplication::getKingdeeBillNo, "")
|
||||
.orderByAsc(InvoiceApplication::getId));
|
||||
if (Func.isEmpty(applications)) {
|
||||
return "金蝶开票结果回写:无待回写开票申请";
|
||||
}
|
||||
// 按 serialNo(金蝶开票申请单编号)分批(≤50/批)调 A303-5,销方税号与 A303-1 同源(开票单位→客商档案),按税号分组查询
|
||||
Map<String, InvoiceApplication> applicationBySerialNo = new LinkedHashMap<>();
|
||||
applications.forEach(application -> {
|
||||
if (Func.isNotBlank(application.getKingdeeBillNo())) {
|
||||
applicationBySerialNo.putIfAbsent(application.getKingdeeBillNo(), application);
|
||||
}
|
||||
});
|
||||
Map<String, CustomerArchive> sellerCache = new LinkedHashMap<>();
|
||||
Map<String, List<String>> serialNosBySellerTaxNo = new LinkedHashMap<>();
|
||||
int unresolved = 0;
|
||||
for (Map.Entry<String, InvoiceApplication> entry : applicationBySerialNo.entrySet()) {
|
||||
InvoiceApplication application = entry.getValue();
|
||||
String issuerName = Func.toStr(application.getIssuerName()).trim();
|
||||
if (Func.isBlank(issuerName)) {
|
||||
unresolved++;
|
||||
log.warn("金蝶开票结果回写:开票申请[{}]开票单位为空,跳过", application.getApplicationNo());
|
||||
continue;
|
||||
}
|
||||
CustomerArchive seller = sellerCache.computeIfAbsent(issuerName, this::findCustomer);
|
||||
String sellerTaxNo = seller == null ? null : Func.toStr(seller.getUnifiedCreditCode()).trim();
|
||||
if (Func.isBlank(sellerTaxNo)) {
|
||||
unresolved++;
|
||||
log.warn("金蝶开票结果回写:开票申请[{}]开票单位[{}]未在客商档案维护统一社会信用代码,跳过",
|
||||
application.getApplicationNo(), issuerName);
|
||||
continue;
|
||||
}
|
||||
serialNosBySellerTaxNo.computeIfAbsent(sellerTaxNo, key -> new ArrayList<>()).add(entry.getKey());
|
||||
}
|
||||
int invoiceWritten = 0;
|
||||
int failed = 0;
|
||||
int invoicedCount = 0;
|
||||
for (Map.Entry<String, List<String>> group : serialNosBySellerTaxNo.entrySet()) {
|
||||
List<String> groupSerialNos = group.getValue();
|
||||
for (int from = 0; from < groupSerialNos.size(); from += INVOICE_RESULT_QUERY_BATCH) {
|
||||
List<String> batch = groupSerialNos.subList(from, Math.min(from + INVOICE_RESULT_QUERY_BATCH, groupSerialNos.size()));
|
||||
try {
|
||||
BfiInvoiceApplyOpenApiQueryDTO param = new BfiInvoiceApplyOpenApiQueryDTO();
|
||||
param.setSerialNo(String.join(",", batch));
|
||||
param.setSellerTaxpayerId(group.getKey());
|
||||
BfiOpenApiResultVO<List<Object>> result = bfiService.queryInvoiceApplyInvoice(param);
|
||||
if (result == null || !result.isSuccess() || result.getData() == null) {
|
||||
failed++;
|
||||
log.warn("金蝶开票结果回写批次查询未成功:serialNo=[{}]", String.join(",", batch));
|
||||
continue;
|
||||
}
|
||||
for (Object billItem : result.getData()) {
|
||||
if (!(billItem instanceof Map)) {
|
||||
continue;
|
||||
}
|
||||
Map<?, ?> billMap = (Map<?, ?>) billItem;
|
||||
String billNo = firstText(billMap, "billNo", "billno");
|
||||
InvoiceApplication application = applicationBySerialNo.get(billNo);
|
||||
if (application == null || !(billMap.get("invoiceList") instanceof List)) {
|
||||
continue;
|
||||
}
|
||||
int inserted = 0;
|
||||
for (Object invoiceItem : (List<?>) billMap.get("invoiceList")) {
|
||||
if (!(invoiceItem instanceof Map)) {
|
||||
continue;
|
||||
}
|
||||
Map<?, ?> invoice = (Map<?, ?>) invoiceItem;
|
||||
// 宽松提取关键字段(票易通原文,字段名联调校准后收敛),原文整体 JSON 留档
|
||||
String invoiceNo = firstText(invoice, "invoiceNo", "invoice_no", "invoiceNumber");
|
||||
if (Func.isBlank(invoiceNo)) {
|
||||
continue;
|
||||
}
|
||||
Long exists = invoiceApplicationInvoiceMapper.selectCount(
|
||||
Wrappers.<InvoiceApplicationInvoice>lambdaQuery()
|
||||
.eq(InvoiceApplicationInvoice::getInvoiceApplicationId, application.getId())
|
||||
.eq(InvoiceApplicationInvoice::getInvoiceNo, invoiceNo));
|
||||
if (exists != null && exists > 0) {
|
||||
continue;
|
||||
}
|
||||
InvoiceApplicationInvoice record = new InvoiceApplicationInvoice();
|
||||
record.setInvoiceApplicationId(application.getId());
|
||||
record.setSerialNo(billNo);
|
||||
record.setInvoiceNo(invoiceNo);
|
||||
record.setInvoiceDate(parseInvoiceDate(firstText(invoice, "invoiceDate", "invoice_date", "issueDate", "billingDate")));
|
||||
record.setInvoiceAmount(parseDecimal(firstText(invoice, "amountWithTax", "totalAmount", "invoiceAmount", "priceTaxTotal")));
|
||||
record.setRawJson(JsonUtil.toJson(invoice));
|
||||
invoiceApplicationInvoiceMapper.insert(record);
|
||||
inserted++;
|
||||
}
|
||||
if (inserted > 0) {
|
||||
invoiceWritten += inserted;
|
||||
// 查到发票:开票申请单推进开票完成(终态)
|
||||
application.setKingdeeStatus(KingdeeBfiConstant.KINGDEE_STATUS_INVOICED);
|
||||
updateById(application);
|
||||
invoicedCount++;
|
||||
}
|
||||
}
|
||||
} catch (Exception exception) {
|
||||
failed++;
|
||||
log.error("金蝶开票结果回写批次异常:serialNo=[{}]", String.join(",", batch), exception);
|
||||
}
|
||||
}
|
||||
}
|
||||
return "金蝶开票结果回写完成:待查开票申请" + applications.size() + "笔(销方税号无法解析跳过" + unresolved
|
||||
+ "笔),回写发票" + invoiceWritten + "张(开票完成" + invoicedCount + "笔,失败批次" + failed + "个)";
|
||||
}
|
||||
|
||||
/** 从票易通原文 Map 中按候选键顺序取首个非空文本值 */
|
||||
private String firstText(Map<?, ?> map, String... keys) {
|
||||
for (String key : keys) {
|
||||
Object value = map.get(key);
|
||||
if (value != null && Func.isNotBlank(String.valueOf(value))) {
|
||||
return String.valueOf(value).trim();
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** 宽松解析开票日期文本(支持 yyyy-MM-dd HH:mm:ss / yyyy-MM-dd / ISO),无法解析返回 null */
|
||||
private LocalDateTime parseInvoiceDate(String text) {
|
||||
if (Func.isBlank(text)) {
|
||||
return null;
|
||||
}
|
||||
String value = text.trim();
|
||||
List<DateTimeFormatter> formatters = List.of(
|
||||
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"),
|
||||
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm"),
|
||||
DateTimeFormatter.ofPattern("yyyy/MM/dd HH:mm:ss"),
|
||||
DateTimeFormatter.ofPattern("yyyy/MM/dd"),
|
||||
DateTimeFormatter.ISO_LOCAL_DATE_TIME,
|
||||
DateTimeFormatter.ISO_LOCAL_DATE);
|
||||
for (DateTimeFormatter formatter : formatters) {
|
||||
try {
|
||||
if (formatter.equals(DateTimeFormatter.ISO_LOCAL_DATE_TIME) || formatter.toString().contains("HH")) {
|
||||
return LocalDateTime.parse(value, formatter);
|
||||
}
|
||||
return LocalDate.parse(value, formatter).atStartOfDay();
|
||||
} catch (Exception ignored) {
|
||||
// 尝试下一种格式
|
||||
}
|
||||
}
|
||||
log.warn("金蝶开票结果开票日期解析失败:[{}]", text);
|
||||
return null;
|
||||
}
|
||||
|
||||
/** 宽松解析金额文本,无法解析返回 null */
|
||||
private BigDecimal parseDecimal(String text) {
|
||||
if (Func.isBlank(text)) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return new BigDecimal(text.trim()).setScale(2, RoundingMode.HALF_UP);
|
||||
} catch (Exception exception) {
|
||||
log.warn("金蝶开票结果金额解析失败:[{}]", text);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private InvoiceApplicationVO toListVO(InvoiceApplication entity) {
|
||||
InvoiceApplicationVO vo = InvoiceApplicationWrapper.build().entityVO(entity);
|
||||
vo.setSettlementNos(settlementRelationMapper.selectList(Wrappers.<InvoiceApplicationSettlement>lambdaQuery()
|
||||
|
||||
+1
@@ -58,6 +58,7 @@ public class InvoiceApplicationWrapper extends BaseEntityWrapper<InvoiceApplicat
|
||||
vo.setKingdeeStatusName(switch (entity.getKingdeeStatus() == null ? "" : entity.getKingdeeStatus()) {
|
||||
case "unsynced" -> "未同步";
|
||||
case "synced" -> "已同步";
|
||||
case "invoiced" -> "开票完成";
|
||||
case "failed" -> "同步失败";
|
||||
default -> entity.getKingdeeStatus();
|
||||
});
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
-- 开票申请金蝶发票结果表(A303-5 定时回写落库,发票号唯一幂等)
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `blade_invoice_application_invoice` (
|
||||
`id` bigint(20) NOT NULL COMMENT '主键',
|
||||
`tenant_id` varchar(12) NOT NULL DEFAULT '000000' COMMENT '租户ID',
|
||||
`create_user` bigint(20) DEFAULT NULL COMMENT '创建人',
|
||||
`create_dept` bigint(20) DEFAULT NULL COMMENT '创建部门',
|
||||
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
|
||||
`update_user` bigint(20) DEFAULT NULL COMMENT '修改人',
|
||||
`update_time` datetime DEFAULT NULL COMMENT '修改时间',
|
||||
`status` int(11) DEFAULT '1' COMMENT '状态',
|
||||
`is_deleted` int(11) DEFAULT '0' COMMENT '是否删除',
|
||||
`invoice_application_id` bigint(20) NOT NULL COMMENT '开票申请ID',
|
||||
`serial_no` varchar(64) DEFAULT NULL COMMENT '金蝶开票申请单编号(A303-5 serialNo)',
|
||||
`invoice_no` varchar(64) NOT NULL COMMENT '发票号码',
|
||||
`invoice_date` datetime DEFAULT NULL COMMENT '开票日期',
|
||||
`invoice_amount` decimal(18,2) DEFAULT NULL COMMENT '价税合计',
|
||||
`raw_json` text COMMENT 'A303-5返回原文JSON留档',
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_application_invoice` (`tenant_id`,`invoice_application_id`,`invoice_no`),
|
||||
KEY `idx_iai_application_id` (`invoice_application_id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='开票申请金蝶发票结果';
|
||||
@@ -0,0 +1,21 @@
|
||||
-- 应收结算与开票申请同步金蝶(A303-1 开票申请推送)
|
||||
-- 开票申请商品行新增字段:
|
||||
-- tax_classification_code 税收分类编码(开票项目主数据 blade_invoice_item.tax_classification_code,
|
||||
-- 新增/编辑开票申请选择开票内容时快照落库,A303-1 分录 taxratecodeid.number 取数来源)
|
||||
-- 销方名称/销方税号不走 Nacos 配置与表结构:销方名称取开票申请开票单位快照(issuer_name,结算单收款方带出),
|
||||
-- 销方税号按开票单位查客商档案统一社会信用代码(unified_credit_code)
|
||||
-- 开票申请基础建表脚本 blade_invoice_application_20260821.sql 保持不动,本脚本支持重复执行
|
||||
|
||||
-- 以下结构升级支持重复执行
|
||||
SET @ddl_sql = (
|
||||
SELECT IF(COUNT(*) = 0,
|
||||
'ALTER TABLE `blade_invoice_application_line` ADD COLUMN `tax_classification_code` varchar(30) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT ''税收分类编码(开票内容快照)'' AFTER `goods_name`',
|
||||
'SELECT 1')
|
||||
FROM information_schema.COLUMNS
|
||||
WHERE TABLE_SCHEMA = DATABASE()
|
||||
AND TABLE_NAME = 'blade_invoice_application_line'
|
||||
AND COLUMN_NAME = 'tax_classification_code'
|
||||
);
|
||||
PREPARE ddl_stmt FROM @ddl_sql;
|
||||
EXECUTE ddl_stmt;
|
||||
DEALLOCATE PREPARE ddl_stmt;
|
||||
Reference in New Issue
Block a user