Merge remote-tracking branch 'gitee/master'

付款申请预付联动冲突按拉下来的版本处理:金蝶生成即回显预结算预付信息,并保留多结算单保存逻辑。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-10-16 16:12:46 +08:00
13 changed files with 781 additions and 93 deletions
@@ -307,6 +307,12 @@ public class BfiProperties {
*/
private String invoiceTypes = "";
/**
* 客商单位过滤开关:开启后只同步开票单位(销方)与受票单位(购方)税号均已登记在
* 客商发票信息(blade_customer_invoice_info)中的发票,前端票池展示查询同用此开关过滤,默认关闭
*/
private boolean customerUnitsOnly = false;
/**
* 同步 single-flight 锁租约秒数,须大于最坏一轮耗时;进程异常退出时锁在租约到期后自动释放
*/
@@ -44,11 +44,13 @@ public interface KingdeeInvoicePoolMapper extends BaseMapper<KingdeeInvoicePool>
/**
* 金蝶票据池镜像统一分页查询:仅出可用票(剩余可用金额>0 且未使用),认领/登记标记由 EXISTS 子查询直出
*
* @param page 分页参数
* @param query 统一查询条件(已在上层完成 trim 与日期格式校验)
* @param tenantId 当前租户
* @param page 分页参数
* @param query 统一查询条件(已在上层完成 trim 与日期格式校验)
* @param tenantId 当前租户
* @param customerUnitsOnly 客商单位过滤开关:开启时只出销/购方税号均登记在客商发票信息中的发票(与定时同步同开关)
* @return 统一视图分页
*/
IPage<KingdeeInvoicePoolVO> selectInvoicePoolPage(IPage<KingdeeInvoicePoolVO> page,
@Param("query") KingdeeInvoicePoolQueryDTO query, @Param("tenantId") String tenantId);
@Param("query") KingdeeInvoicePoolQueryDTO query, @Param("tenantId") String tenantId,
@Param("customerUnitsOnly") boolean customerUnitsOnly);
}
@@ -39,12 +39,24 @@
AND p.available_amount &gt; 0 AND p.amount_use_status = '0'
</sql>
<!-- 客商单位过滤:开票单位(销方)与受票单位(购方)税号均须登记在有效客商发票信息中
(与定时同步拉取过滤同开关同口径;自定义 SQL 无租户插件,手工带租户与逻辑删除) -->
<sql id="customerUnitsFilter">
AND EXISTS (SELECT 1 FROM blade_customer_invoice_info ci
WHERE ci.is_deleted = 0 AND ci.status = 1 AND ci.tenant_id = p.tenant_id AND ci.tax_no = p.saler_tax_no)
AND EXISTS (SELECT 1 FROM blade_customer_invoice_info cj
WHERE cj.is_deleted = 0 AND cj.status = 1 AND cj.tenant_id = p.tenant_id AND cj.tax_no = p.buyer_tax_no)
</sql>
<select id="selectInvoicePoolPage" resultType="org.springblade.transport.pojo.vo.KingdeeInvoicePoolVO">
SELECT
<include refid="invoicePoolVoColumns"/>
FROM blade_kingdee_invoice_pool p
WHERE p.is_deleted = 0 AND p.status = 1 AND p.tenant_id = #{tenantId}
<include refid="usableFilter"/>
<if test="customerUnitsOnly">
<include refid="customerUnitsFilter"/>
</if>
<if test="query.invoiceNo != null and query.invoiceNo != ''">
AND p.invoice_no LIKE CONCAT('%', #{query.invoiceNo}, '%')
</if>
@@ -97,6 +97,7 @@ 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.IKingdeeBillLockService;
import org.springblade.transport.service.KingdeeSyncContext;
import org.springblade.transport.support.KingdeeApBillSyncSupport;
import org.springblade.transport.support.KingdeeArBillSyncSupport;
import org.springblade.transport.support.KingdeeSyncLockSupport;
@@ -806,6 +807,8 @@ public class InvoiceApplicationServiceImpl extends BaseServiceImpl<InvoiceApplic
if (dept == null || Func.isBlank(dept.getKingdeeDeptCode())) {
throw new ServiceException("推送金蝶开票申请失败:申请部门[" + Func.toStr(entity.getDeptId()) + "]未维护金蝶组织编码,请检查部门数据");
}
// 金蝶组织编码:项目业务部门反查唯一平台公司 → 平台公司金蝶组织编码
String kingdeeOrgNumber = kingdeeApBillSyncSupport.kingdeeOrgNumber(entity.getProjectId(), KingdeeSyncContext.empty());
CustomerArchive customer = entity.getReceiverCustomerId() == null ? null : customerArchiveMapper.selectById(entity.getReceiverCustomerId());
if (customer == null || Func.isBlank(customer.getCustomerCode())) {
throw new ServiceException("推送金蝶开票申请失败:受票方客户未在客商档案中维护客商编码,请补充后重试");
@@ -836,7 +839,7 @@ public class InvoiceApplicationServiceImpl extends BaseServiceImpl<InvoiceApplic
+ "]无商品行,无法组装分录");
}
BfiInvoiceApplySaveDTO param = this.buildInvoiceApplyParam(entity, sheetBillNo, seller, salerAddr, salerBank,
dept, customer, firstArSrcCode, money(sheet.getInvoiceAmount()), items);
dept, kingdeeOrgNumber, customer, firstArSrcCode, money(sheet.getInvoiceAmount()), items);
log.info("金蝶开票申请推送:开票申请[{}]发票张[{}],billno=[{}],价税合计=[{}],分录{}条",
entity.getApplicationNo(), sheet.getSheetNo(), sheetBillNo, sheet.getInvoiceAmount(), items.size());
BfiBillSaveResultVO saveResult = bfiService.saveInvoiceApply(param);
@@ -885,16 +888,17 @@ public class InvoiceApplicationServiceImpl extends BaseServiceImpl<InvoiceApplic
* 组装 A303-1 单张开票申请单入参(字段映射见设计文档 6.3/6.4),
* 购销方等主表公共字段随开票申请取数,价税合计与单据体分录取本张发票
*
* @param orgNumber 金蝶组织编码(项目业务部门反查平台公司)
* @param firstArSrcCode 上游来源单据号:首张关联结算单的金蝶应收单防重键 awrp_src_code(多张结算单取第一张)
*/
private BfiInvoiceApplySaveDTO buildInvoiceApplyParam(InvoiceApplication entity, String billNo,
CustomerArchive seller, String salerAddr, String salerBank, Dept dept, CustomerArchive customer,
CustomerArchive seller, String salerAddr, String salerBank, Dept dept, String orgNumber, CustomerArchive customer,
String firstArSrcCode, BigDecimal sheetAmount, List<BfiInvoiceApplySaveEntryDTO> items) {
BfiInvoiceApplySaveDTO param = new BfiInvoiceApplySaveDTO();
// ==================== 以下为【必填】字段(BfiInvoiceApplySaveDTO 标注),非必填字段先注释保留 ====================
// 【必填】组织.编码:申请部门 deptId → 金蝶组织编码
param.setOrgidNumber(dept.getKingdeeDeptCode());
// 【必填】申请部门.编码:同上
// 【必填】组织.编码:项目业务部门反查唯一平台公司 → 平台公司金蝶组织编码
param.setOrgidNumber(orgNumber);
// 【必填】申请部门.编码:申请部门 deptId → 金蝶部门编码
param.setAwrpOrgfieldNumber(dept.getKingdeeDeptCode());
// 【必填】业务单据编号(候选键):单张发票候选 billno(多张发票时开票申请单号-张次序号)
param.setBillno(billNo);
@@ -63,6 +63,7 @@ import org.springblade.transport.mapper.FormalSettlementMapper;
import org.springblade.transport.mapper.InvoiceReceiptMapper;
import org.springblade.transport.mapper.InvoiceReceiptRecordMapper;
import org.springblade.transport.mapper.InvoiceReceiptSettlementMapper;
import org.springblade.transport.mapper.CustomerInvoiceInfoMapper;
import org.springblade.transport.mapper.KingdeeInvoicePoolMapper;
import org.springblade.transport.pojo.dto.InvoiceReceiptSaveRequest;
import org.springblade.transport.pojo.dto.InvoiceReceiptStatusRequest;
@@ -70,6 +71,7 @@ import org.springblade.transport.pojo.dto.KingdeeInvoicePoolQueryDTO;
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.CustomerInvoiceInfo;
import org.springblade.transport.pojo.entity.FormalSettlement;
import org.springblade.transport.pojo.entity.FormalSettlementDetail;
import org.springblade.transport.pojo.entity.InvoiceReceipt;
@@ -146,6 +148,7 @@ public class InvoiceReceiptServiceImpl extends BaseServiceImpl<InvoiceReceiptMap
private final FormalSettlementDetailMapper formalSettlementDetailMapper;
private final CustomerArchiveMapper customerArchiveMapper;
private final CustomerContactMapper customerContactMapper;
private final CustomerInvoiceInfoMapper customerInvoiceInfoMapper;
private final IBfiService bfiService;
private final BfiProperties bfiProperties;
private final KingdeeApBillSyncSupport kingdeeApBillSyncSupport;
@@ -243,9 +246,10 @@ public class InvoiceReceiptServiceImpl extends BaseServiceImpl<InvoiceReceiptMap
return page;
}
/** 本地镜像真分页(可用过滤/认领登记标记均在 SQL 内直出) */
/** 本地镜像真分页(可用过滤/认领登记标记均在 SQL 内直出;客商单位过滤与定时同步同开关同口径) */
private IPage<KingdeeInvoicePoolVO> selectLocalPoolPage(KingdeeInvoicePoolQueryDTO query, long current, long size) {
return invoicePoolMapper.selectInvoicePoolPage(new Page<>(current, size), query, AuthUtil.getTenantId());
return invoicePoolMapper.selectInvoicePoolPage(new Page<>(current, size), query, AuthUtil.getTenantId(),
bfiProperties.getInvoiceReceipt().isCustomerUnitsOnly());
}
/**
@@ -1248,20 +1252,25 @@ public class InvoiceReceiptServiceImpl extends BaseServiceImpl<InvoiceReceiptMap
LocalDateTime windowStart = LocalDateTime.now().minusDays(bfiProperties.getInvoiceReceipt().getIncrementDays());
LocalDateTime windowEnd = LocalDateTime.now();
List<String> invoiceTypes = this.poolInvoiceTypeCodes();
// 客商单位过滤集:开关开启时一次性载入有效客商发票信息税号(销/购方税号都须命中才入镜像),整轮共享避免逐页查询
Set<String> customerTaxNos = bfiProperties.getInvoiceReceipt().isCustomerUnitsOnly() ? this.customerInvoiceTaxNos() : null;
SyncStats stats = new SyncStats();
log.info("金蝶全票池同步开始,增量窗口 {} ~ {},发票种类:{},单页 {} 条最多 {} 页(触顶自动二分切片)",
log.info("金蝶全票池同步开始,增量窗口 {} ~ {},发票种类:{},客商单位过滤:{},单页 {} 条最多 {} 页(触顶自动二分切片)",
windowStart.format(POOL_TIME_FORMATTER), windowEnd.format(POOL_TIME_FORMATTER),
invoiceTypes.isEmpty() ? "不限制" : invoiceTypes, POOL_PAGE_SIZE, MAX_POOL_PAGES);
invoiceTypes.isEmpty() ? "不限制" : invoiceTypes, customerTaxNos != null ? "开启" : "关闭",
POOL_PAGE_SIZE, MAX_POOL_PAGES);
if (invoiceTypes.isEmpty()) {
this.syncWindowSlice(windowStart, windowEnd, null, stats);
this.syncWindowSlice(windowStart, windowEnd, null, customerTaxNos, stats);
} else {
// 单值参数拆轮:每个票种编码独立完整切片续扫,互不挤占翻页额度
for (String invoiceType : invoiceTypes) {
this.syncWindowSlice(windowStart, windowEnd, invoiceType, stats);
this.syncWindowSlice(windowStart, windowEnd, invoiceType, customerTaxNos, stats);
}
}
String summary = "金蝶全票池同步:翻页" + stats.pageCount + "页切片" + stats.sliceCount + "片拉取"
+ stats.pulledCount + "张发票,镜像新增" + stats.created + "更新" + stats.updated + "无变化"
+ stats.pulledCount + "张发票"
+ (stats.customerFiltered > 0 ? "(客商单位过滤" + stats.customerFiltered + "张)" : "")
+ ",镜像新增" + stats.created + "更新" + stats.updated + "无变化"
+ stats.mirrorUnchanged + ",补绑" + stats.bound + ",登记无变化跳过" + stats.unchanged
+ ",失败" + stats.failed + ",耗时" + (System.currentTimeMillis() - startMillis) + "ms";
log.info("金蝶全票池同步结束,{}", summary);
@@ -1284,6 +1293,8 @@ public class InvoiceReceiptServiceImpl extends BaseServiceImpl<InvoiceReceiptMap
private static final class SyncStats {
/** A801-1 原始拉取张数 */
int pulledCount;
/** 客商单位过滤剔除张数(销/购方税号未同时登记在客商发票信息中,开关开启时统计) */
int customerFiltered;
/** 镜像新增条数(票池有、本地无) */
int created;
/** 镜像有变化并重写条数 */
@@ -1307,11 +1318,13 @@ public class InvoiceReceiptServiceImpl extends BaseServiceImpl<InvoiceReceiptMap
* 触顶说明该时间片数据 >2.5 万,从中点二分为两片递归续扫(递归深度受时间粒度自然收敛,
* 单片 500 条以内 1 页即短页终止)
*
* @param sliceStart 切片起点(含)
* @param sliceEnd 切片终点(不含,避免与下一片边界重复拉取)
* @param stats 整轮统计累加器
* @param sliceStart 切片起点(含)
* @param sliceEnd 切片终点(不含,避免与下一片边界重复拉取)
* @param customerTaxNos 客商发票信息税号集,null 表示客商单位过滤关闭(整轮共享)
* @param stats 整轮统计累加器
*/
private void syncWindowSlice(LocalDateTime sliceStart, LocalDateTime sliceEnd, String invoiceType, SyncStats stats) {
private void syncWindowSlice(LocalDateTime sliceStart, LocalDateTime sliceEnd, String invoiceType,
Set<String> customerTaxNos, SyncStats stats) {
// 二分收敛保护:切片粒度细到 1 秒仍触顶说明远端异常(同秒 >2.5 万条不可能),不再二分直接记漏
if (!sliceStart.isBefore(sliceEnd)) {
log.error("金蝶全票池同步切片区间为空({} ~ {}),远端 updateTime 粒度异常,放弃该切片", sliceStart, sliceEnd);
@@ -1325,6 +1338,13 @@ public class InvoiceReceiptServiceImpl extends BaseServiceImpl<InvoiceReceiptMap
}
stats.pageCount++;
stats.pulledCount += rows.size();
// 客商单位过滤:销/购方税号未同时登记在客商发票信息中的发票不入镜像;
// 短页判定用原始条数,过滤后的空页不能提前终止翻页
int rawPageSize = rows.size();
if (customerTaxNos != null) {
rows = rows.stream().filter(row -> this.bothUnitsRegistered(row, customerTaxNos)).toList();
stats.customerFiltered += rawPageSize - rows.size();
}
BatchUpsertResult upsertResult = this.batchUpsertInvoicePool(rows);
stats.created += upsertResult.created();
stats.updated += upsertResult.updated();
@@ -1336,8 +1356,8 @@ public class InvoiceReceiptServiceImpl extends BaseServiceImpl<InvoiceReceiptMap
stats.unchanged += refreshResult.unchanged();
stats.failed += refreshResult.failed();
}
// 远程短页:该片到底,正常终止
if (rows.size() < POOL_PAGE_SIZE) {
// 远程短页:该片到底,正常终止(按原始拉取条数判断,客商过滤不改变翻页节奏)
if (rawPageSize < POOL_PAGE_SIZE) {
stats.sliceCount++;
return;
}
@@ -1355,13 +1375,34 @@ public class InvoiceReceiptServiceImpl extends BaseServiceImpl<InvoiceReceiptMap
}
log.warn("金蝶全票池同步切片 {} ~ {} 触达翻页保护 {} 页({} 条),二分续扫 [{} ~ {}) + [{} ~ {})",
sliceStart, sliceEnd, MAX_POOL_PAGES, MAX_POOL_PAGES * POOL_PAGE_SIZE, sliceStart, mid, mid, sliceEnd);
this.syncWindowSlice(sliceStart, mid, invoiceType, stats);
this.syncWindowSlice(mid, sliceEnd, invoiceType, stats);
this.syncWindowSlice(sliceStart, mid, invoiceType, customerTaxNos, stats);
this.syncWindowSlice(mid, sliceEnd, invoiceType, customerTaxNos, stats);
return;
}
stats.sliceCount++;
}
/**
* 有效客商发票信息税号集(状态正常且未删除,MP 租户插件自动隔离租户):
* 客商单位过滤开关开启时,同步拉取与票池展示查询共用的单位白名单
*/
private Set<String> customerInvoiceTaxNos() {
return customerInvoiceInfoMapper.selectList(Wrappers.<CustomerInvoiceInfo>lambdaQuery()
.eq(CustomerInvoiceInfo::getStatus, 1)
.eq(CustomerInvoiceInfo::getIsDeleted, 0)).stream()
.map(CustomerInvoiceInfo::getTaxNo)
.filter(StringUtil::isNotBlank)
.collect(Collectors.toSet());
}
/**
* 开票单位(销方)与受票单位(购方)税号是否均已登记在客商发票信息中;税号缺失视为未登记(开关开启时不入镜像)
*/
private boolean bothUnitsRegistered(BfiInvoicePoolVO row, Set<String> customerTaxNos) {
return StringUtil.isNotBlank(row.getSalerTaxNo()) && StringUtil.isNotBlank(row.getBuyerTaxNo())
&& customerTaxNos.contains(row.getSalerTaxNo()) && customerTaxNos.contains(row.getBuyerTaxNo());
}
/** 已登记票跟随刷新结果统计(绑定/刷新/无变化/失败) */
private record RefreshResult(int bound, int unchanged, int failed) {
}
@@ -454,6 +454,8 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl<PaymentApplic
entity.setCurrentProcessor(AuthUtil.getUserName());
entity.setRemark(request.getReason() == null ? entity.getRemark() : limit(request.getReason(), 200));
updateById(entity);
// 联动作废提前建档的预结算预付记录,避免残留记录阻断预结算作废
voidPreSettlementAdvance(entity);
refreshFormalSettlement(entity);
}
@@ -609,8 +611,9 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl<PaymentApplic
// 真实付款结果改由定时回写任务驱动;联调环境需要模拟时恢复以下注释即可
// entity.setPaidAmount(money(entity.getAppliedAmount()).setScale(2, RoundingMode.HALF_UP));
// mockPaymentRecords(entity);
// syncPreSettlementAdvance(entity);
// syncFormalSettlementPayments(entity);
// 金蝶付款申请单生成成功:提前联动预结算预付记录(approved/已付0)回显预付信息,付款完成回写时更新为已付款
syncPreSettlementAdvance(entity, TransportBusiConstant.APPROVED, money(entity.getPaidAmount()));
updateById(entity);
log.info("付款申请[{}]同步金蝶完成:kingdeeStatus={},kingdeeBillId={},kingdeeBillNo={}",
entity.getPaymentNo(), entity.getKingdeeStatus(), kingdeeBillId, entity.getKingdeeBillNo());
@@ -679,6 +682,8 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl<PaymentApplic
.set(PaymentApplication::getKingdeeStatus, KingdeeBfiConstant.KINGDEE_STATUS_FAILED)
.set(PaymentApplication::getKingdeeBillId, null)
.set(PaymentApplication::getKingdeeBillNo, null));
// 金蝶单据已删除/解锁重推:联动作废提前建档的预付记录,重推生成新单后重新建档
voidPreSettlementAdvance(entity);
}
/**
@@ -828,9 +833,10 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl<PaymentApplic
String orgNumber = payApply == null ? null : payApply.getPayorgNumber();
if (Func.isBlank(orgNumber)) {
try {
// A103-4 付款人(公司)编码必填:优先取 A103-2 返回付款组织编码,兜底按合同所属组织解析
// A103-4 付款人(公司)编码必填:优先取 A103-2 返回付款组织编码,兜底按合同关联项目解析
KingdeeSyncContext context = KingdeeSyncContext.empty();
orgNumber = kingdeeApBillSyncSupport.bfiOrgNumber(
kingdeeApBillSyncSupport.requireContract(application.getContractId(), KingdeeSyncContext.empty()));
kingdeeApBillSyncSupport.requireContract(application.getContractId(), context), context);
} catch (Exception exception) {
log.warn("付款申请[{}]付款处理查询跳过,付款组织编码获取失败:{}", application.getPaymentNo(), exception.getMessage());
continue;
@@ -896,10 +902,15 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl<PaymentApplic
updateById(entity);
if (KingdeeBfiConstant.KINGDEE_STATUS_PAID.equals(kingdeeStatus)) {
// 付款完成:联动履约付款数据(预结算预付记录、正式结算付款记录)
syncPreSettlementAdvance(entity);
syncPreSettlementAdvance(entity, "paid", money(entity.getAppliedAmount()));
syncFormalSettlementPayments(entity);
return true;
}
// 生成失败单据勾稽确认金蝶已生成(已生成/付款中):与推送成功同语义,提前落预付记录回显预付信息
if (KingdeeBfiConstant.KINGDEE_STATUS_SYNCED.equals(kingdeeStatus)
|| KingdeeBfiConstant.KINGDEE_STATUS_PAYING.equals(kingdeeStatus)) {
syncPreSettlementAdvance(entity, TransportBusiConstant.APPROVED, money(entity.getPaidAmount()));
}
return false;
}
@@ -1154,7 +1165,7 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl<PaymentApplic
*/
private String pushBfiPayApply(PaymentApplication entity, KingdeeSyncContext context) {
ContractManage contract = kingdeeApBillSyncSupport.requireContract(entity.getContractId(), context);
String orgNumber = kingdeeApBillSyncSupport.bfiOrgNumber(contract);
String orgNumber = kingdeeApBillSyncSupport.bfiOrgNumber(contract, context);
String currency = kingdeeApBillSyncSupport.bfiCurrency(contract);
// 申请日期:yyyy-MM-dd HH:mm:ss(与 A102-2/A103-1 统一,金蝶各接口日期格式要求一致)
String applyDateText = bfiDateTimeText(entity);
@@ -1260,7 +1271,7 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl<PaymentApplic
*/
private String pushBfiPayApplyPush(PaymentApplication entity, KingdeeSyncContext context) {
ContractManage contract = kingdeeApBillSyncSupport.requireContract(entity.getContractId(), context);
String orgNumber = kingdeeApBillSyncSupport.bfiOrgNumber(contract);
String orgNumber = kingdeeApBillSyncSupport.bfiOrgNumber(contract, context);
// 申请日期:yyyy-MM-dd HH:mm:ss(与 A102-2 单据日期格式统一)
String applyDateText = bfiDateTimeText(entity);
BfiPayApplyPushDTO param = new BfiPayApplyPushDTO();
@@ -1574,39 +1585,26 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl<PaymentApplic
}
}
private void syncPreSettlementAdvance(PaymentApplication entity) {
/**
* 付款申请联动预结算预付记录:金蝶付款申请单生成成功(synced)即建档提前回显预付信息,
* 付款完成回写(paid)更新为已付款;按付款申请单号幂等,不产生重复记录
*
* @param billStatus 预付记录状态:approved(已生成待付)/ paid(已付款)
* @param paidAmount 已付金额:synced 阶段通常为0,paid 阶段取申请全额
*/
private void syncPreSettlementAdvance(PaymentApplication entity, String billStatus, BigDecimal paidAmount) {
if (entity.getPreSettlementId() == null) return;
if (!List.of("progress_advance", "settlement_payment").contains(entity.getPaymentType())) return;
List<PaymentApplicationSettlement> relations = settlementRelationMapper.selectList(
Wrappers.<PaymentApplicationSettlement>lambdaQuery()
.eq(PaymentApplicationSettlement::getPaymentApplicationId, entity.getId())
.eq(PaymentApplicationSettlement::getIsDeleted, 0)
.isNotNull(PaymentApplicationSettlement::getPreSettlementId));
if (relations.isEmpty()) {
if (entity.getPreSettlementId() == null) return;
upsertPreSettlementAdvance(entity.getPreSettlementId(), entity.getPaymentNo(), money(entity.getAppliedAmount()), entity);
refreshPreSettlementPaidLinks(entity.getPreSettlementId());
return;
}
for (PaymentApplicationSettlement relation : relations) {
String advanceNo = relations.size() == 1 ? entity.getPaymentNo()
: trimText(entity.getPaymentNo() + "-" + relation.getPreSettlementNo(), 100);
upsertPreSettlementAdvance(relation.getPreSettlementId(), advanceNo, money(relation.getAppliedAmount()), entity);
refreshPreSettlementPaidLinks(relation.getPreSettlementId());
}
}
private void upsertPreSettlementAdvance(Long preSettlementId, String advanceNo, BigDecimal appliedAmount,
PaymentApplication entity) {
PreSettlementAdvance advance = preSettlementAdvanceMapper.selectList(
Wrappers.<PreSettlementAdvance>lambdaQuery()
.eq(PreSettlementAdvance::getPreSettlementId, preSettlementId)
.eq(PreSettlementAdvance::getPreSettlementId, entity.getPreSettlementId())
.eq(PreSettlementAdvance::getIsDeleted, 0)
.ne(PreSettlementAdvance::getBillStatus, TransportBusiConstant.VOIDED)
.eq(PreSettlementAdvance::getAdvanceNo, advanceNo)
.eq(PreSettlementAdvance::getAdvanceNo, entity.getPaymentNo())
.last("limit 1")).stream().findFirst().orElse(null);
if (advance == null && Objects.equals(advanceNo, entity.getPaymentNo())) {
if (advance == null) {
advance = preSettlementAdvanceMapper.selectList(Wrappers.<PreSettlementAdvance>lambdaQuery()
.eq(PreSettlementAdvance::getPreSettlementId, preSettlementId)
.eq(PreSettlementAdvance::getPreSettlementId, entity.getPreSettlementId())
.eq(PreSettlementAdvance::getIsDeleted, 0)
.ne(PreSettlementAdvance::getBillStatus, TransportBusiConstant.VOIDED)
.eq(PreSettlementAdvance::getPaidAmount, BigDecimal.ZERO)
@@ -1615,34 +1613,40 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl<PaymentApplic
}
if (advance == null) {
advance = new PreSettlementAdvance();
advance.setPreSettlementId(preSettlementId);
advance.setAdvanceNo(advanceNo);
advance.setAppliedAmount(money(appliedAmount));
advance.setPreSettlementId(entity.getPreSettlementId());
advance.setAdvanceNo(entity.getPaymentNo());
advance.setAppliedAmount(money(entity.getAppliedAmount()));
}
advance.setPaidAmount(money(appliedAmount));
advance.setBillStatus("paid");
advance.setPaidAmount(paidAmount);
advance.setBillStatus(billStatus);
advance.setKingdeeAdvanceNo(entity.getKingdeeBillNo());
if (advance.getId() == null) preSettlementAdvanceMapper.insert(advance);
else preSettlementAdvanceMapper.updateById(advance);
}
private void refreshPreSettlementPaidLinks(Long preSettlementId) {
refreshPreSettlementAdvanceSummary(preSettlementId);
refreshPreSettlementAdvanceSummary(entity.getPreSettlementId());
formalSettlementSourceMapper.selectList(Wrappers.<FormalSettlementSource>lambdaQuery()
.eq(FormalSettlementSource::getPreSettlementId, preSettlementId)
.eq(FormalSettlementSource::getPreSettlementId, entity.getPreSettlementId())
.eq(FormalSettlementSource::getIsDeleted, 0)).forEach(source -> {
source.setAdvanceAppliedAmount(findPreSettlementApplied(preSettlementId));
source.setAdvancePaidAmount(findPreSettlementPaid(preSettlementId));
source.setAdvanceAppliedAmount(findPreSettlementApplied(entity.getPreSettlementId()));
source.setAdvancePaidAmount(findPreSettlementPaid(entity.getPreSettlementId()));
formalSettlementSourceMapper.updateById(source);
});
formalSettlementService.refreshPaymentSummariesForPreSettlement(preSettlementId);
formalSettlementService.refreshPaymentSummariesForPreSettlement(entity.getPreSettlementId());
}
private String trimText(String value, int length) {
if (value == null || value.length() <= length) {
return value;
}
return value.substring(0, length);
/**
* 联动作废付款申请关联的预结算预付记录(付款申请作废、金蝶反审核解锁场景):
* 仅清理提前建档的未付记录,已付款记录不联动(付款完成的事实不回退),避免残留记录阻断预结算作废
*/
private void voidPreSettlementAdvance(PaymentApplication entity) {
if (entity.getPreSettlementId() == null) return;
if (!List.of("progress_advance", "settlement_payment").contains(entity.getPaymentType())) return;
preSettlementAdvanceMapper.update(null, Wrappers.<PreSettlementAdvance>lambdaUpdate()
.eq(PreSettlementAdvance::getPreSettlementId, entity.getPreSettlementId())
.eq(PreSettlementAdvance::getAdvanceNo, entity.getPaymentNo())
.eq(PreSettlementAdvance::getIsDeleted, 0)
.ne(PreSettlementAdvance::getBillStatus, "paid")
.set(PreSettlementAdvance::getBillStatus, TransportBusiConstant.VOIDED));
refreshPreSettlementAdvanceSummary(entity.getPreSettlementId());
}
private BigDecimal findPreSettlementPaid(Long preSettlementId) {
@@ -233,7 +233,7 @@ public class KingdeeApBillSyncSupport {
if (Func.isBlank(billtypeNumber)) {
throw new ServiceException("推送金蝶财务应付单失败:单据类型[" + KingdeeBfiConstant.BFI_AP_BILL_BILLTYPE_NAME + "]未在映射字典中维护");
}
String orgNumber = bfiOrgNumber(contract);
String orgNumber = bfiOrgNumber(contract, context);
// 币种取合同结算币种
String currency = bfiCurrency(contract);
// 单据日期:yyyy-MM-dd HH:mm:ss(各接口统一格式),流程拆分后以同步当日作为应付单据日期
@@ -925,22 +925,50 @@ public class KingdeeApBillSyncSupport {
}
/**
* 合同所属组织编码
* 合同金蝶组织编码:合同关联项目 → 项目业务部门沿部门树向上反查唯一平台公司 → 平台公司金蝶组织编码
*
* @param contract 合同实体
* @param context 金蝶同步上下文(缓存载体)
* @return 组织编码
*/
public String bfiOrgNumber(ContractManage contract) {
Dept dept = contract.getOrganizationId() == null ? null : SysCache.getDept(contract.getOrganizationId());
// dept为空,直接报错
if (dept == null) {
throw new ServiceException("推送金蝶失败,合同所属组织不存在,请检查合同所属组织配置");
public String bfiOrgNumber(ContractManage contract, KingdeeSyncContext context) {
if (contract.getProjectId() == null) {
throw new ServiceException("推送金蝶失败,合同[" + Func.toStr(contract.getContractNo()) + "]未关联项目,无法确定金蝶组织编码");
}
// kingdeeDeptCode为空,直接报错
if (Func.isBlank(dept.getKingdeeDeptCode())) {
throw new ServiceException("推送金蝶失败,合同所属组织编码不能为空,请检查组织[" + dept.getDeptName() + "]是否维护金蝶组织编码");
return this.kingdeeOrgNumber(contract.getProjectId(), context);
}
/**
* 金蝶组织编码:项目业务部门沿部门树向上反查唯一平台公司(是否平台公司=是),取该平台公司的金蝶组织编码;
* 项目未关联/未维护业务部门/业务部门未归属平台公司/平台公司未维护金蝶组织编码时拦截
*
* @param projectId 项目id
* @param context 金蝶同步上下文(缓存载体)
* @return 组织编码
*/
public String kingdeeOrgNumber(Long projectId, KingdeeSyncContext context) {
if (projectId == null) {
throw new ServiceException("推送金蝶失败,单据未关联项目,无法确定金蝶组织编码");
}
return dept.getKingdeeDeptCode();
ProjectApply project = context.projectCache().computeIfAbsent(projectId, projectApplyMapper::selectById);
if (project == null) {
throw new ServiceException("推送金蝶失败,项目[" + projectId + "]不存在,无法确定金蝶组织编码");
}
if (project.getBusinessDeptId() == null) {
throw new ServiceException("推送金蝶失败,项目[" + Func.toStr(project.getProjectName()) + "]未维护业务部门,无法确定金蝶组织编码");
}
// 沿部门树向上反查平台公司,上限 20 级防脏数据成环死循环
Dept dept = SysCache.getDept(project.getBusinessDeptId());
for (int depth = 0; dept != null && depth < 20; depth++) {
if (Objects.equals(dept.getIsPlatformCompany(), 1)) {
if (Func.isBlank(dept.getKingdeeDeptCode())) {
throw new ServiceException("推送金蝶失败,平台公司[" + dept.getDeptName() + "]未维护金蝶组织编码,请检查部门数据");
}
return dept.getKingdeeDeptCode();
}
dept = dept.getParentId() == null ? null : SysCache.getDept(dept.getParentId());
}
throw new ServiceException("推送金蝶失败,项目业务部门[" + Func.toStr(project.getBusinessDeptName()) + "]未归属平台公司,无法确定金蝶组织编码");
}
/**
@@ -955,7 +983,7 @@ public class KingdeeApBillSyncSupport {
return "RMB".equalsIgnoreCase(currency) ? "CNY" : currency;
}
/** 财务应付单往来类型:合同类别为“客户合同”时为 bd_customer(客户),否则 bd_supplier(供应商) */
/** 财务应付单/付款申请/应收单/收票单统一往来类型:固定 bd_customer(按合同类别区分供应商的逻辑已注释停用,恢复时同步影响付款申请) */
public String bfiAsstactType(ContractManage contract) {
return KingdeeBfiConstant.BFI_CUSTOMER_ASSTACT_TYPE;
/*return "客户合同".equals(contract.getContractCategory())
@@ -170,7 +170,7 @@ public class KingdeeArBillSyncSupport {
if (Func.isBlank(billtypeNumber)) {
throw new ServiceException("推送金蝶财务应收单失败:单据类型[" + KingdeeBfiConstant.BFI_AR_BILL_BILLTYPE_NAME + "]未在映射字典中维护");
}
String orgNumber = kingdeeApBillSyncSupport.bfiOrgNumber(contract);
String orgNumber = kingdeeApBillSyncSupport.bfiOrgNumber(contract, context);
// 币种取合同结算币种
String currency = kingdeeApBillSyncSupport.bfiCurrency(contract);
// 单据日期:yyyy-MM-dd HH:mm:ss(各接口统一格式),以同步当日作为应收单据日期
@@ -193,8 +193,8 @@ public class KingdeeArBillSyncSupport {
param.setRecorgNumber(orgNumber);
// 【必填】单据日期
param.setBizdate(billDateText);
// 【必填】往来类型:应收固定客户
param.setAsstacttype(KingdeeBfiConstant.BFI_CUSTOMER_ASSTACT_TYPE);
// 【必填】往来类型:统一固定客户(KingdeeApBillSyncSupport#bfiAsstactType,与应付单/付款申请同口径)
param.setAsstacttype(kingdeeApBillSyncSupport.bfiAsstactType(contract));
// 【必填】往来户.编码:结算单付款方(客户)
param.setAsstactNumber(customerCode);
// 【必填】付款方式(CASH:现销, CREDIT:赊销),现销由金蝶自动推收款
@@ -183,10 +183,10 @@ public class KingdeeReceivedInvoiceSyncSupport {
param.setInvoicetypef7Number(mirror.getInvoiceTypeCode());
// 【必填】付款方式:(与应付单一致,不允许金蝶自动推付款)
param.setPaymenttype(BfiConstant.PAY_MODE_CASH);
// 【必填】往来类型:进项收票固定供应商
param.setAsstacttype(BfiConstant.ASSTACT_TYPE_SUPPLIER);
// 【必填】结算组织.编码:合同所属组织(与应付单同源)
param.setOrgNumber(kingdeeApBillSyncSupport.bfiOrgNumber(contract));
// 【必填】往来类型:统一固定客户(KingdeeApBillSyncSupport#bfiAsstactType,与应付单/付款申请同口径)
param.setAsstacttype(kingdeeApBillSyncSupport.bfiAsstactType(contract));
// 【必填】结算组织.编码:合同关联项目业务部门反查平台公司(与应付单同源)
param.setOrgNumber(kingdeeApBillSyncSupport.bfiOrgNumber(contract, context));
// 【必填】结算币别:合同结算币种
param.setCurrencyNumber(kingdeeApBillSyncSupport.bfiCurrency(contract));
// 【必填】往来户.编码:合同乙方(开票供应商)客商编码