Merge remote-tracking branch 'gitee/master'
付款申请预付联动冲突按拉下来的版本处理:金蝶生成即回显预结算预付信息,并保留多结算单保存逻辑。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+6
@@ -307,6 +307,12 @@ public class BfiProperties {
|
||||
*/
|
||||
private String invoiceTypes = "";
|
||||
|
||||
/**
|
||||
* 客商单位过滤开关:开启后只同步开票单位(销方)与受票单位(购方)税号均已登记在
|
||||
* 客商发票信息(blade_customer_invoice_info)中的发票,前端票池展示查询同用此开关过滤,默认关闭
|
||||
*/
|
||||
private boolean customerUnitsOnly = false;
|
||||
|
||||
/**
|
||||
* 同步 single-flight 锁租约秒数,须大于最坏一轮耗时;进程异常退出时锁在租约到期后自动释放
|
||||
*/
|
||||
|
||||
+6
-4
@@ -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);
|
||||
}
|
||||
|
||||
+12
@@ -39,12 +39,24 @@
|
||||
AND p.available_amount > 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>
|
||||
|
||||
+9
-5
@@ -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);
|
||||
|
||||
+56
-15
@@ -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) {
|
||||
}
|
||||
|
||||
+54
-50
@@ -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) {
|
||||
|
||||
+40
-12
@@ -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())
|
||||
|
||||
+3
-3
@@ -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:赊销),现销由金蝶自动推收款
|
||||
|
||||
+4
-4
@@ -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));
|
||||
// 【必填】往来户.编码:合同乙方(开票供应商)客商编码
|
||||
|
||||
Reference in New Issue
Block a user