feat: A303-1 开票申请推送放开特殊票种E04并随单报送货物运输单据体

This commit is contained in:
刘泉佳
2026-10-09 10:33:53 +08:00
parent 311da67744
commit 22b0b07ae2
2 changed files with 86 additions and 2 deletions
@@ -159,6 +159,12 @@ public final class KingdeeBfiConstant {
/** A303-1 特殊票种货物运输判定关键字:命中商品名称或商品和服务分类简称即视为货物运输类。 */
public static final List<String> BFI_INVOICE_FREIGHT_KEYWORDS = List.of("货物运输", "运费");
/** A303-1 货物运输单据体运输工具种类合法值:字典 transport_type 翻译名称命中则直送,否则兜底其它运输工具。 */
public static final List<String> BFI_FREIGHT_TRANSPORT_TYPES = List.of("铁路运输", "公路运输", "水路运输", "航空运输", "管道运输", "其它运输工具");
/** A303-1 货物运输单据体运输工具种类兜底值。 */
public static final String BFI_FREIGHT_TRANSPORT_TYPE_OTHER = "其它运输工具";
/** 金蝶同步状态:开票完成(A303-5 查到发票,终态,开票申请专用)。 */
public static final String KINGDEE_STATUS_INVOICED = "invoiced";
@@ -44,6 +44,7 @@ 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.dto.BfiInvoiceApplySaveFreightDTO;
import org.springblade.thirdparty.bfi.pojo.vo.BfiBillSaveResultVO;
import org.springblade.thirdparty.bfi.pojo.vo.BfiInvoiceApplyOpenApiBillVO;
import org.springblade.thirdparty.bfi.pojo.vo.BfiInvoiceApplyOpenApiInvoiceVO;
@@ -97,6 +98,7 @@ import org.springblade.transport.support.KingdeeArBillSyncSupport;
import org.springblade.transport.support.KingdeeSyncLockSupport;
import org.springblade.transport.support.KingdeeAttachmentSyncSupport;
import org.springblade.transport.wrapper.InvoiceApplicationWrapper;
import org.springblade.transport.wrapper.WaybillWrapper;
import org.springframework.beans.factory.annotation.Value;
import org.springblade.core.tool.utils.SpringUtil;
import org.springframework.stereotype.Service;
@@ -780,8 +782,18 @@ public class InvoiceApplicationServiceImpl extends BaseServiceImpl<InvoiceApplic
param.setTotalamount(sheetAmount);
// 是否含税:金蝶要求统一按不含税口径送(联调反馈),固定 0 不含税
param.setHsbz("0");
// todo 特殊票种:金蝶要求映射(联调反馈)——本张发票商品行全部为货物运输(运费/货物运输)送 E04,其余(其他、非货物运输的运输类等)送 00 非特殊票种
// param.setSpecialtype(this.specialTypeOf(items));
// 特殊票种:金蝶要求映射(联调反馈)——本张发票商品行全部为货物运输(运费/货物运输)送 E04,其余(其他、非货物运输的运输类等)送 00 非特殊票种
param.setSpecialtype(this.specialTypeOf(items));
// 货物运输单据体:送 E04 时随单填报(车牌/起运地/到达地/货物名称取开票申请结算明细快照),数据为空不阻断推送
if (KingdeeBfiConstant.BFI_INVOICE_SPECIAL_TYPE_FREIGHT.equals(param.getSpecialtype())) {
List<BfiInvoiceApplySaveFreightDTO> freights = this.buildFreights(entity);
if (Func.isNotEmpty(freights)) {
param.setFreights(freights);
} else {
log.warn("金蝶开票申请推送:开票申请[{}]按货物运输特殊票种(E04)推送但无结算明细快照,货物运输单据体未填报",
entity.getApplicationNo());
}
}
// 【必填】客户.编码:受票方客商档案客商编码
param.setCustomnameidNumber(customer.getCustomerCode());
// 【必填】项目.项目编码(联调期临时传金蝶测试档案值 999)
@@ -893,6 +905,72 @@ public class InvoiceApplicationServiceImpl extends BaseServiceImpl<InvoiceApplic
return KingdeeBfiConstant.BFI_INVOICE_FREIGHT_KEYWORDS.stream().anyMatch(goodsText::contains);
}
/**
* 组装 A303-1 货物运输单据体(特殊票种 E04 随单报送)。
* <p>
* 金蝶数电货运发票要求随单报送运输工具信息,取数与组装规则:
* <ul>
* <li>数据来源:开票申请结算明细快照(blade_invoice_application_detail,制单时自正式结算明细快照,含车牌/线路/货物运输信息)</li>
* <li>字段映射:vehicleNo→licenseplate(运输工具牌号)、departureAddress→startplace(起运地)、
* arrivalAddress→endplace(到达地)、cargoName→transportgoods(运输货物名称)、
* transportType→transporttype(运输工具种类,字典键翻译后映射,见 {@link #kingdeeTransportTypeOf(String)})</li>
* <li>行归并:结算明细按车次成行,同一「车牌×起运地×到达地×货物×运输工具种类」的多笔明细只报送一条</li>
* <li>空行剔除:牌号/起运地/到达地/货物名称均为空的明细行不报送(快照缺运输信息),避免金蝶侧空行报错</li>
* </ul>
*
* @param entity 开票申请
* @return 货物运输单据体分录(无可用快照数据时返回空列表,由调用方决定是否告警)
*/
private List<BfiInvoiceApplySaveFreightDTO> buildFreights(InvoiceApplication entity) {
List<InvoiceApplicationDetail> details = applicationDetailMapper.selectList(
Wrappers.<InvoiceApplicationDetail>lambdaQuery()
.eq(InvoiceApplicationDetail::getInvoiceApplicationId, entity.getId()));
// 按运程归并键去重:结算明细按车次成行,同一运程多笔明细金蝶侧只需一条(LinkedHashMap 保持明细行序)
Map<String, BfiInvoiceApplySaveFreightDTO> freightByKey = new LinkedHashMap<>();
int emptyRowSkipCount = 0;
for (InvoiceApplicationDetail detail : details) {
BfiInvoiceApplySaveFreightDTO freight = new BfiInvoiceApplySaveFreightDTO();
// 运输工具种类:字典 transport_type 键翻译后映射合法枚举,未命中兜底其它运输工具
freight.setTransporttype(this.kingdeeTransportTypeOf(detail.getTransportType()));
// 运输工具牌号/运输货物名称/起运地/到达地:结算明细快照直传
freight.setLicenseplate(detail.getVehicleNo());
freight.setTransportgoods(detail.getCargoName());
freight.setStartplace(detail.getDepartureAddress());
freight.setEndplace(detail.getArrivalAddress());
// 全空明细行(快照缺运输信息)不报送,避免金蝶侧空行报错
if (Func.isBlank(freight.getLicenseplate()) && Func.isBlank(freight.getStartplace())
&& Func.isBlank(freight.getEndplace()) && Func.isBlank(freight.getTransportgoods())) {
emptyRowSkipCount++;
continue;
}
String dedupeKey = Func.toStr(freight.getLicenseplate()) + "|" + Func.toStr(freight.getStartplace())
+ "|" + Func.toStr(freight.getEndplace()) + "|" + Func.toStr(freight.getTransportgoods())
+ "|" + Func.toStr(freight.getTransporttype());
freightByKey.putIfAbsent(dedupeKey, freight);
}
List<BfiInvoiceApplySaveFreightDTO> freights = new ArrayList<>(freightByKey.values());
log.info("金蝶开票申请货物运输单据体组装:开票申请[{}],结算明细快照{}条,剔除空行{}条,去重归并后分录{}条",
entity.getApplicationNo(), details.size(), emptyRowSkipCount, freights.size());
freights.forEach(freight -> log.info("金蝶开票申请货物运输分录:开票申请[{}],运输工具种类[{}],牌号[{}],货物[{}],起运地[{}],到达地[{}]",
entity.getApplicationNo(), freight.getTransporttype(), freight.getLicenseplate(),
freight.getTransportgoods(), freight.getStartplace(), freight.getEndplace()));
return freights;
}
/**
* 运输工具种类映射:结算明细快照存字典 transport_type 键,按业务/系统字典翻译为名称
* (复用运单展示口径 {@link WaybillWrapper#transportTypeName(String)}),
* 翻译名称命中 A303-1 合法枚举(铁路/公路/水路/航空/管道/其它运输工具)直送,否则兜底其它运输工具
*
* @param transportType 结算明细快照运输方式(字典键)
* @return 金蝶运输工具种类
*/
private String kingdeeTransportTypeOf(String transportType) {
String name = WaybillWrapper.transportTypeName(transportType);
return KingdeeBfiConstant.BFI_FREIGHT_TRANSPORT_TYPES.contains(name)
? name : KingdeeBfiConstant.BFI_FREIGHT_TRANSPORT_TYPE_OTHER;
}
@Override
public String syncKingdeeInvoiceResult() {
return kingdeeSyncLockSupport.runSingleFlight(INVOICE_RESULT_SYNC_LOCK_PREFIX, invoiceResultLockLeaseSeconds,