diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/InvoiceReceiptSaveRequest.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/InvoiceReceiptSaveRequest.java index f7b4c29b..52cd3376 100644 --- a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/InvoiceReceiptSaveRequest.java +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/InvoiceReceiptSaveRequest.java @@ -58,6 +58,14 @@ public class InvoiceReceiptSaveRequest implements Serializable { private String departmentEmails; private String attachmentsJson; private String remark; + /** + * 结算组织(页面手选部门,先票后款链路收票登记不再必选关联结算单) + */ + private Long deptId; + private String deptName; + /** + * 关联结算单分摊(兼容历史入参:先票后款链路不再传,关联关系由付款结算单侧写入) + */ private List settlements; /** diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/FormalSettlementInvoice.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/FormalSettlementInvoice.java index ad5c6265..9c303407 100644 --- a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/FormalSettlementInvoice.java +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/FormalSettlementInvoice.java @@ -8,6 +8,7 @@ */ package org.springblade.transport.pojo.entity; +import com.baomidou.mybatisplus.annotation.TableField; import com.baomidou.mybatisplus.annotation.TableName; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; @@ -39,4 +40,10 @@ public class FormalSettlementInvoice extends TenantEntity { private BigDecimal availableInvoiceAmount; private BigDecimal matchedAmount; private String attachmentJson; + @TableField(exist = false) + @Schema(description = "收票单金蝶推送状态(unsynced未推送/pushed已保存收票单/assigned已指定应付/failed推送失败),详情回显用") + private String kingdeePushStatus; + @TableField(exist = false) + @Schema(description = "收票单金蝶推送失败原因(详情失败行悬浮展示用)") + private String kingdeePushMsg; } diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/InvoiceReceipt.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/InvoiceReceipt.java index 91a33972..f3d58532 100644 --- a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/InvoiceReceipt.java +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/InvoiceReceipt.java @@ -112,6 +112,10 @@ public class InvoiceReceipt extends TenantEntity { * 收款方 */ private String payeeName; + /** + * 往来户金蝶供应商代码(发票销售方,保存时经客商档案/A901-1 回填,推送 A303-2 前强校验) + */ + private String supplierCode; /** * 开票单位开户银行 */ diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/FormalSettlementVO.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/FormalSettlementVO.java index b879bd0c..8010f82c 100644 --- a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/FormalSettlementVO.java +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/FormalSettlementVO.java @@ -48,6 +48,10 @@ public class FormalSettlementVO extends FormalSettlement implements KingdeeUnloc @TableField(exist = false) private Integer attachmentCount; @TableField(exist = false) private String attachmentSyncStatus; @TableField(exist = false) private String attachmentSyncStatusName; + @TableField(exist = false) @Schema(description = "已指定应付聚合态(assigned全部已指定/failed存在指定失败/unspecified存在未指定),空展示-") + private String kingdeeAssignStatus; + @TableField(exist = false) @Schema(description = "指定失败原因(存在指定失败时聚合各票原因,悬浮展示)") + private String kingdeeAssignFailReason; @TableField(exist = false) @Schema(description = "金蝶锁状态:locked-锁定中/failed-锁定失败;unlocked为终态不返回,null=无锁或已解锁") private String kingdeeLockStatus; @TableField(exist = false) @Schema(description = "金蝶锁失败原因(仅锁定失败时有值)") diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/PaymentApplicationVO.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/PaymentApplicationVO.java index 8c079128..2f2c481b 100644 --- a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/PaymentApplicationVO.java +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/PaymentApplicationVO.java @@ -50,6 +50,10 @@ public class PaymentApplicationVO extends PaymentApplication implements KingdeeU @TableField(exist = false) private String kingdeeLockStatus; @TableField(exist = false) private String kingdeeLockFailReason; @TableField(exist = false) private Integer kingdeeTryCount; + /** 已指定应付聚合态(assigned全部已指定/failed存在指定失败/unspecified存在未指定),空展示- */ + @TableField(exist = false) private String kingdeeAssignStatus; + /** 指定失败原因(存在指定失败时聚合各票原因,悬浮展示) */ + @TableField(exist = false) private String kingdeeAssignFailReason; /** 金蝶解锁终态标识(unlocked 锁记录回填):解锁时间非空即代表该单曾被金蝶解锁/删单,供页面展示明显标识 */ @TableField(exist = false) private LocalDateTime kingdeeUnlockTime; /** 金蝶侧解锁操作人(回调报文 operator) */ diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/config/BfiProperties.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/config/BfiProperties.java index 635207fb..19174df4 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/config/BfiProperties.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/config/BfiProperties.java @@ -235,6 +235,11 @@ public class BfiProperties { * 装卸大类、手动添加非运费杂费物料编码(装卸) */ private String loading = "XDWL11002004"; + + /** + * 仓储大类、手动添加仓储费用物料编码(仓储) + */ + private String warehouse = "XDWL11002001"; } /** @@ -254,7 +259,7 @@ public class BfiProperties { private long lockLeaseSeconds = 1800; /** - * A103-4 付款处理查询业务日期窗口天数,默认 60 + * A103-4 付款处理查询业务日期窗口天数(接口必填参数,按 awrp_sourcebillno 精确过滤单据),默认 60 */ private int queryWindowDays = 60; } @@ -269,6 +274,13 @@ public class BfiProperties { * 回写任务开关,默认关闭(联调稳定后开启) */ private boolean syncEnabled = false; + + /** + * A302-6 指定应付自动补偿开关:pushed 且已认领的收票单是否由本回写任务自动重发指定应付, + * 默认关闭(安全兜底),不自动推送指定应付,pushed 单据由人工入口 + * (发票登记推送 / 认领发票重推按钮)触发指定;置 true 开启自动补偿 + */ + private boolean assignCompensationEnabled = false; } /** diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/constant/KingdeeBfiConstant.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/constant/KingdeeBfiConstant.java index 052a50e1..353583f7 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/constant/KingdeeBfiConstant.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/constant/KingdeeBfiConstant.java @@ -192,6 +192,9 @@ public final class KingdeeBfiConstant { /** A303-2 业务类型(PUR:采购 / FEE:费用,TMS 服务费进项票暂按采购)。待确认:取值需金蝶侧最终确认。 */ public static final String BFI_RECEIVED_BIZ_TYPE = "PUR"; + /** A303-2 结算币别:收票登记不挂合同(先票后款),数电票固定人民币。 */ + public static final String BFI_RECEIVED_CURRENCY_NUMBER = "CNY"; + /** A303-2 必填:外部单据类型(收票单来源标识)。待确认:取值需金蝶侧确认,确认后如有调整直接改此常量。 */ public static final String BFI_RECEIVED_INVOICE_SRC_TYPE = "TMS收票登记"; diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/FormalSettlementController.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/FormalSettlementController.java index d128ed17..53be18ea 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/FormalSettlementController.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/FormalSettlementController.java @@ -201,20 +201,7 @@ public class FormalSettlementController extends BladeController { @Operation(summary = "审批通过") public R approve(@RequestBody FormalSettlementStatusRequest request) { formalSettlementService.approve(request); - return approveResponse(); - } - - /** 审批接口统一响应:审批已生效,自动推送结果(成功单据号/失败原因)一并返回;失败用 data=false 供前端区分提示样式 */ - private R approveResponse() { - String syncFailure = KingdeeApprovalSyncListener.pollSyncFailure(); - String syncResult = KingdeeApprovalSyncListener.pollSyncResult(); - if (syncFailure != null) { - return R.data(Boolean.FALSE, "审批通过,但自动同步金蝶失败:" + syncFailure); - } - if (syncResult != null) { - return R.success("审批通过,已同步金蝶:" + syncResult); - } - return R.success("审批通过"); + return KingdeeApprovalSyncListener.billSyncedResponse(); } @PostMapping("/return") @@ -232,6 +219,21 @@ public class FormalSettlementController extends BladeController { @Operation(summary = "推送金蝶(应付单/应收单)") public R syncKingdee(@RequestParam Long id) { return R.data(formalSettlementService.syncKingdee(id)); } + @PostMapping("/invoice-assign-kingdee") + @ApiOperationSupport(order = 14) + @Operation(summary = "发票登记确认推送金蝶(先票后款:认领收票单逐单 A302-6 指定应付)") + public R invoiceAssignKingdee(@RequestParam Long id) { + return R.data(formalSettlementService.pushInvoiceAssignKingdee(id)); + } + + @PostMapping("/repush-claimed-invoice") + @ApiOperationSupport(order = 16) + @Operation(summary = "重推认领收票单(查看页入口:收票单A303-2+指定应付A302-6 一步式,failed/pushed 通吃,可带匹配金额)") + public R repushClaimedInvoice(@RequestParam Long id, @RequestParam String invoiceNo, + @RequestParam(required = false) BigDecimal matchedAmount) { + return R.data(formalSettlementService.repushClaimedInvoice(id, invoiceNo, matchedAmount)); + } + @PostMapping("/sync-kingdee-ar-result") @ApiOperationSupport(order = 15) @Operation(summary = "手动勾稽金蝶应收单(A302-4 回写纠偏)") diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/InvoiceApplicationController.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/InvoiceApplicationController.java index 34c74ec3..55e877fe 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/InvoiceApplicationController.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/InvoiceApplicationController.java @@ -131,20 +131,7 @@ public class InvoiceApplicationController extends BladeController { @Operation(summary = "审批通过开票申请") public R approve(@RequestBody InvoiceApplicationStatusRequest request) { invoiceApplicationService.approve(request); - return approveResponse(); - } - - /** 审批接口统一响应:审批已生效,自动推送结果(成功单据号/失败原因)一并返回;失败用 data=false 供前端区分提示样式 */ - private R approveResponse() { - String syncFailure = KingdeeApprovalSyncListener.pollSyncFailure(); - String syncResult = KingdeeApprovalSyncListener.pollSyncResult(); - if (syncFailure != null) { - return R.data(Boolean.FALSE, "审批通过,但自动同步金蝶失败:" + syncFailure); - } - if (syncResult != null) { - return R.success("审批通过,已同步金蝶:" + syncResult); - } - return R.success("审批通过"); + return KingdeeApprovalSyncListener.billSyncedResponse(); } @PostMapping("/return") diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/InvoiceReceiptController.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/InvoiceReceiptController.java index c9023ceb..7b333c2b 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/InvoiceReceiptController.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/InvoiceReceiptController.java @@ -36,10 +36,12 @@ import org.springblade.core.mp.support.Condition; import org.springblade.core.mp.support.Query; import org.springblade.core.secure.annotation.PreAuth; import org.springblade.core.tool.api.R; +import org.springblade.thirdparty.bfi.pojo.vo.BfiInvoicePoolItemVO; import org.springblade.transport.listener.KingdeeApprovalSyncListener; import org.springblade.transport.pojo.dto.InvoiceReceiptSaveRequest; import org.springblade.transport.pojo.dto.InvoiceReceiptStatusRequest; import org.springblade.transport.pojo.dto.KingdeeInvoicePoolQueryDTO; +import org.springblade.transport.pojo.entity.InvoiceReceipt; import org.springblade.transport.pojo.vo.InvoiceReceiptVO; import org.springblade.transport.pojo.vo.KingdeeInvoicePoolVO; import org.springblade.transport.service.IInvoiceReceiptService; @@ -147,20 +149,7 @@ public class InvoiceReceiptController extends BladeController { @Operation(summary = "审批通过收票登记") public R approve(@RequestBody InvoiceReceiptStatusRequest request) { invoiceReceiptService.approve(request); - return approveResponse(); - } - - /** 审批接口统一响应:审批已生效,自动推送结果(成功单据号/失败原因)一并返回;失败用 data=false 供前端区分提示样式 */ - private R approveResponse() { - String syncFailure = KingdeeApprovalSyncListener.pollSyncFailure(); - String syncResult = KingdeeApprovalSyncListener.pollSyncResult(); - if (syncFailure != null) { - return R.data(Boolean.FALSE, "审批通过,但自动同步金蝶失败:" + syncFailure); - } - if (syncResult != null) { - return R.success("审批通过,已同步金蝶:" + syncResult); - } - return R.success("审批通过"); + return KingdeeApprovalSyncListener.summarySyncedResponse(); } @PostMapping("/return") @@ -181,7 +170,7 @@ public class InvoiceReceiptController extends BladeController { @PostMapping("/push-kingdee") @ApiOperationSupport(order = 15) - @Operation(summary = "批量推送金蝶收票单(A303-2 收票单保存 + A302-6 收票指定应付)") + @Operation(summary = "批量推送金蝶收票单(A303-2 收票单保存,先票后款:指定应付由付款结算单侧执行)") public R pushKingdee(@RequestBody List ids) { return R.data(invoiceReceiptService.pushKingdeeReceivedInvoice(ids)); } @@ -192,4 +181,25 @@ public class InvoiceReceiptController extends BladeController { public R syncKingdeeResult() { return R.data(invoiceReceiptService.syncKingdeeReceivedInvoiceResult()); } + + @GetMapping("/invoice-detail-query") + @ApiOperationSupport(order = 17) + @Operation(summary = "发票明细查询(先票后款:收票登记明细区块按票号带出发票明细行)") + public R> invoiceDetailQuery(@RequestParam String invoiceNo) { + return R.data(invoiceReceiptService.invoiceDetailQuery(invoiceNo)); + } + + @GetMapping("/claim-candidates") + @ApiOperationSupport(order = 18) + @Operation(summary = "结算单认领收票单候选分页(先票后款:审批通过且未被有效应付结算单认领)") + public R> claimCandidates(@RequestParam(required = false) String keyword, Query pageQuery) { + return R.data(invoiceReceiptService.claimCandidates(keyword, Condition.getPage(pageQuery))); + } + + @GetMapping("/claimed-list") + @ApiOperationSupport(order = 19) + @Operation(summary = "结算单已认领收票登记列表(先票后款:发票登记对话框回显已认领票)") + public R> claimedList(@RequestParam Long formalSettlementId) { + return R.data(invoiceReceiptService.claimedReceipts(formalSettlementId)); + } } diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/PaymentApplicationController.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/PaymentApplicationController.java index 149f908e..f2003c5f 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/PaymentApplicationController.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/PaymentApplicationController.java @@ -106,29 +106,21 @@ public class PaymentApplicationController extends BladeController { return approveResponse(request.getId()); } - /** 审批接口统一响应:审批已生效,付款特批发起/自动同步结果(成功单据号/失败原因)一并返回;失败用 data=false 供前端区分提示样式 */ + /** 审批接口统一响应:审批已生效,付款特批发起结果优先返回;特批完成/未走特批时金蝶同步结果走统一响应(成功单据号/失败原因) */ private R approveResponse(Long id) { String hcsFailure = HcsImmediatePushSupport.pollSyncFailure(); - String syncFailure = KingdeeApprovalSyncListener.pollSyncFailure(); - String syncResult = KingdeeApprovalSyncListener.pollSyncResult(); if (hcsFailure != null) { return R.data(Boolean.FALSE, "审批通过,但发起付款特批/推送合规失败:" + hcsFailure + ",可稍后重推合规"); } PaymentApplication payment = paymentApplicationService.getById(id); String specStatus = payment == null ? null : payment.getHcsSpecStatus(); - if (HcsSyncConstant.SPEC_STATUS_NEED.equals(specStatus)) { + if (hcsImmediatePushSupport.isSpecPayEnabled() && HcsSyncConstant.SPEC_STATUS_NEED.equals(specStatus)) { return R.success("审批通过,付款特批待发起,可在详情页重推合规"); } if (Arrays.asList(HcsSyncConstant.SPEC_STATUS_PUSHING, HcsSyncConstant.SPEC_STATUS_PROCESSING).contains(specStatus)) { return R.success("审批通过,已发起付款特批,特批通过后将自动推送履约数据并同步金蝶"); } - if (syncFailure != null) { - return R.data(Boolean.FALSE, "审批通过,但自动同步金蝶失败:" + syncFailure); - } - if (syncResult != null) { - return R.success("审批通过,已同步金蝶:" + syncResult); - } - return R.success("审批通过"); + return KingdeeApprovalSyncListener.billSyncedResponse(); } @PostMapping("/return") @ApiOperationSupport(order = 8) diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/SettlementAdjustmentController.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/SettlementAdjustmentController.java index 86772411..9ed37905 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/SettlementAdjustmentController.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/SettlementAdjustmentController.java @@ -36,20 +36,9 @@ public class SettlementAdjustmentController extends BladeController { @PostMapping("/remove") public R remove(@RequestParam Long id) { service.removeDraft(id); return R.success("删除成功"); } @PostMapping("/submit") public R submit(@RequestBody SettlementAdjustmentStatusRequest request) { service.submit(request); return R.success("提交成功"); } @PostMapping("/approve") public R approve(@RequestBody SettlementAdjustmentStatusRequest request) { + // 未推送场景自动推送失败原因以警告文案返回(data=false 供前端区分提示样式);hint 优先于同步成功提示 String hint = service.approve(request); - // 未推送场景自动推送失败原因以警告文案返回(data=false 供前端区分提示样式) - String syncFailure = KingdeeApprovalSyncListener.pollSyncFailure(); - String syncResult = KingdeeApprovalSyncListener.pollSyncResult(); - if (Func.isNotBlank(syncFailure)) { - return R.data(Boolean.FALSE, "审批通过,但自动同步金蝶失败:" + syncFailure); - } - if (Func.isNotBlank(hint)) { - return R.data(Boolean.TRUE, "审批通过;" + hint); - } - if (Func.isNotBlank(syncResult)) { - return R.success("审批通过,已同步金蝶:" + syncResult); - } - return R.success("审批通过"); + return KingdeeApprovalSyncListener.billSyncedResponse(Func.isBlank(hint) ? null : hint); } @PostMapping("/return") public R returnBill(@RequestBody SettlementAdjustmentStatusRequest request) { service.returnBill(request); return R.success("已驳回"); } @PostMapping("/repush") public R repush(@RequestParam Long id) { return R.data(service.repush(id)); } diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/listener/KingdeeApprovalSyncListener.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/listener/KingdeeApprovalSyncListener.java index fbecca86..491f5a36 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/listener/KingdeeApprovalSyncListener.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/listener/KingdeeApprovalSyncListener.java @@ -21,8 +21,10 @@ package org.springblade.transport.listener; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.springblade.core.tool.api.R; import org.springblade.transport.config.BfiProperties; import org.springblade.transport.constant.HcsSyncConstant; +import org.springblade.hcs.config.HcsProperties; import org.springblade.transport.event.KingdeeBillApprovedEvent; import org.springblade.transport.pojo.entity.PaymentApplication; import org.springblade.transport.service.IFormalSettlementService; @@ -64,6 +66,7 @@ public class KingdeeApprovalSyncListener { private final IInvoiceApplicationService invoiceApplicationService; private final IInvoiceReceiptService invoiceReceiptService; private final BfiProperties bfiProperties; + private final HcsProperties hcsProperties; /** 当前审批请求线程的自动推送失败原因,供 Controller 取走提示前端(见类注释线程说明) */ private static final ThreadLocal SYNC_FAILURE = new ThreadLocal<>(); @@ -98,6 +101,47 @@ public class KingdeeApprovalSyncListener { } } + /** + * 审批接口统一金蝶同步响应(单据号域:正式结算单/开票申请/付款申请/结算调整): + * 同步失败原因以 data=false 返回供前端区分提示样式,成功提示"单据号[xxx]已同步金蝶",未触发推送返回"审批通过" + */ + public static R billSyncedResponse() { + return billSyncedResponse(null); + } + + /** + * 同 {@link #billSyncedResponse()},pendingHint 非空时优先于同步成功提示返回(结算调整 hint 场景) + * + * @param pendingHint 审批附带提示(如"存在未勾稽明细"),无同步失败时以 data=true 警告返回 + */ + public static R billSyncedResponse(String pendingHint) { + String syncResult = pollSyncResult(); + return response(syncResult == null ? null : "审批通过,单据号[" + syncResult + "]已同步金蝶", pendingHint); + } + + /** + * 审批接口统一金蝶同步响应(收票登记域):A303-2 推送结果为笔数汇总而非单据号,成功提示原样返回汇总文案 + */ + public static R summarySyncedResponse() { + String syncResult = pollSyncResult(); + return response(syncResult == null ? null : "审批通过,已同步金蝶:" + syncResult, null); + } + + /** 同步响应骨架:同步失败原因优先(data=false),其次审批附带提示(data=true),再次成功提示,均无返回"审批通过" */ + private static R response(String syncedTip, String pendingHint) { + String syncFailure = pollSyncFailure(); + if (syncFailure != null) { + return R.data(Boolean.FALSE, "审批通过,但自动同步金蝶失败:" + syncFailure); + } + if (pendingHint != null) { + return R.data(Boolean.TRUE, "审批通过;" + pendingHint); + } + if (syncedTip != null) { + return R.success(syncedTip); + } + return R.success("审批通过"); + } + /** * 事务提交后同步推送。fallbackExecution=true 兜底:approve 未开启事务时事件不再被默认丢弃。 * @@ -141,18 +185,19 @@ public class KingdeeApprovalSyncListener { return; } // 付款特批未完成(高风险/黑名单/额度超额)时金蝶必然拒推:跳过而非置失败, - // 特批通过后由 HcsPaymentSyncSupport 重新发布本事件自动补推 + // 特批通过后由 HcsPaymentSyncSupport 重新发布本事件自动补推; + // 特批不走合同系统(spec-pay.enabled=false)时不跳过 PaymentApplication payment = paymentApplicationService.getById(id); - if (payment != null && payment.getHcsSpecStatus() != null + if (hcsProperties.getSpecPay().isEnabled() && payment != null && payment.getHcsSpecStatus() != null && List.of(HcsSyncConstant.SPEC_STATUS_NEED, HcsSyncConstant.SPEC_STATUS_PUSHING, HcsSyncConstant.SPEC_STATUS_PROCESSING).contains(payment.getHcsSpecStatus())) { log.info("付款申请[{}]审批通过自动同步金蝶跳过(付款特批未完成):hcsSpecStatus={}", id, payment.getHcsSpecStatus()); return; } log.info("付款申请[{}]审批通过自动同步金蝶开始", id); - String kingdeeBillId = paymentApplicationService.syncKingdee(id); - recordResult(kingdeeBillId); - log.info("付款申请[{}]审批通过自动同步金蝶完成:kingdeeBillId={}", id, kingdeeBillId); + String billNo = paymentApplicationService.syncKingdee(id); + recordResult(billNo); + log.info("付款申请[{}]审批通过自动同步金蝶完成:billNo={}", id, billNo); } private void pushInvoiceApply(Long id) { diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IFormalSettlementService.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IFormalSettlementService.java index 34c5649b..af7c759c 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IFormalSettlementService.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IFormalSettlementService.java @@ -18,6 +18,7 @@ import org.springblade.transport.pojo.vo.PreSettlementVO; import org.springblade.transport.pojo.entity.PreSettlement; import org.springblade.transport.pojo.dto.PreSettlementDetailAdjustRequest; import org.springblade.transport.pojo.entity.FormalSettlementDetailFee; +import java.math.BigDecimal; import java.util.Collection; import java.util.List; import org.springblade.transport.pojo.dto.FormalSettlementPaymentRequest; @@ -62,6 +63,28 @@ public interface IFormalSettlementService extends BaseService */ String doSyncKingdee(Long id); + /** + * 发票登记确认推送金蝶(先票后款路径B:结算单审批通过后"发票登记"关联收票单 → 指定应付)。 + * 前置:付款结算单 + 审批通过 + 财务应付单已同步;对本单认领且收票单已推(pushed)的 + * 逐单调 A302-6 指定应付,单笔失败整体报因(已指定的幂等跳过,可重试) + * + * @param settlementId 正式结算单id + * @return 推送结果描述 + */ + String pushInvoiceAssignKingdee(Long settlementId); + + /** + * 重推本单认领的收票登记(查看页「重新推送」入口,failed/pushed 通吃,assigned 拦截): + * 前置付款结算单 + 审批通过 + 财务应付单已同步;先复用收票推送状态机重推金蝶收票单(A303-2, + * A801-3 幂等预检防重复保存),再组装应付单防重键逐单 A302-6 指定应付(幂等) + * + * @param id 正式结算单id + * @param invoiceNo 认领发票号 + * @param matchedAmount 匹配结算单金额(可空:空不修改;非空校验 0~发票金额 后更新发票行并重建认领分摊) + * @return 重推结果描述 + */ + String repushClaimedInvoice(Long id, String invoiceNo, BigDecimal matchedAmount); + /** * 独立事务落"生成失败"状态并记失败原因(在推送事务回滚、行锁释放后执行,与付款/收票口径统一); * 仅更新审批通过且未同步的单据,未审批/已同步/已删除单据自动跳过,保持原状态 diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IInvoiceReceiptService.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IInvoiceReceiptService.java index c8756876..0e546a1b 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IInvoiceReceiptService.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IInvoiceReceiptService.java @@ -28,6 +28,7 @@ package org.springblade.transport.service; import com.baomidou.mybatisplus.core.metadata.IPage; import jakarta.servlet.http.HttpServletResponse; import org.springblade.core.mp.base.BaseService; +import org.springblade.thirdparty.bfi.pojo.vo.BfiInvoicePoolItemVO; import org.springblade.transport.pojo.dto.InvoiceReceiptSaveRequest; import org.springblade.transport.pojo.dto.InvoiceReceiptStatusRequest; import org.springblade.transport.pojo.dto.KingdeeInvoicePoolQueryDTO; @@ -105,10 +106,10 @@ public interface IInvoiceReceiptService extends BaseService { void voidBill(InvoiceReceiptStatusRequest request); /** - * 批量推送金蝶收票单(图05 环节②③:A303-2 收票单批量保存 + A302-6 收票指定应付)。 - *

逐单独立成败:前置校验(审批通过、已绑定镜像、查验通过、恰好分摊一张应付结算单)→ - * A102-4 预检财务应付单已生成 → A801-3 幂等预检(命中跳过保存续推指定)→ A303-2 保存 → - * 立即 A302-6 指定应付;成功置 assigned 并锁单,失败置 failed 提示人工处理后重推。 + * 批量推送金蝶收票单(A303-2 收票单批量保存,先票后款:推送即终态 pushed)。 + *

逐单独立成败:前置校验(审批通过、已绑定镜像、查验通过、已选结算组织)→ + * A801-3 幂等预检(命中跳过保存)→ A303-2 保存(分录=发票明细行)→ 终态 pushed; + * A302-6 指定应付由付款结算单侧在财务应付单(A102-2)生成后按认领关系执行 * * @param ids 收票登记id列表 * @return 执行摘要(逐单失败原因) @@ -117,7 +118,7 @@ public interface IInvoiceReceiptService extends BaseService { /** * 自动推送路径批量推送金蝶收票单(审批通过事件 / 定时重推专用): - * 前置拦截(未绑定镜像/查验未通过/前置应付单未生成等)一并置 failed + 原因,纳入定时重推循环 + * 前置拦截(未绑定镜像/查验未通过/未选结算组织等)一并置 failed + 原因,纳入定时重推循环 * * @param ids 收票登记id列表 * @param autoSync 自动推送标记(前置拦截置 failed) @@ -125,6 +126,54 @@ public interface IInvoiceReceiptService extends BaseService { */ String pushKingdeeReceivedInvoice(List ids, boolean autoSync); + /** + * 发票明细查询(先票后款:收票登记明细区块按票号带出):读票池镜像 items_json, + * 镜像无明细按票号回查 A801-1 补写后重读;票未登记时直接取窄查返回行明细 + * + * @param invoiceNo 发票号码 + * @return 发票明细行 + */ + List invoiceDetailQuery(String invoiceNo); + + /** + * 结算单认领收票单候选(先票后款路径A/B 选票数据源):审批通过 + 未被有效应付结算单认领, + * 关键字匹配发票号/销方/购方,分页返回 + * + * @param keyword 关键字(发票号/销方/购方),空不过滤 + * @param page 分页参数 + * @return 候选收票登记分页 + */ + IPage claimCandidates(String keyword, IPage page); + + /** + * 结算单已认领收票登记列表(先票后款:发票登记对话框打开时回显已认领票): + * 按认领关系查收票登记,与认领候选行结构一致(同一 InvoiceReceipt 实体) + * + * @param settlementId 正式结算单id + * @return 本单已认领的收票登记 + */ + List claimedReceipts(Long settlementId); + + /** + * 结算单收票认领关系同步(先票后款:结算单保存/认领后由结算单侧调用): + * 按行发票号匹配审批通过收票登记,一票一结算单校验(已挂其他有效应付结算单拦截), + * 删旧建新重建本结算单认领关系(认领金额=发票金额全额)并刷新结算单收票状态 + * + * @param settlementId 正式结算单id + * @param invoiceNos 认领发票号码集合(结算单发票信息行发票号) + */ + void syncSettlementReceiptRelations(Long settlementId, List invoiceNos); + + /** + * 结算单认领收票登记逐单 A302-6 指定应付(先票后款路径:A102-2 成功后调用): + * 收票单 pushed → 指定 → assigned;未推送/已指定跳过;单笔失败收集汇总返回,不阻断结算同步 + * + * @param settlementId 正式结算单id + * @param apBillSrcCode 财务应付单防重键(A102-2 的 awrp_src_code) + * @return 失败明细(空串表示全部成功或无需指定) + */ + String assignClaimedReceipts(Long settlementId, String apBillSrcCode); + /** * 重推金蝶推送失败(kingdeePushStatus=failed 且审批通过)的收票登记:逐单独立成败,失败不中断。 * 只扫 failed 不扫 unsynced——存量"审批通过但从未推送"的历史单不自动补推,仍走手动按钮 @@ -135,7 +184,7 @@ public interface IInvoiceReceiptService extends BaseService { /** * 定时回写金蝶收票单结果(A801-3):金蝶侧删除/红冲/作废 → 回收为 failed 提示人工(不自动作废登记); - * pushed(保存成功后中断)→ 指定补偿;assigned 仅补登缺失的收票单号 + * pushed 且已认领 → 指定补偿(重试引擎),未认领跳过;assigned 仅补登缺失的收票单号 * * @return 执行摘要 */ diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/FormalSettlementServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/FormalSettlementServiceImpl.java index 7900ef9d..c1ddc927 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/FormalSettlementServiceImpl.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/FormalSettlementServiceImpl.java @@ -230,9 +230,72 @@ public class FormalSettlementServiceImpl extends BaseServiceImpl voPage = result.convert(item -> this.toVO(item, writeOffAmounts)); fillAttachmentSyncStatus(voPage.getRecords()); fillKingdeeLockStatus(voPage.getRecords()); + fillKingdeeAssignStatus(voPage.getRecords()); return voPage; } + /** + * 填充已指定应付聚合态(仅付款结算单):按本页结算单一次批量查认领关系与收票登记,避免 N+1。 + * 聚合规则:任一指定失败 → failed;全部已指定 → assigned;其余(有认领但未指定/未推送)→ unspecified; + * 无认领收票或全部认领票无效不填充,前端展示"-" + * + * @param rows 本页 VO 列表 + */ + private void fillKingdeeAssignStatus(List rows) { + // 已指定应付仅付款结算单(先票后款 A302-6)有,收款结算单不参与聚合,避免无意义查询与字段下发 + List payableRows = rows.stream() + .filter(row -> "payable".equals(row.getSettlementType())) + .toList(); + if (Func.isEmpty(payableRows)) { + return; + } + List relations = invoiceReceiptSettlementMapper.selectList( + Wrappers.lambdaQuery() + .in(InvoiceReceiptSettlement::getFormalSettlementId, + payableRows.stream().map(FormalSettlementVO::getId).filter(Objects::nonNull).toList())); + if (relations.isEmpty()) { + return; + } + List receiptIds = relations.stream() + .map(InvoiceReceiptSettlement::getInvoiceReceiptId).filter(Objects::nonNull).distinct().toList(); + Map receiptById = invoiceReceiptMapper.selectByIds(receiptIds).stream() + .filter(receipt -> !TransportBusiConstant.VOIDED.equals(receipt.getApprovalStatus()) + && !Objects.equals(receipt.getIsDeleted(), 1)) + .collect(Collectors.toMap(InvoiceReceipt::getId, receipt -> receipt, (left, right) -> left)); + Map> receiptsBySettlement = new HashMap<>(); + for (InvoiceReceiptSettlement relation : relations) { + InvoiceReceipt receipt = receiptById.get(relation.getInvoiceReceiptId()); + if (receipt == null) { + continue; + } + receiptsBySettlement.computeIfAbsent(relation.getFormalSettlementId(), key -> new ArrayList<>()) + .add(receipt); + } + for (FormalSettlementVO vo : payableRows) { + List claimed = receiptsBySettlement.get(vo.getId()); + if (Func.isEmpty(claimed)) { + continue; + } + List statuses = claimed.stream() + .map(receipt -> Func.toStrWithEmpty(receipt.getKingdeePushStatus(), + KingdeeBfiConstant.RECEIVED_PUSH_UNSYNCED)) + .toList(); + String status = statuses.contains(KingdeeBfiConstant.RECEIVED_PUSH_FAILED) ? "failed" + : statuses.stream().allMatch(KingdeeBfiConstant.RECEIVED_PUSH_ASSIGNED::equals) + ? "assigned" : "unspecified"; + vo.setKingdeeAssignStatus(status); + // 指定失败行聚合各票失败原因,供悬浮展示 + if ("failed".equals(status)) { + vo.setKingdeeAssignFailReason(claimed.stream() + .filter(receipt -> KingdeeBfiConstant.RECEIVED_PUSH_FAILED.equals( + Func.toStrWithEmpty(receipt.getKingdeePushStatus(), KingdeeBfiConstant.RECEIVED_PUSH_UNSYNCED))) + .map(receipt -> "发票" + Func.toStrWithEmpty(receipt.getInvoiceNo(), String.valueOf(receipt.getId())) + + ":" + Func.toStrWithEmpty(receipt.getKingdeePushMsg(), "推送失败")) + .collect(Collectors.joining(";"))); + } + } + } + /** * 填充金蝶锁状态标识(locked-锁定中/failed-锁定失败;unlocked 终态不返回): * 锁表按本页源单一次批量查询,避免 N+1;locked 时前端可禁用编辑/作废, @@ -454,13 +517,14 @@ public class FormalSettlementServiceImpl extends BaseServiceImpl receiptMap = Map.of(); if (!receiptRelations.isEmpty()) { List receiptIds = receiptRelations.stream() .map(InvoiceReceiptSettlement::getInvoiceReceiptId) .filter(Objects::nonNull) .distinct() .toList(); - Map receiptMap = receiptIds.isEmpty() ? Map.of() + receiptMap = receiptIds.isEmpty() ? Map.of() : invoiceReceiptMapper.selectByIds(receiptIds).stream() .filter(receipt -> !TransportBusiConstant.VOIDED.equals(receipt.getApprovalStatus()) && !Objects.equals(receipt.getIsDeleted(), 1)) @@ -483,6 +547,17 @@ public class FormalSettlementServiceImpl extends BaseServiceImpl invoices = new ArrayList<>(invoiceMap.values()); + // 发票行回显收票单金蝶推送状态(是否已指定应付展示用):认领收票按票号匹配本单已认领收票登记 + Map receiptByInvoiceNo = receiptMap.values().stream() + .filter(receipt -> Func.isNotBlank(receipt.getInvoiceNo())) + .collect(Collectors.toMap(InvoiceReceipt::getInvoiceNo, Function.identity(), (left, right) -> left)); + for (FormalSettlementInvoice invoice : invoices) { + InvoiceReceipt receipt = receiptByInvoiceNo.get(invoice.getInvoiceNo()); + if (receipt != null) { + invoice.setKingdeePushStatus(receipt.getKingdeePushStatus()); + invoice.setKingdeePushMsg(receipt.getKingdeePushMsg()); + } + } for (int index = 0; index < invoices.size(); index++) { invoices.get(index).setLineNo(index + 1); } @@ -1008,6 +1083,8 @@ public class FormalSettlementServiceImpl extends BaseServiceImpl entryGroups = kingdeeApBillSyncSupport.resolveEntryGroups(settlement, details, context); KingdeeApBillSyncSupport.BfiApBillPushOutcome outcome = kingdeeApBillSyncSupport.pushApBill( apBillSrcCode, settlement, entryGroups, settlement.getFormalSettlementNo(), context); @@ -1042,11 +1119,143 @@ public class FormalSettlementServiceImpl extends BaseServiceImpl details = detailMapper.selectList(Wrappers.lambdaQuery() + .eq(FormalSettlementDetail::getFormalSettlementId, settlementId) + .eq(FormalSettlementDetail::getIsDeleted, 0) + .orderByAsc(FormalSettlementDetail::getLineNo)); + if (Func.isEmpty(details)) { + throw new ServiceException("推送失败:正式结算单无结算明细,无法组装财务应付单防重键"); + } + String apBillSrcCode = kingdeeApBillSyncSupport.apBillSrcCode(settlement, details); + String assignFailures = invoiceReceiptService.assignClaimedReceipts(settlement.getId(), apBillSrcCode); + if (Func.isNotBlank(assignFailures)) { + log.warn("正式结算单[{}]发票登记推送金蝶存在失败:{}", settlement.getFormalSettlementNo(), assignFailures); + throw new ServiceException("发票登记推送金蝶完成,存在失败(已指定的幂等跳过,处理后可重试):" + assignFailures); + } + log.info("正式结算单[{}]发票登记推送金蝶完成:认领收票单已全部指定应付,应付单防重键={}", + settlement.getFormalSettlementNo(), apBillSrcCode); + return "结算单[" + settlement.getFormalSettlementNo() + "]发票登记推送金蝶成功:认领收票单已指定应付"; + } + + @Override + public String repushClaimedInvoice(Long id, String invoiceNo, BigDecimal matchedAmount) { + FormalSettlement settlement = lockedExisting(id); + if (!"payable".equals(settlement.getSettlementType())) { + throw new ServiceException("仅付款结算单允许重推认领收票单"); + } + if (!TransportBusiConstant.APPROVED.equals(settlement.getApprovalStatus())) { + throw new ServiceException("仅审批通过的付款结算单允许重推认领收票单"); + } + if (!KingdeeBfiConstant.KINGDEE_STATUS_SYNCED.equals(settlement.getKingdeeSyncStatus())) { + throw new ServiceException("请先同步金蝶财务应付单后再重推认领收票单"); + } + String trimmedInvoiceNo = requiredText(invoiceNo, "发票号"); + InvoiceReceipt receipt = invoiceReceiptMapper.selectOne(Wrappers.lambdaQuery() + .eq(InvoiceReceipt::getInvoiceNo, trimmedInvoiceNo).last("LIMIT 1")); + if (receipt == null || Objects.equals(receipt.getIsDeleted(), 1)) { + throw new ServiceException("收票登记不存在:发票" + trimmedInvoiceNo); + } + // 认领关系校验:该票必须认领在本单名下,防止跨结算单误推 + Long relationCount = invoiceReceiptSettlementMapper.selectCount(Wrappers.lambdaQuery() + .eq(InvoiceReceiptSettlement::getFormalSettlementId, id) + .eq(InvoiceReceiptSettlement::getInvoiceReceiptId, receipt.getId())); + if (relationCount == null || relationCount == 0) { + throw new ServiceException("发票" + trimmedInvoiceNo + "未认领在本结算单名下,不允许重推"); + } + String pushStatus = Func.toStrWithEmpty(receipt.getKingdeePushStatus(), KingdeeBfiConstant.RECEIVED_PUSH_UNSYNCED); + if (KingdeeBfiConstant.RECEIVED_PUSH_ASSIGNED.equals(pushStatus)) { + throw new ServiceException("发票" + trimmedInvoiceNo + "已指定应付,无需重复推送"); + } + // 匹配结算单金额随重推一并生效:更新发票行并重建本单认领分摊(收票覆盖度随之刷新) + if (matchedAmount != null) { + FormalSettlementInvoice settlementInvoice = invoiceMapper.selectOne( + Wrappers.lambdaQuery() + .eq(FormalSettlementInvoice::getFormalSettlementId, id) + .eq(FormalSettlementInvoice::getInvoiceNo, trimmedInvoiceNo) + .last("LIMIT 1")); + if (settlementInvoice == null) { + throw new ServiceException("发票" + trimmedInvoiceNo + "不在本结算单发票信息中,无法修改匹配金额"); + } + if (matchedAmount.compareTo(BigDecimal.ZERO) < 0) { + throw new ServiceException("发票" + trimmedInvoiceNo + "匹配结算单金额不能为负数"); + } + if (matchedAmount.compareTo(money(settlementInvoice.getInvoiceAmount())) > 0) { + throw new ServiceException("发票" + trimmedInvoiceNo + "匹配结算单金额不能超过发票金额" + + money(settlementInvoice.getInvoiceAmount())); + } + invoiceMapper.update(null, Wrappers.lambdaUpdate() + .eq(FormalSettlementInvoice::getId, settlementInvoice.getId()) + .set(FormalSettlementInvoice::getMatchedAmount, matchedAmount)); + // 关系重建语义为删全量重建,须传本单全部发票号,只传单票会清掉其他认领关系 + List allInvoiceNos = invoiceMapper.selectList(Wrappers.lambdaQuery() + .eq(FormalSettlementInvoice::getFormalSettlementId, id)).stream() + .map(FormalSettlementInvoice::getInvoiceNo).filter(Func::isNotBlank).distinct().toList(); + invoiceReceiptService.syncSettlementReceiptRelations(id, allInvoiceNos); + } + // 第一步:复用收票推送状态机重推金蝶收票单(A303-2,A801-3 幂等预检防重复保存,单笔事务独立回滚) + String pushResult = invoiceReceiptService.pushKingdeeReceivedInvoice(List.of(receipt.getId())); + // 第二步:收票单就绪后按应付单防重键 A302-6 指定应付(幂等,已指定的跳过) + List details = detailMapper.selectList(Wrappers.lambdaQuery() + .eq(FormalSettlementDetail::getFormalSettlementId, id) + .eq(FormalSettlementDetail::getIsDeleted, 0) + .orderByAsc(FormalSettlementDetail::getLineNo)); + if (Func.isEmpty(details)) { + throw new ServiceException("推送失败:正式结算单无结算明细,无法组装财务应付单防重键"); + } + String apBillSrcCode = kingdeeApBillSyncSupport.apBillSrcCode(settlement, details); + String assignFailures = invoiceReceiptService.assignClaimedReceipts(id, apBillSrcCode); + // 复读最新状态组装结果:assigned 即成功;收票单仍未推送成功按失败报因;pushed 未指定报因可重试 + InvoiceReceipt latest = invoiceReceiptMapper.selectById(receipt.getId()); + String latestStatus = Func.toStrWithEmpty(latest.getKingdeePushStatus(), KingdeeBfiConstant.RECEIVED_PUSH_UNSYNCED); + if (KingdeeBfiConstant.RECEIVED_PUSH_ASSIGNED.equals(latestStatus)) { + log.info("正式结算单[{}]认领发票[{}]重推完成:已指定应付", settlement.getFormalSettlementNo(), trimmedInvoiceNo); + return "发票" + trimmedInvoiceNo + "重推成功:已指定应付"; + } + if (!KingdeeBfiConstant.RECEIVED_PUSH_PUSHED.equals(latestStatus)) { + throw new ServiceException("发票" + trimmedInvoiceNo + "重推收票单失败:" + pushResult); + } + if (Func.isNotBlank(assignFailures)) { + throw new ServiceException("发票" + trimmedInvoiceNo + "收票单已推送金蝶,指定应付失败:" + assignFailures + + "(已指定的自动跳过,可重试)"); + } + log.info("正式结算单[{}]认领发票[{}]重推完成:收票单已推送金蝶并指定应付", settlement.getFormalSettlementNo(), trimmedInvoiceNo); + return "发票" + trimmedInvoiceNo + "重推成功:已指定应付"; + } + @Override public String syncKingdeeArResult() { return kingdeeSyncLockSupport.runSingleFlight(AR_RESULT_SYNC_LOCK_PREFIX, bfiProperties.getArResult().getLockLeaseSeconds(), @@ -1291,17 +1500,23 @@ public class FormalSettlementServiceImpl extends BaseServiceImpl invoices = request.getInvoices(); Set invoiceNumbers = new LinkedHashSet<>(); - BigDecimal matchedTotal = BigDecimal.ZERO; for (FormalSettlementSaveRequest.Invoice item : invoices) { String invoiceNo = item == null ? "" : requiredText(item.getInvoiceNo(), "发票号"); if (!invoiceNumbers.add(invoiceNo)) throw new ServiceException("发票号" + invoiceNo + "重复"); - matchedTotal = matchedTotal.add(money(item.getMatchedAmount())); + // 匹配结算单金额支持手动修改:后端兜底校验 0 ≤ 匹配金额 ≤ 发票金额(匹配合计不设上限,两个口径) + BigDecimal matched = money(item.getMatchedAmount()); + BigDecimal invoiceAmount = money(item.getInvoiceAmount()); + if (matched.compareTo(BigDecimal.ZERO) < 0) { + throw new ServiceException("发票" + invoiceNo + "匹配结算单金额不能为负数"); + } + if (matched.compareTo(invoiceAmount) > 0) { + throw new ServiceException("发票" + invoiceNo + "匹配结算单金额不能超过发票金额" + invoiceAmount); + } } // 认领闸门:确认前按票号重查金蝶复核可用性,选中到确认之间票被认领/使用/红冲在此拦截 invoiceReceiptService.assertInvoicesClaimable(invoiceNumbers); - if (matchedTotal.compareTo(money(settlement.getSettlementAmount())) > 0) { - throw new ServiceException("发票匹配结算单金额合计不能超过结算金额"); - } + // 先票后款:认领金额=发票全额,发票合计可超过结算金额(发票金额与结算金额本就是两个口径), + // 不再校验"匹配合计不能超过结算金额";收票状态以是否覆盖结算金额判定(invoiceStatus) List existingInvoices = invoiceMapper.selectList( Wrappers.lambdaQuery().eq(FormalSettlementInvoice::getFormalSettlementId, settlement.getId())); Set existingNumbers = existingInvoices.stream().map(FormalSettlementInvoice::getInvoiceNo) @@ -1357,6 +1572,13 @@ public class FormalSettlementServiceImpl extends BaseServiceImpl allInvoiceNos = new LinkedHashSet<>(existingNumbers); + allInvoiceNos.addAll(invoiceNumbers); + invoiceReceiptService.syncSettlementReceiptRelations(settlement.getId(), new ArrayList<>(allInvoiceNos)); + } } private String createPayment(FormalSettlement settlement, BigDecimal appliedAmount, String remark) { @@ -1804,9 +2026,8 @@ public class FormalSettlementServiceImpl extends BaseServiceImpl 0) { - throw new ServiceException("发票匹配结算单金额合计不能超过结算金额"); - } + // 先票后款:认领金额=发票全额,发票合计可超过结算金额(发票金额与结算金额本就是两个口径), + // 不再校验"匹配合计不能超过结算金额";收票状态以是否覆盖结算金额判定(invoiceStatus) invoiceMapper.delete(Wrappers.lambdaQuery() .eq(FormalSettlementInvoice::getFormalSettlementId, settlement.getId())); int lineNo = 1; @@ -1827,6 +2048,10 @@ public class FormalSettlementServiceImpl extends BaseServiceImpl(invoiceNumbers)); + } } private void refreshSettlementAmount(FormalSettlement settlement) { @@ -2138,7 +2363,8 @@ public class FormalSettlementServiceImpl extends BaseServiceImpl goodsNames = new LinkedHashSet<>(); LinkedHashSet goodsTypes = new LinkedHashSet<>(); if (waybill != null) { @@ -2389,7 +2615,8 @@ public class FormalSettlementServiceImpl extends BaseServiceImpl= 0 ? "completed" : "partial"; } private record PaymentSummary(BigDecimal appliedAmount, BigDecimal paidAmount) { diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/InvoiceApplicationServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/InvoiceApplicationServiceImpl.java index 6dabb44e..f2f9e7ac 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/InvoiceApplicationServiceImpl.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/InvoiceApplicationServiceImpl.java @@ -43,6 +43,7 @@ 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.BfiBillSaveResultItemVO; import org.springblade.thirdparty.bfi.pojo.vo.BfiBillSaveResultVO; import org.springblade.thirdparty.bfi.pojo.vo.BfiInvoiceApplyOpenApiBillVO; import org.springblade.thirdparty.bfi.pojo.vo.BfiInvoiceApplyOpenApiInvoiceVO; @@ -733,9 +734,11 @@ public class InvoiceApplicationServiceImpl extends BaseServiceImpl sheetBillNos = sheets.stream().map(sheet -> this.sheetBillNo(entity, sheet, sheets.size())).toList(); // A303-4 幂等预检:按全部候选 billno 查询金蝶是否已生成开票申请单(防上次推送成功但本地回滚的断链重推), - // 已生成的张次幂等跳过,未生成的张次重新推送 + // 实测返回 billno 与推送值完全一致,按 billno 精确匹配;部分张次已生成时不可整单提前返回,须断点续推剩余张次 BfiInvoiceApplyQueryDTO idempotentQuery = new BfiInvoiceApplyQueryDTO(); idempotentQuery.setPageNo(1); idempotentQuery.setPageSize(Math.max(sheets.size(), 10)); idempotentQuery.setBillno(sheetBillNos); - Set existSheetBillNos = new LinkedHashSet<>(); + Map existApplyByBillNo = new LinkedHashMap<>(); BfiPageVO existPage = bfiService.queryInvoiceApply(idempotentQuery); if (existPage != null && Func.isNotEmpty(existPage.getRows())) { for (BfiInvoiceApplyVO existApply : existPage.getRows()) { if (existApply != null && Func.isNotBlank(existApply.getBillno()) && sheetBillNos.contains(existApply.getBillno())) { - existSheetBillNos.add(existApply.getBillno()); - // 金蝶已生成:直接回填视为同步成功(勾稽纠偏),星瀚内码一并回填(A902 附件上传定位用) - log.info("开票申请[{}]经 A303-4 查得金蝶开票申请单已生成(单据编号[{}],星瀚内码[{}]),跳过重复推送直接回填", + existApplyByBillNo.put(existApply.getBillno(), existApply); + log.info("开票申请[{}]经 A303-4 查得金蝶开票申请单已生成(单据编号[{}],星瀚内码[{}]),幂等跳过推送", entity.getApplicationNo(), existApply.getBillno(), existApply.getId()); - entity.setKingdeeBillNo(existApply.getBillno()); - entity.setKingdeeBillId(existApply.getId() == null ? null : String.valueOf(existApply.getId())); - entity.setKingdeeStatus(KingdeeBfiConstant.KINGDEE_STATUS_SYNCED); - entity.setSyncedTime(LocalDateTime.now()); - updateById(entity); - record(entity.getId(), "sync", "同步金蝶(勾稽回填)", TransportBusiConstant.APPROVED, TransportBusiConstant.APPROVED, null, existApply.getBillno()); - return existApply.getBillno(); } } } + // 全部张次金蝶均已生成:直接勾稽回填视为同步成功(星瀚内码一并回填,A902 附件上传定位用) + if (!existApplyByBillNo.isEmpty() && existApplyByBillNo.keySet().containsAll(sheetBillNos)) { + entity.setKingdeeBillNo(String.join(",", existApplyByBillNo.keySet())); + entity.setKingdeeBillId(existApplyByBillNo.values().stream() + .map(BfiInvoiceApplyVO::getId).filter(Objects::nonNull).findFirst().map(String::valueOf).orElse(null)); + entity.setKingdeeStatus(KingdeeBfiConstant.KINGDEE_STATUS_SYNCED); + entity.setSyncedTime(LocalDateTime.now()); + updateById(entity); + record(entity.getId(), "sync", "同步金蝶(勾稽回填)", TransportBusiConstant.APPROVED, TransportBusiConstant.APPROVED, null, entity.getKingdeeBillNo()); + return entity.getKingdeeBillNo(); + } + // 部分张次已生成:先回填星瀚内码,已生成张次在下方逐张循环幂等跳过,未生成张次继续推送 + Set existSheetBillNos = existApplyByBillNo.keySet(); + if (Func.isBlank(entity.getKingdeeBillId())) { + entity.setKingdeeBillId(existApplyByBillNo.values().stream() + .map(BfiInvoiceApplyVO::getId).filter(Objects::nonNull).findFirst().map(String::valueOf).orElse(null)); + } // 组装 A303-1 开票申请单(字段映射见设计文档 6.3/6.4) if (Func.isBlank(entity.getIssuerName())) { throw new ServiceException("推送金蝶开票申请失败:开票单位(销方)为空"); @@ -879,7 +891,12 @@ public class InvoiceApplicationServiceImpl extends BaseServiceImpl requestedRows = distinctSettlementRows( request.getSettlements()); Map settlementMap = lockSettlements(requestedRows.keySet().stream().sorted().toList()); List settlements = requestedRows.keySet().stream().map(settlementMap::get).toList(); - assertCompatible(settlements); - + // 历史兼容链路(传 settlements)保留原整体一致性校验;先票后款链路(不传)跳过 + if (!requestedRows.isEmpty()) { + assertCompatible(settlements); + } for (Map.Entry entry : requestedRows.entrySet()) { FormalSettlement settlement = settlementMap.get(entry.getKey()); BigDecimal allocated = nonNegative(entry.getValue().getAllocatedInvoiceAmount(), "分摊发票金额"); @@ -418,13 +422,32 @@ public class InvoiceReceiptServiceImpl extends BaseServiceImpllambdaQuery() - .eq(InvoiceReceiptSettlement::getInvoiceReceiptId, entity.getId())); - saveRelations(entity.getId(), requestedRows, settlementMap); - Set refreshIds = new LinkedHashSet<>(oldSettlementIds); - refreshIds.addAll(settlementMap.keySet()); - refreshIds.forEach(this::refreshSettlementInvoiceStatus); + if (requestedRows.isEmpty()) { + // 未传 settlements:不动既有分摊关系(先票后款链路,关系只由结算单侧重建维护) + log.info("收票登记[{}]保存未关联结算单(先票后款链路),既有分摊关系保持不动", entity.getInvoiceNo()); + } else { + settlementRelationMapper.delete(Wrappers.lambdaQuery() + .eq(InvoiceReceiptSettlement::getInvoiceReceiptId, entity.getId())); + saveRelations(entity.getId(), requestedRows, settlementMap); + Set refreshIds = new LinkedHashSet<>(oldSettlementIds); + refreshIds.addAll(settlementMap.keySet()); + refreshIds.forEach(this::refreshSettlementInvoiceStatus); + } record(entity.getId(), creating ? "create" : "save", creating ? "创建草稿" : "保存草稿", entity.getApprovalStatus(), entity.getApprovalStatus(), null, entity.getKingdeeReceivedInvoiceNo()); return entity.getId(); @@ -536,11 +564,10 @@ public class InvoiceReceiptServiceImpl extends BaseServiceImpl - * 编排流程:参数去重校验 → 批量组装推送计划(分摊/镜像/结算单/结算明细一次取回防 N+1)→ - * A102-4 应付单已生成预检 → A801-3 幂等预检(金蝶已有收票单则跳过保存续推指定)→ - * 逐单推送(单笔失败不中断整批)→ 汇总成功/失败笔数返回 + * 编排流程:参数去重校验 → 批量组装推送计划(镜像一次取回防 N+1)→ + * A801-3 幂等预检(金蝶已有收票单则跳过保存)→ 逐单推送(单笔失败不中断整批)→ 汇总成功/失败笔数返回 * * @param ids 收票登记主键集合 * @param autoSync 自动推送路径(审批通过事件/定时重推):前置拦截置 failed + 原因纳入重推循环; @@ -561,56 +588,23 @@ public class InvoiceReceiptServiceImpl extends BaseServiceImpl> relationsByReceipt = settlementRelationMapper.selectList( - Wrappers.lambdaQuery() - .in(InvoiceReceiptSettlement::getInvoiceReceiptId, distinctIds) - .orderByAsc(InvoiceReceiptSettlement::getCreateTime)).stream() - .collect(Collectors.groupingBy(InvoiceReceiptSettlement::getInvoiceReceiptId)); - // 2) 票池镜像(A801-1 同步落库的发票数据),提供明细行 itemsJson、票种编码、购方单位等推送必填字段 + // 批量组装推送计划:票池镜像一次取回防 N+1(先票后款:不再依赖分摊结算单/结算明细与 A102-4 应付单预检) List mirrorIds = receipts.stream().map(InvoiceReceipt::getKingdeeInvoicePoolId) .filter(Objects::nonNull).distinct().toList(); Map mirrorById = mirrorIds.isEmpty() ? Map.of() : invoicePoolMapper.selectBatchIds(mirrorIds).stream() .collect(Collectors.toMap(KingdeeInvoicePool::getId, Function.identity(), (first, second) -> first)); - // 3) 分摊关系指向的正式结算单,后续校验须为审批通过的应付类型结算单 - List settlementIds = relationsByReceipt.values().stream().flatMap(List::stream) - .map(InvoiceReceiptSettlement::getFormalSettlementId).filter(Objects::nonNull).distinct().toList(); - Map settlementById = settlementIds.isEmpty() ? Map.of() - : formalSettlementMapper.selectBatchIds(settlementIds).stream() - .collect(Collectors.toMap(FormalSettlement::getId, Function.identity(), (first, second) -> first)); - // 4) 应付结算单的明细行(按行号升序),用于组装 A302-6 指定应付的防重键 apBillSrcCode - List payableSettlementIds = settlementById.values().stream() - .filter(settlement -> "payable".equals(settlement.getSettlementType())) - .map(FormalSettlement::getId).toList(); - Map> detailsBySettlement = payableSettlementIds.isEmpty() ? Map.of() - : formalSettlementDetailMapper.selectList(Wrappers.lambdaQuery() - .in(FormalSettlementDetail::getFormalSettlementId, payableSettlementIds) - .eq(FormalSettlementDetail::getIsDeleted, 0) - .orderByAsc(FormalSettlementDetail::getLineNo)).stream() - .collect(Collectors.groupingBy(FormalSettlementDetail::getFormalSettlementId)); - log.info("推送金蝶收票单批量取数完成:分摊关系 {} 条,票池镜像 {} 个,结算单 {} 张(应付 {} 张),结算明细 {} 行", - relationsByReceipt.size(), mirrorById.size(), settlementById.size(), payableSettlementIds.size(), - detailsBySettlement.values().stream().mapToInt(List::size).sum()); - // 5) 逐单组装推送计划:状态机/绑定/查验/一票一应付/防重键校验,不满足项写入 blockedReason 不抛异常(逐单报因) + log.info("推送金蝶收票单批量取数完成:票池镜像 {} 个", mirrorById.size()); + // 逐单组装推送计划:状态机/绑定/查验/结算组织校验,不满足项写入 blockedReason 不抛异常(逐单报因) Map planByReceipt = new LinkedHashMap<>(); for (InvoiceReceipt receipt : receipts) { - planByReceipt.put(receipt.getId(), buildReceivedInvoicePushPlan(receipt, - relationsByReceipt.getOrDefault(receipt.getId(), List.of()), mirrorById, settlementById, detailsBySettlement)); + planByReceipt.put(receipt.getId(), buildReceivedInvoicePushPlan(receipt, mirrorById)); } long blockedCount = planByReceipt.values().stream().filter(plan -> plan.blockedReason() != null).count(); log.info("推送金蝶收票单计划组装完成:本批 {} 笔,前置校验拦截 {} 笔", planByReceipt.size(), blockedCount); - // A102-4 应付单已生成预检:本批防重键一次批量查询,缺单的登记逐单提示先完成付款侧同步 - List apSrcCodes = planByReceipt.values().stream() - .filter(plan -> plan.blockedReason() == null) - .map(ReceivedInvoicePushPlan::apBillSrcCode).filter(Func::isNotBlank).distinct().toList(); - Map apBillNoBySrcCode = apSrcCodes.isEmpty() ? Map.of() - : kingdeeApBillSyncSupport.queryApBillExistNos(apSrcCodes); - log.info("推送金蝶收票单 A102-4 预检:待校验防重键 {} 个,已生成财务应付单 {} 个", apSrcCodes.size(), apBillNoBySrcCode.size()); - // A801-3 幂等预检:按购方单位(接口必填)分组回看窗口翻页,本地按防重键匹配(金蝶已存在则跳过保存续推指定) + // A801-3 幂等预检:按购方单位(接口必填)分组回看窗口翻页,本地按防重键匹配(金蝶已存在则跳过保存) Map kingdeeRowByInvoiceNo = queryKingdeeReceivedInvoiceRows(planByReceipt); - log.info("推送金蝶收票单 A801-3 预检:金蝶已存在收票单 {} 笔,命中单据将跳过保存续推指定", kingdeeRowByInvoiceNo.size()); + log.info("推送金蝶收票单 A801-3 预检:金蝶已存在收票单 {} 笔,命中单据将跳过保存", kingdeeRowByInvoiceNo.size()); KingdeeSyncContext context = KingdeeSyncContext.empty(); int success = 0; List failures = new ArrayList<>(); @@ -618,7 +612,7 @@ public class InvoiceReceiptServiceImpl extends BaseServiceImpl pushOneReceivedInvoice( - planByReceipt.get(receipt.getId()), kingdeeRowByInvoiceNo, apBillNoBySrcCode, context, autoSync)); + planByReceipt.get(receipt.getId()), kingdeeRowByInvoiceNo, context, autoSync)); success++; } catch (RuntimeException pushException) { failures.add("发票" + Func.toStrWithEmpty(receipt.getInvoiceNo(), String.valueOf(receipt.getId())) @@ -672,20 +666,19 @@ public class InvoiceReceiptServiceImpl extends BaseServiceImpl kingdeeRows, - Map apBillNoBySrcCode, KingdeeSyncContext context, boolean autoSync) { + KingdeeSyncContext context, boolean autoSync) { // 行锁读:同一登记并发推送串行化(锁随本单事务提交释放,事务由批量编排的 TransactionTemplate 开启) InvoiceReceipt receipt = baseMapper.selectOne(Wrappers.lambdaQuery() .eq(InvoiceReceipt::getId, plan.receipt().getId()).last("FOR UPDATE")); @@ -701,8 +694,8 @@ public class InvoiceReceiptServiceImpl extends BaseServiceImpl relations, - Map mirrorById, - Map settlementById, - Map> detailsBySettlement) { + private ReceivedInvoicePushPlan buildReceivedInvoicePushPlan(InvoiceReceipt receipt, Map mirrorById) { String pushStatus = Func.toStrWithEmpty(receipt.getKingdeePushStatus(), KingdeeBfiConstant.RECEIVED_PUSH_UNSYNCED); if (!TransportBusiConstant.APPROVED.equals(receipt.getApprovalStatus())) { return new ReceivedInvoicePushPlan(receipt, null, null, null, null, "仅审批通过的收票登记允许推送金蝶"); @@ -865,24 +833,10 @@ public class InvoiceReceiptServiceImpl extends BaseServiceImpl details = detailsBySettlement.getOrDefault(settlement.getId(), List.of()); - if (details.isEmpty()) { - return new ReceivedInvoicePushPlan(receipt, mirror, settlement, null, null, - "结算单" + settlement.getFormalSettlementNo() + "无结算明细,无法组装应付单防重键"); - } - return new ReceivedInvoicePushPlan(receipt, mirror, settlement, details, - kingdeeApBillSyncSupport.apBillSrcCode(settlement, details), null); + return new ReceivedInvoicePushPlan(receipt, mirror, null, null, null, null); } /** A801-3 幂等预检批量查询:未终态计划按购方单位分组,窗口内翻页匹配防重键 */ @@ -909,7 +863,7 @@ public class InvoiceReceiptServiceImpl extends BaseServiceImpl details, String apBillSrcCode, String blockedReason) { + FormalSettlement settlement, List details, String apBillSrcCode, String blockedReason) { + } + + // ==================== 先票后款:发票明细查询 / 认领候选 / 结算侧关系同步与指定应付 ==================== + + @Override + public List invoiceDetailQuery(String invoiceNo) { + if (StringUtil.isBlank(invoiceNo)) { + throw new ServiceException("请填写发票号码"); + } + String trimmedInvoiceNo = invoiceNo.trim(); + KingdeeInvoicePool mirror = invoicePoolMapper.selectOne(Wrappers.lambdaQuery() + .eq(KingdeeInvoicePool::getInvoiceNo, trimmedInvoiceNo)); + if (mirror != null) { + if (Func.isBlank(mirror.getItemsJson())) { + log.info("发票[{}]镜像明细为空,按票号回查 A801-1 补写", trimmedInvoiceNo); + fetchMirrorItemsFromPool(mirror); + } + List items = this.parseMirrorItems(trimmedInvoiceNo, mirror.getItemsJson()); + if (Func.isNotEmpty(items)) { + return items; + } + } + // 无镜像或补查后仍无明细:直接取窄查返回行的明细(票未登记的明细预览场景) + BfiInvoicePoolVO poolRow = queryPoolByInvoiceNo(trimmedInvoiceNo).stream() + .filter(row -> Func.isNotEmpty(row.getInvoiceNo()) && trimmedInvoiceNo.equals(row.getInvoiceNo().trim())) + .findFirst().orElse(null); + if (poolRow == null) { + throw new ServiceException("金蝶票池未查询到发票 " + trimmedInvoiceNo + ",无法查询发票明细"); + } + return Func.isEmpty(poolRow.getItems()) ? List.of() : poolRow.getItems(); + } + + @Override + public IPage claimCandidates(String keyword, IPage page) { + String trimmedKeyword = StringUtil.isBlank(keyword) ? null : keyword.trim(); + // 排除已被有效应付结算单(未删除+启用+应付类型)认领的票,关系仍指向作废结算单的可重新选 + String claimedReceiptSubSql = "SELECT r.invoice_receipt_id FROM blade_invoice_receipt_settlement r" + + " INNER JOIN blade_formal_settlement s ON s.id = r.formal_settlement_id" + + " WHERE s.is_deleted = 0 AND s.status = 1 AND s.settlement_type = 'payable'"; + return page(page, Wrappers.lambdaQuery() + .eq(InvoiceReceipt::getApprovalStatus, TransportBusiConstant.APPROVED) + .and(Func.isNotBlank(trimmedKeyword), item -> item.like(InvoiceReceipt::getInvoiceNo, trimmedKeyword) + .or().like(InvoiceReceipt::getIssuerName, trimmedKeyword) + .or().like(InvoiceReceipt::getReceiverName, trimmedKeyword)) + .notInSql(InvoiceReceipt::getId, claimedReceiptSubSql) + .orderByDesc(InvoiceReceipt::getCreateTime)); + } + + @Override + public List claimedReceipts(Long settlementId) { + List relations = settlementRelationMapper.selectList( + Wrappers.lambdaQuery() + .eq(InvoiceReceiptSettlement::getFormalSettlementId, settlementId)); + if (relations.isEmpty()) { + return List.of(); + } + return listByIds(relations.stream() + .map(InvoiceReceiptSettlement::getInvoiceReceiptId).filter(Objects::nonNull).distinct().toList()); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void syncSettlementReceiptRelations(Long settlementId, List invoiceNos) { + FormalSettlement settlement = formalSettlementMapper.selectById(settlementId); + if (settlement == null || Objects.equals(settlement.getIsDeleted(), 1)) { + throw new ServiceException("正式结算单不存在,无法同步收票认领关系"); + } + List distinctNos = invoiceNos == null ? List.of() : invoiceNos.stream() + .filter(Func::isNotBlank).map(String::trim).distinct().toList(); + // 按行发票号匹配审批通过的收票登记(行锁防两结算单并发认领同一票) + List matchedReceipts = distinctNos.isEmpty() ? List.of() + : list(Wrappers.lambdaQuery() + .in(InvoiceReceipt::getInvoiceNo, distinctNos) + .eq(InvoiceReceipt::getApprovalStatus, TransportBusiConstant.APPROVED) + .last("FOR UPDATE")); + this.checkSingleSettlementClaim(settlement, matchedReceipts); + // 重建本结算单认领关系(删旧建新):分摊金额取发票行的匹配结算单金额(支持手动修改),未匹配到的回退发票全额 + Map matchedByInvoiceNo = formalSettlementInvoiceMapper.selectList( + Wrappers.lambdaQuery() + .eq(FormalSettlementInvoice::getFormalSettlementId, settlementId)) + .stream() + .filter(row -> Func.isNotBlank(row.getInvoiceNo())) + .collect(Collectors.toMap(FormalSettlementInvoice::getInvoiceNo, + row -> money(row.getMatchedAmount()), (left, right) -> left)); + settlementRelationMapper.delete(Wrappers.lambdaQuery() + .eq(InvoiceReceiptSettlement::getFormalSettlementId, settlementId)); + for (InvoiceReceipt receipt : matchedReceipts) { + InvoiceReceiptSettlement relation = new InvoiceReceiptSettlement(); + relation.setInvoiceReceiptId(receipt.getId()); + relation.setFormalSettlementId(settlementId); + relation.setFormalSettlementNo(settlement.getFormalSettlementNo()); + relation.setSettlementAmount(money(settlement.getSettlementAmount())); + relation.setReceivedInvoiceAmount(receivedAmount(settlementId, receipt.getId())); + relation.setAllocatedInvoiceAmount( + matchedByInvoiceNo.getOrDefault(receipt.getInvoiceNo(), money(receipt.getInvoiceAmount()))); + settlementRelationMapper.insert(relation); + log.info("结算单[{}]认领收票登记[{}]:分摊金额 {}", settlement.getFormalSettlementNo(), + receipt.getInvoiceNo(), relation.getAllocatedInvoiceAmount()); + } + refreshSettlementInvoiceStatus(settlementId); + log.info("结算单[{}]收票认领关系同步完成:匹配收票登记 {} 张(入参发票号 {} 个)", + settlement.getFormalSettlementNo(), matchedReceipts.size(), distinctNos.size()); + } + + @Override + public String assignClaimedReceipts(Long settlementId, String apBillSrcCode) { + if (Func.isBlank(apBillSrcCode)) { + log.warn("结算单[{}]财务应付单防重键为空,跳过收票单指定应付", settlementId); + return ""; + } + List relations = settlementRelationMapper.selectList( + Wrappers.lambdaQuery() + .eq(InvoiceReceiptSettlement::getFormalSettlementId, settlementId)); + if (relations.isEmpty()) { + log.info("结算单[{}]无认领收票登记,跳过 A302-6 指定应付", settlementId); + return ""; + } + List receipts = listByIds(relations.stream() + .map(InvoiceReceiptSettlement::getInvoiceReceiptId).filter(Objects::nonNull).distinct().toList()); + int assigned = 0; + int skipped = 0; + List failures = new ArrayList<>(); + for (InvoiceReceipt receipt : receipts) { + String pushStatus = Func.toStrWithEmpty(receipt.getKingdeePushStatus(), KingdeeBfiConstant.RECEIVED_PUSH_UNSYNCED); + if (!KingdeeBfiConstant.RECEIVED_PUSH_PUSHED.equals(pushStatus)) { + // 未推送(unsynced/failed)或已指定(assigned)均跳过:未推送由 A801-3 回写"指定补偿"按认领关系自动补指定 + skipped++; + log.info("结算单[{}]收票登记[{}]推送状态[{}],跳过 A302-6 指定应付", settlementId, receipt.getInvoiceNo(), pushStatus); + continue; + } + try { + kingdeeReceivedInvoiceSyncSupport.assignApBill(String.valueOf(receipt.getId()), apBillSrcCode); + applyReceivedInvoicePushResult(receipt, receipt.getKingdeeReceivedInvoiceNo(), receipt.getKingdeeReceivedInvoiceId(), + KingdeeBfiConstant.RECEIVED_PUSH_ASSIGNED, null, true); + assigned++; + log.info("结算单[{}]收票登记[{}]A302-6 指定应付成功:应付单防重键={}", settlementId, receipt.getInvoiceNo(), apBillSrcCode); + } catch (RuntimeException assignException) { + // 单笔失败不阻断结算同步:A801-3 回写"指定补偿"按认领关系自动重试 + log.warn("结算单[{}]收票登记[{}]A302-6 指定应付失败(由 A801-3 指定补偿重试)", settlementId, receipt.getInvoiceNo(), assignException); + failures.add("发票" + receipt.getInvoiceNo() + ":" + assignException.getMessage()); + } + } + String summary = "结算单收票指定应付:认领 " + receipts.size() + " 笔,指定 " + assigned + " 笔,跳过 " + skipped + " 笔"; + if (!failures.isEmpty()) { + summary = summary + ",失败 " + failures.size() + " 笔:" + String.join(";", failures); + } + log.info("{},应付单防重键={}", summary, apBillSrcCode); + return failures.isEmpty() ? "" : String.join(";", failures); + } + + /** 票池镜像明细行反序列化:空返回空集,解析异常按脏数据拦截 */ + private List parseMirrorItems(String invoiceNo, String itemsJson) { + if (Func.isBlank(itemsJson)) { + return List.of(); + } + try { + BfiInvoicePoolItemVO[] items = JsonUtil.parse(itemsJson, BfiInvoicePoolItemVO[].class); + return items == null ? List.of() : Arrays.asList(items); + } catch (RuntimeException exception) { + log.warn("发票[{}]票池镜像明细行反序列化失败", invoiceNo, exception); + throw new ServiceException("发票[" + invoiceNo + "]票池镜像明细行数据异常,请先按票号补查票池明细"); + } + } + + /** 一票一结算单校验:认领票已挂其他有效应付结算单时拦截(指向本结算单的旧关系即将重建,不参与校验) */ + private void checkSingleSettlementClaim(FormalSettlement settlement, List receipts) { + if (receipts.isEmpty()) { + return; + } + List receiptIds = receipts.stream().map(InvoiceReceipt::getId).toList(); + List otherRelations = settlementRelationMapper.selectList( + Wrappers.lambdaQuery() + .in(InvoiceReceiptSettlement::getInvoiceReceiptId, receiptIds) + .ne(InvoiceReceiptSettlement::getFormalSettlementId, settlement.getId())).stream() + .filter(relation -> relation.getFormalSettlementId() != null).toList(); + if (otherRelations.isEmpty()) { + return; + } + List otherSettlementIds = otherRelations.stream() + .map(InvoiceReceiptSettlement::getFormalSettlementId).distinct().toList(); + Map otherSettlements = formalSettlementMapper.selectBatchIds(otherSettlementIds).stream() + .collect(Collectors.toMap(FormalSettlement::getId, Function.identity(), (first, second) -> first)); + for (InvoiceReceipt receipt : receipts) { + for (InvoiceReceiptSettlement relation : otherRelations) { + if (!relation.getInvoiceReceiptId().equals(receipt.getId())) { + continue; + } + FormalSettlement other = otherSettlements.get(relation.getFormalSettlementId()); + boolean activeSettlement = other != null && !Objects.equals(other.getIsDeleted(), 1) + && Objects.equals(other.getStatus(), 1) && "payable".equals(other.getSettlementType()); + if (activeSettlement) { + throw new ServiceException("发票" + receipt.getInvoiceNo() + "已被结算单" + + other.getFormalSettlementNo() + "认领,一张收票单只能被一张付款结算单认领"); + } + } + } } // ==================== A801-3 收票结果定时回写(异常回收/指定补偿/单号补登) ==================== @@ -974,7 +1124,8 @@ public class InvoiceReceiptServiceImpl extends BaseServiceImpl匹配与容错说明: * ① 匹配键为收票登记 id,与 A303-2 落库的 awrp_src_code 同源,按发票号匹配将永远查不到自推单据; @@ -987,6 +1138,7 @@ public class InvoiceReceiptServiceImpl extends BaseServiceImpl pendingReceipts = list(Wrappers.lambdaQuery() .in(InvoiceReceipt::getKingdeePushStatus, KingdeeBfiConstant.RECEIVED_PUSH_PUSHED, KingdeeBfiConstant.RECEIVED_PUSH_ASSIGNED) @@ -1001,14 +1153,17 @@ public class InvoiceReceiptServiceImpl extends BaseServiceImpl KingdeeBfiConstant.RECEIVED_PUSH_PUSHED.equals(receipt.getKingdeePushStatus())).count(); log.info("金蝶收票单结果回写开始:待回写收票登记{}笔(pushed待指定补偿 {} 笔,assigned待单号补登 {} 笔)", pendingReceipts.size(), pushedCount, pendingReceipts.size() - pushedCount); + if (!assignCompensationEnabled) { + log.info("A302-6 指定应付自动补偿已关闭,pushed 单据由人工入口(发票登记推送/重推按钮)指定"); + } List mirrorIds = pendingReceipts.stream().map(InvoiceReceipt::getKingdeeInvoicePoolId) .filter(Objects::nonNull).distinct().toList(); Map mirrorById = invoicePoolMapper.selectBatchIds(mirrorIds).stream() .collect(Collectors.toMap(KingdeeInvoicePool::getId, Function.identity(), (first, second) -> first)); - // pushed(保存成功后中断、指定未完成)集合:批量预组装应付单防重键供指定补偿 - List pushedIds = pendingReceipts.stream() + // pushed(保存成功后中断、指定未完成)集合:批量预组装应付单防重键供指定补偿(补偿关闭时跳过组装) + List pushedIds = assignCompensationEnabled ? pendingReceipts.stream() .filter(receipt -> KingdeeBfiConstant.RECEIVED_PUSH_PUSHED.equals(receipt.getKingdeePushStatus())) - .map(InvoiceReceipt::getId).toList(); + .map(InvoiceReceipt::getId).toList() : List.of(); Map apSrcCodeByReceiptId = pushedIds.isEmpty() ? Map.of() : resolveApBillSrcCodesForWriteBack(pushedIds); Map> receiptsByBuyer = new LinkedHashMap<>(); @@ -1050,6 +1205,11 @@ public class InvoiceReceiptServiceImpl extends BaseServiceImpl distinctSettlementRows( List rows) { Map result = new LinkedHashMap<>(); + if (rows == null || rows.isEmpty()) { + return result; + } for (InvoiceReceiptSaveRequest.SettlementRow row : rows) { if (row == null || row.getSettlementId() == null) { throw new ServiceException("正式结算单不能为空"); @@ -1965,8 +2134,9 @@ public class InvoiceReceiptServiceImpl extends BaseServiceImpl= 0 ? "completed" : "partial"; settlement.setInvoiceAmount(invoiceAmount); settlement.setInvoiceStatus(invoiceStatus); formalSettlementMapper.updateById(settlement); @@ -2016,7 +2186,9 @@ public class InvoiceReceiptServiceImpl extends BaseServiceImpl settlementMap = lockSettlements(relations.stream() .map(InvoiceReceiptSettlement::getFormalSettlementId) diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/PaymentApplicationServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/PaymentApplicationServiceImpl.java index 1fba462b..c5cf9012 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/PaymentApplicationServiceImpl.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/PaymentApplicationServiceImpl.java @@ -51,6 +51,7 @@ import org.springblade.thirdparty.bfi.util.BfiUtil; import org.springblade.transport.config.BfiProperties; import org.springblade.transport.constant.KingdeeAttachmentConstant; import org.springblade.transport.constant.HcsSyncConstant; +import org.springblade.hcs.config.HcsProperties; import org.springblade.transport.constant.KingdeeBfiConstant; import org.springblade.transport.constant.KingdeeLockConstant; import org.springblade.transport.constant.TransportBusiConstant; @@ -64,6 +65,8 @@ import org.springblade.transport.mapper.FormalSettlementDetailMapper; import org.springblade.transport.mapper.FormalSettlementMapper; import org.springblade.transport.mapper.FormalSettlementPaymentMapper; import org.springblade.transport.mapper.FormalSettlementSourceMapper; +import org.springblade.transport.mapper.InvoiceReceiptMapper; +import org.springblade.transport.mapper.InvoiceReceiptSettlementMapper; import org.springblade.transport.mapper.PaymentApplicationInvoiceMapper; import org.springblade.transport.mapper.PaymentApplicationMapper; import org.springblade.transport.mapper.PaymentApplicationRecordMapper; @@ -85,6 +88,8 @@ import org.springblade.transport.pojo.entity.CustomerReceiptAccount; import org.springblade.transport.pojo.entity.FormalSettlement; import org.springblade.transport.pojo.entity.FormalSettlementDetail; import org.springblade.transport.pojo.entity.FormalSettlementPayment; +import org.springblade.transport.pojo.entity.InvoiceReceipt; +import org.springblade.transport.pojo.entity.InvoiceReceiptSettlement; import org.springblade.transport.pojo.entity.FormalSettlementSource; import org.springblade.transport.pojo.entity.HcsContractExeStage; import org.springblade.transport.pojo.entity.KingdeeBillLock; @@ -170,6 +175,8 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl rows) { + Set settlementIds = rows.stream() + .map(PaymentApplicationVO::getSettlementId).filter(Objects::nonNull).collect(Collectors.toSet()); + if (settlementIds.isEmpty()) { + return; + } + List relations = invoiceReceiptRelationMapper.selectList( + Wrappers.lambdaQuery() + .in(InvoiceReceiptSettlement::getFormalSettlementId, settlementIds)); + Map> receiptIdsBySettlement = relations.stream() + .filter(relation -> relation.getFormalSettlementId() != null && relation.getInvoiceReceiptId() != null) + .collect(Collectors.groupingBy(InvoiceReceiptSettlement::getFormalSettlementId, + Collectors.mapping(InvoiceReceiptSettlement::getInvoiceReceiptId, Collectors.toList()))); + List receiptIds = receiptIdsBySettlement.values().stream() + .flatMap(List::stream).distinct().toList(); + Map receiptById = receiptIds.isEmpty() ? Map.of() + : invoiceReceiptMapper.selectBatchIds(receiptIds).stream() + .filter(receipt -> !TransportBusiConstant.VOIDED.equals(receipt.getApprovalStatus()) + && !Objects.equals(receipt.getIsDeleted(), 1)) + .collect(Collectors.toMap(InvoiceReceipt::getId, receipt -> receipt, (left, right) -> left)); + for (PaymentApplicationVO vo : rows) { + List claimed = receiptIdsBySettlement.getOrDefault(vo.getSettlementId(), List.of()).stream() + .map(receiptById::get).filter(Objects::nonNull).toList(); + if (claimed.isEmpty()) { + continue; + } + List statuses = claimed.stream() + .map(receipt -> Func.toStrWithEmpty(receipt.getKingdeePushStatus(), + KingdeeBfiConstant.RECEIVED_PUSH_UNSYNCED)) + .toList(); + String status = statuses.contains(KingdeeBfiConstant.RECEIVED_PUSH_FAILED) ? "failed" + : statuses.stream().allMatch(KingdeeBfiConstant.RECEIVED_PUSH_ASSIGNED::equals) + ? "assigned" : "unspecified"; + vo.setKingdeeAssignStatus(status); + // 指定失败行聚合各票失败原因,供悬浮展示 + if ("failed".equals(status)) { + vo.setKingdeeAssignFailReason(claimed.stream() + .filter(receipt -> KingdeeBfiConstant.RECEIVED_PUSH_FAILED.equals( + Func.toStrWithEmpty(receipt.getKingdeePushStatus(), KingdeeBfiConstant.RECEIVED_PUSH_UNSYNCED))) + .map(receipt -> "发票" + Func.toStrWithEmpty(receipt.getInvoiceNo(), String.valueOf(receipt.getId())) + + ":" + Func.toStrWithEmpty(receipt.getKingdeePushMsg(), "推送失败")) + .collect(Collectors.joining(";"))); + } + } + } + /** * 填充金蝶锁状态标识(locked-锁定中/failed-锁定失败;unlocked 终态不返回): * 锁表按本页源单一次批量查询,避免 N+1;locked 时前端可禁用编辑/作废, @@ -658,8 +720,9 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl + * ① 圈定待回写付款申请:审批通过且金蝶状态为 已生成/付款中/生成失败(终态已付款/已关闭不再轮询), + * 生成失败单据一并纳入:金蝶侧已生成说明上次推送实际成功(如下推超时但远端已落单、本地事务回滚), + * 查得即回填纠正为已生成,避免人工重推产生重复单据,未查得则保持生成失败等待人工重推; + * ② A103-2 付款申请单查询回填:按 paymentNo(awrp_src_code)分批查询,同一来源单号取最近修改一条, + * 回填金蝶单号/内码/同步状态/已付金额并联动预结算与正式结算付款记录;连续两轮查询未命中按金蝶反审核删除解锁; + * ③ A103-4 付款处理查询补付款记录:付款中/已付款单据 + 已付款无付款记录单据(补偿), + * 按付款组织 + awrp_sourcebillno(金蝶单据号)数组精确查询,双键勾稽后回填付款记录; + * ④ 汇总执行摘要:各计数随流程累计,单笔失败仅计数不中断整轮 + * * * @return 执行摘要 */ private String executePaymentResultSync() { - // ① 待回写付款申请:审批通过且金蝶状态为 已生成/付款中/生成失败(终态已付款/已关闭不再轮询)。 - // 生成失败单据一并纳入勾稽:金蝶侧已生成说明上次推送实际成功(如下推请求超时但远端已落单、本地事务回滚), - // 查到即回填纠正为已生成,避免人工重新推送产生重复单据;未查到则保持生成失败等待异常处理后重新推送 + // ① 待回写付款申请:审批通过且金蝶状态为 已生成/付款中/生成失败(终态已付款/已关闭不再轮询) List applications = list(Wrappers.lambdaQuery() .eq(PaymentApplication::getApprovalStatus, TransportBusiConstant.APPROVED) .in(PaymentApplication::getKingdeeStatus, KingdeeBfiConstant.KINGDEE_STATUS_SYNCED, KingdeeBfiConstant.KINGDEE_STATUS_PAYING, KingdeeBfiConstant.KINGDEE_STATUS_FAILED) .orderByAsc(PaymentApplication::getId)); + // 自代理:applyPayApplyResult/applyPayBillRecord 带单据级事务,跨方法调用确保注解生效,单笔失败仅回滚自身 IPaymentApplicationService self = SpringUtil.getBean(IPaymentApplicationService.class); int updated = 0; int failed = 0; @@ -913,19 +986,22 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl paymentNos = applications.stream() .map(PaymentApplication::getPaymentNo).filter(Func::isNotEmpty).distinct().toList(); - batchForeach(paymentNos, PAY_RESULT_QUERY_BATCH, batch -> - payApplyMap.putAll(latestPayApplyBySrcCode(queryPayApplyPages(batch)))); + this.batchForeach(paymentNos, PAY_RESULT_QUERY_BATCH, batch -> + payApplyMap.putAll(this.latestPayApplyBySrcCode(this.queryPayApplyPages(batch)))); + // 逐单回写:查得即回填并清零消失计数;未查得累计消失轮次(批次其余单据正常返回,排除服务整体故障) for (PaymentApplication application : applications) { BfiPayApplyVO payApply = payApplyMap.get(application.getPaymentNo()); if (payApply == null) { - // 金蝶侧未查得已推送单据(查询批次其余单据正常返回,排除服务整体故障): // 累计消失轮次,连续两轮视为金蝶已反审核删除,解锁置失败交人工核实重推 this.handlePayApplyMissed(application); continue; } - clearPayApplyMissKey(application.getId()); + this.clearPayApplyMissKey(application.getId()); try { - if (self.applyPayApplyResult(application, payApply)) paidCount++; + // 返回 true 表示付款完成(终态),预结算/正式结算联动由 applyPayApplyResult 内部处理 + if (self.applyPayApplyResult(application, payApply)) { + paidCount++; + } updated++; } catch (Exception exception) { failed++; @@ -933,7 +1009,9 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl recordCandidates = new LinkedHashMap<>(); for (PaymentApplication application : applications) { BfiPayApplyVO payApply = payApplyMap.get(application.getPaymentNo()); @@ -944,18 +1022,19 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpllambdaQuery() .eq(PaymentApplication::getApprovalStatus, TransportBusiConstant.APPROVED) .eq(PaymentApplication::getKingdeeStatus, KingdeeBfiConstant.KINGDEE_STATUS_PAID)) - .stream().filter(application -> realRecordCount(application.getId()) == 0) + .stream().filter(application -> this.realRecordCount(application.getId()) == 0) .forEach(application -> recordCandidates.putIfAbsent(application.getId(), application)); int recordWritten = 0; int recordFailed = 0; if (!recordCandidates.isEmpty()) { + // 按付款组织分组(org_number 接口必传):优先取 A103-2 返回的付款组织编码, + // 兜底按合同关联项目解析(已付款补偿候选不在本轮 A103-2 返回内,须走兜底) Map> orgGroups = new LinkedHashMap<>(); for (PaymentApplication application : recordCandidates.values()) { BfiPayApplyVO payApply = payApplyMap.get(application.getPaymentNo()); String orgNumber = payApply == null ? null : payApply.getPayorgNumber(); if (Func.isBlank(orgNumber)) { try { - // A103-4 付款人(公司)编码必填:优先取 A103-2 返回付款组织编码,兜底按合同关联项目解析 KingdeeSyncContext context = KingdeeSyncContext.empty(); orgNumber = kingdeeApBillSyncSupport.bfiOrgNumber( kingdeeApBillSyncSupport.requireContract(application.getContractId(), context), context); @@ -966,26 +1045,25 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl new ArrayList<>()).add(application); } - // 以付款处理单源单id/源单编号建立索引,与本地金蝶单据id/单据编号勾稽 - Map payBillBySourceId = new HashMap<>(); - Map payBillBySourceNumber = new HashMap<>(); - for (String orgNumber : orgGroups.keySet()) { - for (BfiPayBillVO payBill : queryPayBillPages(orgNumber)) { - if (Func.isEmpty(payBill.getBillno())) continue; - if (payBill.getSourcebillid() != null) { - payBillBySourceId.put(String.valueOf(payBill.getSourcebillid()), payBill); - } - if (Func.isNotEmpty(payBill.getSourcebillnumber())) { - payBillBySourceNumber.put(payBill.getSourcebillnumber(), payBill); - } - } + // 按组织 + 金蝶单据号(awrp_sourcebillno 支持数组)分批精确查询付款处理单,结果汇总后统一勾稽 + List payBills = new ArrayList<>(); + for (Map.Entry> orgEntry : orgGroups.entrySet()) { + List kingdeeBillNos = orgEntry.getValue().stream() + .map(PaymentApplication::getKingdeeBillNo).filter(Func::isNotEmpty).distinct().toList(); + this.batchForeach(kingdeeBillNos, PAY_RESULT_QUERY_BATCH, + batch -> payBills.addAll(this.queryPayBillPages(orgEntry.getKey(), batch))); } + // 逐单勾稽回填:双键匹配(源单id=付款申请金蝶单据内码 / 源单编号=金蝶单据号),未匹配保持现状等下一轮 for (PaymentApplication application : recordCandidates.values()) { - BfiPayBillVO payBill = matchPayBill(application, payBillBySourceId, payBillBySourceNumber); - if (payBill == null) { + if (Func.isBlank(application.getKingdeeBillNo())) { + log.warn("付款申请[{}]付款处理查询跳过,金蝶单据编号为空", application.getPaymentNo()); continue; } try { + BfiPayBillVO payBill = this.matchPayBill(application, payBills); + if (payBill == null) { + continue; + } self.applyPayBillRecord(application, payBill); recordWritten++; } catch (Exception exception) { @@ -1010,7 +1088,7 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl 0) { + if (existing == null || this.strCompare(payApply.getModifytime(), existing.getModifytime()) > 0) { latestBySrcCode.put(payApply.getAwrpSrcCode(), payApply); } } @@ -1145,7 +1223,7 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl - * 窗口天数取 Nacos thirdParty.bfi.payResult.queryWindowDays(默认 60) + * 业务日期窗口与付款组织为接口必填参数,窗口天数取 Nacos thirdParty.bfi.payResult.queryWindowDays + * (默认 60),仅用于满足必填要求;awrp_sourcebillno 精确过滤后仅返回目标单据的付款处理单, + * 不再拉取组织内全量数据做内存勾稽 * - * @param orgNumber 付款人(公司)编码 + * @param orgNumber 付款人(公司)编码,接口必传 + * @param awrpSourceBillnos 付款申请金蝶单据号批次 * @return 付款处理单列表 */ - private List queryPayBillPages(String orgNumber) { + private List queryPayBillPages(String orgNumber, List awrpSourceBillnos) { LocalDate endDate = LocalDate.now(); int windowDays = Math.max(bfiProperties.getPayResult().getQueryWindowDays(), 1); LocalDate beginDate = endDate.minusDays(windowDays); BfiPayBillQueryDTO param = new BfiPayBillQueryDTO(); + // 接口必填参数:付款组织编码 + 业务日期窗口(beigin_bizdate/end_bizdate,金蝶侧拼写如此) param.setOrgNumber(List.of(orgNumber)); param.setBeginBizdate(beginDate.format(DateTimeFormatter.ISO_LOCAL_DATE)); param.setEndBizdate(endDate.format(DateTimeFormatter.ISO_LOCAL_DATE)); + // 精确过滤参数:待回填单据的金蝶单据号数组(sourcebillnumber 与之同源) + param.setAwrpSourceBillno(awrpSourceBillnos); return BfiUtil.queryAllPages(pageNo -> { param.setPageNo(pageNo); param.setPageSize(PAY_RESULT_PAGE_SIZE); @@ -1208,21 +1292,24 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl payBillBySourceId, - Map payBillBySourceNumber) { - if (Func.isNotEmpty(application.getKingdeeBillId()) - && payBillBySourceId.containsKey(application.getKingdeeBillId())) { - return payBillBySourceId.get(application.getKingdeeBillId()); - } - if (Func.isNotEmpty(application.getKingdeeBillNo())) { - return payBillBySourceNumber.get(application.getKingdeeBillNo()); + private BfiPayBillVO matchPayBill(PaymentApplication application, List payBills) { + for (BfiPayBillVO payBill : payBills) { + if (payBill.getSourcebillid() != null + && Func.isNotEmpty(application.getKingdeeBillId()) + && String.valueOf(payBill.getSourcebillid()).equals(application.getKingdeeBillId())) { + return payBill; + } + if (Func.isNotEmpty(payBill.getSourcebillnumber()) + && payBill.getSourcebillnumber().equals(application.getKingdeeBillNo())) { + return payBill; + } } return null; } diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/support/KingdeeApBillSyncSupport.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/support/KingdeeApBillSyncSupport.java index 1ba44b36..ef7203a4 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/support/KingdeeApBillSyncSupport.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/support/KingdeeApBillSyncSupport.java @@ -104,6 +104,33 @@ public class KingdeeApBillSyncSupport { public static final String FEE_CATEGORY_FREIGHT = "A01"; /** 费用大类编码(业务字典 fee_category 键值):装卸 */ public static final String FEE_CATEGORY_LOADING = "A10"; + /** 费用大类编码(业务字典 fee_category 键值):仓储 */ + public static final String FEE_CATEGORY_WAREHOUSE = "A05"; + + /** + * 分录费用大类(三分类):label 用于分组键与分录标识拼接 + */ + public enum FeeCategory { + + /** 运输 */ + FREIGHT("运输费"), + + /** 仓储 */ + WAREHOUSE("仓储"), + + /** 装卸 */ + LOADING("装卸"); + + private final String label; + + FeeCategory(String label) { + this.label = label; + } + + public String getLabel() { + return label; + } + } private final IBfiService bfiService; private final ContractManageMapper contractManageMapper; @@ -418,10 +445,10 @@ public class KingdeeApBillSyncSupport { * 解析单张正式结算单分录:页面结算明细每行(一个运单)按 货物 → 费用大类 → 税率 拆分多行 * + 手动添加的结算合计费用项每行一条(系统自动汇总行是明细金额的再汇总不送)。 *

取数口径: - * 结算明细拆行——一车多货(存在货物费用行)先按货物拆;费用项名称含“装卸”归装卸大类、其余归运输大类, - * 各大类内按合同计费方案规则税率分组、不同税率分行,运输类每行数量取运单运输量、装卸类每行数量默认 1, + * 结算明细拆行——一车多货(存在货物费用行)先按货物拆;费用项按档案分类判定 装卸(A10)/仓储(A05)/运输 三大类, + * 各大类内按合同计费方案规则税率分组、不同税率分行,运输类每行数量取运单运输量、装卸/仓储类每行数量默认 1, * 费用项金额合计与结算金额的残差并入运输类无税率组保证勾稽; - * 物料编码按装卸大类/项目业务模式/运单运输方式映射(thirdparty.bfi.material.* 配置); + * 物料编码按费用大类/项目业务模式/运单运输方式映射(thirdparty.bfi.material.* 配置); * 单价按结算金额/数量反算; * 结算金额:结算明细行取结算金额(含税)、结算合计费用项行取合计行结算金额; * 运输货物/车号仅结算明细拆分行传(货物名称/车号);分录标识:结算明细拆分行取单据号-货物序号-大类-税率、 @@ -494,8 +521,8 @@ public class KingdeeApBillSyncSupport { /** * 单个货物费用行按费用大类/税率拆分生成分录: - * 费用项名称含“装卸”归装卸大类、其余归运输大类,各大类内按合同计费方案税率分组、不同税率分行; - * 运输类每行数量取运单运输量、装卸类每行数量默认 1; + * 费用项按档案分类判定 装卸(A10)/仓储(A05)/运输 三大类,各大类内按合同计费方案税率分组、不同税率分行; + * 运输类每行数量取运单运输量、装卸/仓储类每行数量默认 1; * 运输类首行携带行不含税金额(拆分行无分类不含税金额,仅保证不含税合计勾稽); * 运输货物取货物名称、车号取结算明细车号(同一车牌号就是一个车) */ @@ -519,21 +546,20 @@ public class KingdeeApBillSyncSupport { String entryCodePrefix = documentNo + "-" + cargoSeq; boolean amountNoTaxSent = false; for (FeeLineSplit split : splits) { - String category = split.isLoading() ? "装卸" : "运输费"; - // 物料:装卸大类映射装卸物料;运输类按项目业务模式/运单运输方式映射 - String materialNumber = resolveMaterialNumber(split.isLoading(), context.project(), detail.getTransportType(), sourceDesc); - // 数量:运输类取运输量,装卸类默认 1 - BigDecimal quantity = split.isLoading() ? BigDecimal.ONE : freightQuantity; + // 物料:装卸大类映射装卸物料、仓储大类映射仓储物料;运输类按项目业务模式/运单运输方式映射 + String materialNumber = resolveMaterialNumber(split.getCategory(), context.project(), detail.getTransportType(), sourceDesc); + // 数量:运输类取运输量,装卸/仓储类默认 1 + BigDecimal quantity = split.getCategory() == FeeCategory.FREIGHT ? freightQuantity : BigDecimal.ONE; BigDecimal amount = money(split.getAmount()); BigDecimal price = entryPrice(amount, quantity); // 不含税金额:仅运输类首行携带,保证不含税合计与结算明细勾稽 BigDecimal amountNoTax = null; - if (!split.isLoading() && !amountNoTaxSent && rowAmountNoTax != null) { + if (split.getCategory() == FeeCategory.FREIGHT && !amountNoTaxSent && rowAmountNoTax != null) { amountNoTax = money(rowAmountNoTax); amountNoTaxSent = true; } // 分录唯一标识:单据号-货物序号-费用大类-税率(税率未配置不拼) - String entryCode = entryCodePrefix + "-" + category + split.taxRateSuffix(); + String entryCode = entryCodePrefix + "-" + split.getCategory().getLabel() + split.taxRateSuffix(); entryGroups.add(new BfiEntryGroup(materialNumber, entryCode, quantity, price, amount, amountNoTax, cargoName, vehicleNo, context.settlement().getId())); log.info("金蝶应付单分录生成:来源[{}],分录标识[{}],物料编码[{}],数量[{}],单价[{}],含税金额[{}],不含税金额[{}]", @@ -547,8 +573,8 @@ public class KingdeeApBillSyncSupport { } /** - * 单条手动添加的结算合计费用项生成一条分录:非运费类(装卸/仓储等杂费)映射装卸物料, - * 运费类按项目业务模式/运输方式映射(运输方式取结算明细首行); + * 单条手动添加的结算合计费用项生成一条分录:运费类按项目业务模式/运输方式映射(运输方式取结算明细首行), + * 仓储大类(档案编码 A05)映射仓储物料,其余非运费类映射装卸物料; * 非运费类数量默认 1,金额取合计行结算金额,运输货物/车号不传,分录标识取结算单号-费用类型-费用项拼接 */ private void appendSummaryEntry(List entryGroups, EntryResolveContext context, @@ -557,9 +583,10 @@ public class KingdeeApBillSyncSupport { String feeType = Func.toStr(summaryFee.getFeeType()).trim(); String feeItem = Func.toStr(summaryFee.getFeeItem()).trim(); String sourceDesc = "结算合计[" + feeType + "-" + feeItem + "]"; - // 非运费类(费用项档案分类编码非 A01 且名称无运费字样)统一映射装卸物料 - boolean loadingFee = !isFreightCategory(feeType, feeItem, context.feeCategoryByName()); - String materialNumber = resolveMaterialNumber(loadingFee, context.project(), fallbackTransportType, sourceDesc); + // 物料:运费类按项目业务模式/运输方式映射,仓储大类(档案编码 A05)映射仓储物料,其余非运费类映射装卸物料 + FeeCategory feeCategory = isFreightCategory(feeType, feeItem, context.feeCategoryByName()) ? FeeCategory.FREIGHT + : isWarehouseCategory(feeItem, context.feeCategoryByName()) ? FeeCategory.WAREHOUSE : FeeCategory.LOADING; + String materialNumber = resolveMaterialNumber(feeCategory, context.project(), fallbackTransportType, sourceDesc); // 结算合计费用项行无运输量,数量默认 1 BigDecimal quantity = BigDecimal.ONE; BigDecimal amount = money(summaryFee.getSettlementAmount()); @@ -575,20 +602,22 @@ public class KingdeeApBillSyncSupport { /** * 物料编码映射(Nacos 配置 thirdparty.bfi.material.*): - * 装卸大类(及手动添加的非运费杂费)映射装卸物料 loading;业务模式/项目业务类型含“国际”(国际运输、 + * 装卸大类映射装卸物料 loading;仓储大类映射仓储物料 warehouse;业务模式/项目业务类型含“国际”(国际运输、 * 国际货物运输代理服务)映射国际货代服务 international;国内运输按运单运输方式查 domestic 映射 * (公路运输/铁路运输/水路运输/航空运输),运输方式未配置映射时直接拦截报错 * - * @param loadingFee 该行是否属于装卸大类(或非运费类杂费) + * @param feeCategory 该行费用大类(运输/仓储/装卸) * @param project 结算单关联项目(业务类型/业务模式取数来源) * @param transportType 运单运输方式(结算合计费用项行取结算明细首行运输方式) * @param sourceDesc 来源行描述,用于异常提示 * @return 金蝶物料编码 */ - private String resolveMaterialNumber(boolean loadingFee, ProjectApply project, String transportType, String sourceDesc) { + private String resolveMaterialNumber(FeeCategory feeCategory, ProjectApply project, String transportType, String sourceDesc) { BfiProperties.Material material = bfiProperties.getMaterial(); String materialNumber; - if (loadingFee) { + if (feeCategory == FeeCategory.WAREHOUSE) { + materialNumber = material.getWarehouse(); + } else if (feeCategory == FeeCategory.LOADING) { materialNumber = material.getLoading(); } else if (isInternationalBusiness(project)) { materialNumber = material.getInternational(); @@ -605,9 +634,9 @@ public class KingdeeApBillSyncSupport { if (Func.isBlank(materialNumber)) { throw new ServiceException("推送金蝶失败:" + sourceDesc + "未在配置 thirdparty.bfi.material 中维护对应物料编码,请补充后重试"); } - // 物料映射判定依据(联调排查:装卸大类/业务模式/项目业务类型/运输方式 -> 物料) - log.info("金蝶物料映射:来源[{}],装卸大类[{}],业务模式[{}],项目业务类型[{}],运单运输方式[{}] -> 物料编码[{}]", - sourceDesc, loadingFee ? "是" : "否", + // 物料映射判定依据(联调排查: 费用大类/业务模式/项目业务类型/运输方式 -> 物料) + log.info("金蝶物料映射: 来源[{}], 费用大类[{}], 业务模式[{}], 项目业务类型[{}], 运单运输方式[{}] -> 物料编码[{}]", + sourceDesc, feeCategory.getLabel(), project == null ? null : project.getBusinessMode(), project == null ? null : project.getBusinessType(), transportType, materialNumber); return materialNumber; @@ -684,7 +713,7 @@ public class KingdeeApBillSyncSupport { /** * 费用项按费用大类/税率拆分(纯函数): - * 费用项经费用项档案判为装卸大类(费用大类编码 A10)、其余归运输大类; + * 费用项经费用项档案判为 装卸(A10)/仓储(A05)/运输 三大类(仓储不做名称关键字兜底); * 各大类内按计费方案规则税率分组(未配置税率的费用项归入无税率组), * 费用项金额合计与结算金额的残差并入运输类无税率组,保证拆分后各行金额合计与结算金额勾稽 * @@ -706,11 +735,11 @@ public class KingdeeApBillSyncSupport { if (amount.signum() == 0) { continue; } - boolean loading = isLoadingCategory(item.getKey(), feeCategoryByName); + FeeCategory category = resolveCategory(item.getKey(), feeCategoryByName); BigDecimal taxRate = taxRateByRule == null ? null : taxRateByRule.get(item.getKey()); - FeeLineSplit split = grouped.get(splitGroupKey(loading, taxRate)); + FeeLineSplit split = grouped.get(splitGroupKey(category, taxRate)); if (split == null) { - grouped.put(splitGroupKey(loading, taxRate), new FeeLineSplit(loading, taxRate, amount, item.getKey())); + grouped.put(splitGroupKey(category, taxRate), new FeeLineSplit(category, taxRate, amount, item.getKey())); } else { split.addAmount(amount); } @@ -719,16 +748,37 @@ public class KingdeeApBillSyncSupport { BigDecimal residual = (rowAmount == null ? BigDecimal.ZERO : rowAmount).subtract(grouped.values().stream() .map(FeeLineSplit::getAmount).reduce(BigDecimal.ZERO, BigDecimal::add)); if (residual.signum() != 0) { - FeeLineSplit freightDefault = grouped.computeIfAbsent(splitGroupKey(false, null), - key -> new FeeLineSplit(false, null, BigDecimal.ZERO, FREIGHT_FEE_ITEM)); + FeeLineSplit freightDefault = grouped.computeIfAbsent(splitGroupKey(FeeCategory.FREIGHT, null), + key -> new FeeLineSplit(FeeCategory.FREIGHT, null, BigDecimal.ZERO, FREIGHT_FEE_ITEM)); freightDefault.addAmount(residual); } return new ArrayList<>(grouped.values()); } - /** 费用大类/税率分组键(装卸大类与运输大类、税率去重分组) */ - private static String splitGroupKey(boolean loading, BigDecimal taxRate) { - return (loading ? "装卸" : "运输费") + "@" + (taxRate == null ? "" : taxRate.stripTrailingZeros().toPlainString()); + /** 费用大类/税率分组键(运输/仓储/装卸大类、税率去重分组) */ + private static String splitGroupKey(FeeCategory category, BigDecimal taxRate) { + return category.getLabel() + "@" + (taxRate == null ? "" : taxRate.stripTrailingZeros().toPlainString()); + } + + /** + * 分录费用大类判定(三分类):费用项档案分类编码 A10 归装卸大类(保留名称关键字兜底现状)、 + * A05 归仓储大类(仓储仅认档案编码,不做名称关键字兜底,分类以档案维护为准),其余归运输大类 + */ + static FeeCategory resolveCategory(String feeItemName, Map feeCategoryByName) { + String categoryCode = feeItemName == null || feeCategoryByName == null ? null : feeCategoryByName.get(feeItemName); + if (FEE_CATEGORY_WAREHOUSE.equals(categoryCode)) { + return FeeCategory.WAREHOUSE; + } + return isLoadingCategory(feeItemName, feeCategoryByName) ? FeeCategory.LOADING : FeeCategory.FREIGHT; + } + + /** + * 仓储大类判定(手动添加结算合计费用行物料映射用):费用项档案维护的费用大类编码为 A05 归仓储大类, + * 无名称关键字兜底,分类以档案维护为准 + */ + static boolean isWarehouseCategory(String feeItemName, Map feeCategoryByName) { + String categoryCode = feeItemName == null || feeCategoryByName == null ? null : feeCategoryByName.get(feeItemName); + return FEE_CATEGORY_WAREHOUSE.equals(categoryCode); } /** 装卸大类费用项判定(关键字兜底):费用项名称含“装卸” */ @@ -796,17 +846,17 @@ public class KingdeeApBillSyncSupport { } /** - * 费用大类/税率拆分行:装卸大类 loading(true=装卸)、税率 taxRate(百分数,未配置为 null)、 + * 费用大类/税率拆分行:费用大类 category(运输/仓储/装卸)、税率 taxRate(百分数,未配置为 null)、 * 组内金额合计 amount、组内首个费用项 firstFeeItem */ public static final class FeeLineSplit { - private final boolean loading; + private final FeeCategory category; private final BigDecimal taxRate; private final String firstFeeItem; private BigDecimal amount; - FeeLineSplit(boolean loading, BigDecimal taxRate, BigDecimal amount, String firstFeeItem) { - this.loading = loading; + FeeLineSplit(FeeCategory category, BigDecimal taxRate, BigDecimal amount, String firstFeeItem) { + this.category = category; this.taxRate = taxRate; this.amount = amount; this.firstFeeItem = firstFeeItem; @@ -816,8 +866,18 @@ public class KingdeeApBillSyncSupport { amount = amount.add(value); } + /** 装卸大类判定(兼容保留) */ public boolean isLoading() { - return loading; + return category == FeeCategory.LOADING; + } + + /** 仓储大类判定 */ + public boolean isWarehouse() { + return category == FeeCategory.WAREHOUSE; + } + + public FeeCategory getCategory() { + return category; } public BigDecimal getTaxRate() { @@ -866,7 +926,7 @@ public class KingdeeApBillSyncSupport { /** * 费用项档案分类:费用项名称 -> 费用大类编码(blade_fee_item.fee_category,业务字典 fee_category 键值, - * 运输 A01 / 装卸 A10;批量同步时全批共享一次加载结果),费用大类判定用; + * 运输 A01 / 装卸 A10 / 仓储 A05;批量同步时全批共享一次加载结果),费用大类判定用; * 档案读取失败直接拦截 */ public Map feeCategoryByName(KingdeeSyncContext context) { @@ -997,6 +1057,25 @@ public class KingdeeApBillSyncSupport { return this.kingdeeOrgNumber(contract.getProjectId(), context); } + /** + * 金蝶组织编码:指定部门沿部门树向上反查唯一平台公司(是否平台公司=是),取该平台公司的金蝶组织编码。 + * 先票后款链路收票登记不挂合同/项目,结算组织由页面手选部门派生(A303-2 org_number) + * + * @param deptId 手选部门id(收票登记 deptId) + * @param context 金蝶同步上下文(缓存载体) + * @return 组织编码 + */ + public String kingdeeOrgNumberByDept(Long deptId, KingdeeSyncContext context) { + if (deptId == null) { + throw new ServiceException("推送金蝶失败,收票登记未选择结算组织,无法确定金蝶组织编码"); + } + Dept dept = SysCache.getDept(deptId); + if (dept == null) { + throw new ServiceException("推送金蝶失败,所选结算组织[" + deptId + "]不存在,无法确定金蝶组织编码"); + } + return this.platformCompanyOrgNumber(dept, dept.getDeptName()); + } + /** * 金蝶组织编码:项目业务部门沿部门树向上反查唯一平台公司(是否平台公司=是),取该平台公司的金蝶组织编码; * 项目未关联/未维护业务部门/业务部门未归属平台公司/平台公司未维护金蝶组织编码时拦截 @@ -1007,8 +1086,15 @@ public class KingdeeApBillSyncSupport { */ public String kingdeeOrgNumber(Long projectId, KingdeeSyncContext context) { ProjectApply project = this.requireOrgProject(projectId, context); - // 沿部门树向上反查平台公司,上限 20 级防脏数据成环死循环 Dept dept = SysCache.getDept(project.getBusinessDeptId()); + if (dept == null) { + throw new ServiceException("推送金蝶失败,项目[" + Func.toStr(project.getProjectName()) + "]业务部门不存在,无法确定金蝶组织编码"); + } + return this.platformCompanyOrgNumber(dept, project.getBusinessDeptName()); + } + + /** 部门树向上反查平台公司金蝶组织编码(上限 20 级防脏数据成环死循环),未归属平台公司时拦截 */ + private String platformCompanyOrgNumber(Dept dept, String startDeptName) { for (int depth = 0; dept != null && depth < 20; depth++) { if (Objects.equals(dept.getIsPlatformCompany(), 1)) { if (Func.isBlank(dept.getKingdeeDeptCode())) { @@ -1018,7 +1104,7 @@ public class KingdeeApBillSyncSupport { } dept = dept.getParentId() == null ? null : SysCache.getDept(dept.getParentId()); } - throw new ServiceException("推送金蝶失败,项目业务部门[" + Func.toStr(project.getBusinessDeptName()) + "]未归属平台公司,无法确定金蝶组织编码"); + throw new ServiceException("推送金蝶失败,部门[" + Func.toStr(startDeptName) + "]未归属平台公司,无法确定金蝶组织编码"); } /** @@ -1157,7 +1243,7 @@ public class KingdeeApBillSyncSupport { /** * 财务应付单/付款申请分录:结算明细按 货物→费用大类→税率 拆分行 + 结算合计费用项(手动添加的每行一条) * - * @param materialNumber 物料编码(按装卸大类/项目业务模式/运单运输方式映射,thirdparty.bfi.material.* 配置) + * @param materialNumber 物料编码(按费用大类/项目业务模式/运单运输方式映射,thirdparty.bfi.material.* 配置) * @param entryCode 分录唯一标识(awrp_entry_code):结算明细拆分行取单据号-货物序号-大类-税率,结算合计费用项行取结算单号-费用类型-费用项 * @param quantity 该行分录数量(运输类取运输量,装卸类/其它默认 1) * @param price 该行分录单价(结算金额/数量反算) diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/support/KingdeeReceivedInvoiceSyncSupport.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/support/KingdeeReceivedInvoiceSyncSupport.java index cc2e590c..7208829f 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/support/KingdeeReceivedInvoiceSyncSupport.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/support/KingdeeReceivedInvoiceSyncSupport.java @@ -28,6 +28,7 @@ package org.springblade.transport.support; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springblade.core.log.exception.ServiceException; +import org.springblade.core.tool.jackson.JsonUtil; import org.springblade.core.tool.utils.Func; import org.springblade.thirdparty.bfi.constant.BfiConstant; import org.springblade.thirdparty.bfi.pojo.dto.BfiInvoiceAssignItemDTO; @@ -37,6 +38,7 @@ import org.springblade.thirdparty.bfi.pojo.dto.BfiReceivedInvoiceSaveDTO; import org.springblade.thirdparty.bfi.pojo.dto.BfiReceivedInvoiceSaveEntryDTO; import org.springblade.thirdparty.bfi.pojo.vo.BfiBillSaveResultItemVO; import org.springblade.thirdparty.bfi.pojo.vo.BfiBillSaveResultVO; +import org.springblade.thirdparty.bfi.pojo.vo.BfiInvoicePoolItemVO; import org.springblade.thirdparty.bfi.pojo.vo.BfiReceivedInvoiceVO; import org.springblade.thirdparty.bfi.service.IBfiService; import org.springblade.thirdparty.bfi.util.BfiUtil; @@ -49,10 +51,12 @@ import org.springblade.transport.service.KingdeeSyncContext; import org.springframework.stereotype.Component; import java.math.BigDecimal; +import java.math.RoundingMode; import java.time.LocalDate; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collection; import java.util.LinkedHashMap; import java.util.List; @@ -191,8 +195,11 @@ public class KingdeeReceivedInvoiceSyncSupport { param.setOrgNumber(kingdeeApBillSyncSupport.bfiOrgNumber(contract, context)); // 【必填】结算币别:合同结算币种 param.setCurrencyNumber(kingdeeApBillSyncSupport.bfiCurrency(contract)); - // 【必填】往来户.编码:合同乙方(开票供应商)客商编码 - param.setReceivablessuppNumber(kingdeeApBillSyncSupport.bfiAsstactNumber(contract, context)); + // 【必填】往来户.编码:发票销售方金蝶供应商代码(缺失实时查 A901-1,仍缺失报错阻断) + String supplierNumber = this.requireSupplierNumber(receipt, mirror); + param.setReceivablessuppNumber(supplierNumber); + // 销售方名称.编码(非必填):同往来户供应商代码 + param.setAsstactNumber(supplierNumber); // 【必填】汇率表.编码 param.setExratetableNumber(BfiConstant.EXRATE_TABLE_DEFAULT); // 【必填】销方名称:镜像开票单位 @@ -214,8 +221,8 @@ public class KingdeeReceivedInvoiceSyncSupport { // param.setRemark(receipt.getRemark()); // 【必填】外部单据类型 param.setAwrpSrcType(KingdeeBfiConstant.BFI_RECEIVED_INVOICE_SRC_TYPE); - // 【必填】明细分录:与应付单 A102-2 同源同口径,结算明细行+结算合计费用项各一条(不聚合) - param.setEntry(buildEntries(entryGroups)); + // 【必填】明细分录:逐行取发票明细行(镜像 items_json),不与结算明细同源 + param.setEntry(this.buildEntries(receipt, mirror)); BfiBillSaveResultVO saveResult = bfiService.saveReceivedInvoice(param); kingdeeApBillSyncSupport.requireBfiSaveSuccess(saveResult, "推送金蝶收票单"); @@ -224,6 +231,30 @@ public class KingdeeReceivedInvoiceSyncSupport { savedItem.getId() == null ? null : String.valueOf(savedItem.getId())); } + /** + * 往来户供应商编码:优先取登记上已回填的 supplierCode;缺失时按销方名称实时查一次 A901-1 并回填实体 + * (调用方推送成功后随推送结果落库),仍缺失报错阻断 + * + * @param receipt 收票登记 + * @param mirror 票池镜像(销方名称=开票单位) + * @return 金蝶供应商编码 + */ + private String requireSupplierNumber(InvoiceReceipt receipt, KingdeeInvoicePool mirror) { + String supplierCode = receipt.getSupplierCode(); + if (Func.isBlank(supplierCode)) { + log.info("收票登记[{}]往来户供应商代码缺失,按销方[{}]实时查询 A901-1", receipt.getInvoiceNo(), mirror.getIssuerName()); + supplierCode = kingdeeApBillSyncSupport.queryCustomerCode(mirror.getIssuerName()); + if (Func.isNotBlank(supplierCode)) { + receipt.setSupplierCode(supplierCode); + } + } + if (Func.isBlank(supplierCode)) { + throw new ServiceException("推送金蝶收票单失败:发票销售方[" + Func.toStr(mirror.getIssuerName()) + + "]未建档金蝶供应商编码,请先维护客商档案后再推送"); + } + return supplierCode; + } + /** * A303-2 收票单推送结果 * @@ -234,42 +265,121 @@ public class KingdeeReceivedInvoiceSyncSupport { } /** - * 分录组装与应付单 A102-2 明细同源同口径:结算明细按 货物→费用大类→税率 拆分行 + 手动添加的结算合计费用项每行一条, - * 物料/数量/单价/含税/不含税金额 - * 取自 {@link KingdeeApBillSyncSupport.BfiEntryGroup}(与应付单分录同一解析结果),分录标识同键, - * 便于金蝶侧收票单明细与应付单明细逐行勾稽;数量无效时兜底 1; - * 票面明细行(items_json)不再作为分录取数来源,税收分类/税率/税额随应付口径暂不送(联调强校验时再补) + * 分录组装:逐行取发票明细行(镜像 items_json 反序列化为 {@link BfiInvoicePoolItemVO}),一条明细行一条分录: + * 价税合计=明细金额+税额(合计为 0 拦截);数量取 num(≤0/空/不可解析兜底 1);单价取 unitPrice(空按价税合计/数量反算); + * 物料编码取行商品编码 goodsCode(缺失拦截,去尾部 0);不含税金额取明细金额(空不送); + * 税收分类编码/税率表编码/税额不送;分录标识=发票号码_seq(重推幂等候选键) * - * @param entryGroups 分摊结算单解析出的分录(与应付单 A102-2 同一解析结果) + * @param receipt 收票登记(发票号码,分录标识前缀) + * @param mirror 票池镜像(明细行 items_json) * @return A303-2 明细分录 */ - private List buildEntries(List entryGroups) { + private List buildEntries(InvoiceReceipt receipt, KingdeeInvoicePool mirror) { + if (Func.isBlank(mirror.getItemsJson())) { + throw new ServiceException("推送金蝶收票单失败:发票[" + receipt.getInvoiceNo() + "]票池镜像无明细行,请先按票号补查票池明细"); + } + List items = this.parseMirrorItems(receipt.getInvoiceNo(), mirror.getItemsJson()); + if (Func.isEmpty(items)) { + throw new ServiceException("推送金蝶收票单失败:发票[" + receipt.getInvoiceNo() + "]票池镜像明细行为空,请先按票号补查票池明细"); + } List entries = new ArrayList<>(); - for (KingdeeApBillSyncSupport.BfiEntryGroup entryGroup : entryGroups) { - BfiReceivedInvoiceSaveEntryDTO entry = new BfiReceivedInvoiceSaveEntryDTO(); - // 【必填】价税合计:该行含税金额(与应付单 e_pricetaxtotal 同源取数) - entry.setEPricetaxtotal(kingdeeApBillSyncSupport.money(entryGroup.amount())); - // 【必填】数量:该行运输数量,无吨位数据时兜底 1(与应付单口径一致) - entry.setQuantity(entryGroup.quantity().compareTo(BigDecimal.ZERO) > 0 ? entryGroup.quantity() : BigDecimal.ONE); - // 【必填】单价:该行运输单价(与应付单同源;单据头 isincludetax=true 录入价税合计,单价仅作参考) - entry.setPrice(entryGroup.price()); - // 【必填】物料编码.编码:按装卸大类/项目业务模式/运单运输方式映射(thirdparty.bfi.material.* 配置,与应付单同源) - entry.setMaterialNumber(entryGroup.materialNumber()); - // 金额(不含税):该行结算金额(不含税)(与应付单 e_amount 同源取数;结算合计费用项行无不含税金额不传) - if (entryGroup.amountNoTax() != null) { - entry.setEAmount(kingdeeApBillSyncSupport.money(entryGroup.amountNoTax())); + int rowNo = 0; + for (BfiInvoicePoolItemVO item : items) { + rowNo++; + // 分录序号:优先取明细行 seq,缺失按行号兜底 + String seq = Func.isBlank(item.getSeq()) ? String.valueOf(rowNo) : item.getSeq().trim(); + BigDecimal detailAmount = this.parseMirrorAmount(receipt.getInvoiceNo(), seq, "明细金额", item.getDetailAmount()); + BigDecimal taxAmount = this.parseMirrorAmount(receipt.getInvoiceNo(), seq, "税额", item.getTaxAmount()); + // 【必填】价税合计:明细金额+税额(合计为 0 拦截) + BigDecimal priceTaxTotal = kingdeeApBillSyncSupport.money(detailAmount == null ? BigDecimal.ZERO : detailAmount) + .add(kingdeeApBillSyncSupport.money(taxAmount == null ? BigDecimal.ZERO : taxAmount)); + if (priceTaxTotal.compareTo(BigDecimal.ZERO) == 0) { + throw new ServiceException("推送金蝶收票单失败:发票[" + receipt.getInvoiceNo() + "]分录[" + seq + "]价税合计为 0,无法推送"); } - // 【必填】外部系统来源分录标识(候选键):与应付单分录同键(结算明细行单据号/结算合计费用项拼接),便于逐行勾稽 - entry.setAwrpEntryCode(entryGroup.entryCode()); + if (Func.isBlank(item.getGoodsCode())) { + throw new ServiceException("推送金蝶收票单失败:发票[" + receipt.getInvoiceNo() + "]分录[" + seq + "]缺少行商品编码,无法确定物料编码"); + } + BfiReceivedInvoiceSaveEntryDTO entry = new BfiReceivedInvoiceSaveEntryDTO(); + entry.setEPricetaxtotal(priceTaxTotal); + // 【必填】数量:明细行数量(≤0/空/不可解析兜底 1) + entry.setQuantity(this.mirrorQuantity(item.getNum())); + // 【必填】单价:明细行单价(空按价税合计/数量反算;单据头 isincludetax=true,单价仅作参考) + entry.setPrice(this.mirrorPrice(item.getUnitPrice(), priceTaxTotal, entry.getQuantity())); + String goodsCode = this.stripTrailingZeros(item.getGoodsCode().trim()); + // 【必填】物料编码.编码:行商品编码(金蝶档案不含末尾补零,去掉尾部 0) + // entry.setMaterialNumber(goodsCode); + // 税收分类编码.编码:数电票行商品编码即税收分类编码 + entry.setETaxclassNumber(goodsCode); + // 金额(不含税):明细金额(空不送);税收分类编码/税率表编码/税额不送,税额仅参与价税合计 + if (detailAmount != null) { + entry.setEAmount(kingdeeApBillSyncSupport.money(detailAmount)); + } + // 【必填】外部系统来源分录标识(候选键):发票号码_seq,重推同键幂等 + entry.setAwrpEntryCode(receipt.getInvoiceNo() + "_" + seq); entries.add(entry); } - if (entries.isEmpty()) { - throw new ServiceException("推送金蝶收票单失败:分摊结算单无结算明细与结算合计费用项,无法组装收票单明细"); - } return entries; } + /** 商品编码去尾部 0:金蝶物料编码档案不含末尾补零(如 3010102020100000000→30101020201);全 0 兜底保留原值 */ + private String stripTrailingZeros(String code) { + String stripped = code.replaceAll("0+$", ""); + return stripped.isEmpty() ? code : stripped; + } + + /** 镜像明细行反序列化:items_json 解析异常按脏数据拦截(A801-1 原样落库,理论上可解析) */ + private List parseMirrorItems(String invoiceNo, String itemsJson) { + try { + return Arrays.asList(JsonUtil.parse(itemsJson, BfiInvoicePoolItemVO[].class)); + } catch (RuntimeException exception) { + log.warn("收票登记[{}]票池镜像明细行反序列化失败:{}", invoiceNo, exception.getMessage()); + throw new ServiceException("推送金蝶收票单失败:发票[" + invoiceNo + "]票池镜像明细行数据异常,请先按票号补查票池明细"); + } + } + + /** 明细行数量:num ≤0/空/不可解析兜底 1 */ + private BigDecimal mirrorQuantity(String num) { + BigDecimal quantity = this.parseMirrorAmountQuietly(num); + if (quantity == null || quantity.compareTo(BigDecimal.ZERO) <= 0) { + return BigDecimal.ONE; + } + return quantity; + } + + /** 明细行单价:unitPrice 空/不可解析按 价税合计/数量 反算;金蝶要求小数不超过 4 位,统一 4 位舍入 */ + private BigDecimal mirrorPrice(String unitPrice, BigDecimal priceTaxTotal, BigDecimal quantity) { + BigDecimal price = this.parseMirrorAmountQuietly(unitPrice); + BigDecimal result = price != null && price.compareTo(BigDecimal.ZERO) > 0 + ? price + : priceTaxTotal.divide(quantity, 4, RoundingMode.HALF_UP); + return result.setScale(4, RoundingMode.HALF_UP); + } + + /** 镜像明细数值解析:空返回 null,不可解析返回 null(数量/单价有兜底,金额空不送) */ + private BigDecimal parseMirrorAmountQuietly(String value) { + if (Func.isBlank(value)) { + return null; + } + try { + return new BigDecimal(value.trim()); + } catch (NumberFormatException exception) { + return null; + } + } + + /** 镜像明细金额解析:空返回 null,不可解析按脏数据拦截(金额直接进分录,不能静默丢弃) */ + private BigDecimal parseMirrorAmount(String invoiceNo, String seq, String fieldLabel, String value) { + if (Func.isBlank(value)) { + return null; + } + try { + return new BigDecimal(value.trim()); + } catch (NumberFormatException exception) { + throw new ServiceException("推送金蝶收票单失败:发票[" + invoiceNo + "]分录[" + seq + "]" + fieldLabel + "[" + value.trim() + "]无法解析,请先按票号补查票池明细"); + } + } + // ==================== A302-6 收票指定应付 ==================== /** diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsImmediatePushSupport.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsImmediatePushSupport.java index eee77238..adb50ff2 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsImmediatePushSupport.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsImmediatePushSupport.java @@ -171,6 +171,13 @@ public class HcsImmediatePushSupport { return hcsProperties.getPush().isImmediateEnabled(); } + /** + * 付款特批走合同合规开关(thirdParty.hcs.spec-pay.enabled),关闭时审批响应不再提示特批待发起/已发起 + */ + public boolean isSpecPayEnabled() { + return hcsProperties.getSpecPay().isEnabled(); + } + /** * 项目立项"通过"按钮路径:在审批事务提交返回后由 Controller 同步调用,失败原因留在当前线程供其取走 * diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsPaymentSyncSupport.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsPaymentSyncSupport.java index 3fe88486..c6af98fb 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsPaymentSyncSupport.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsPaymentSyncSupport.java @@ -86,8 +86,9 @@ public class HcsPaymentSyncSupport { public String pushPayment(PaymentApplication payment) { StringBuilder result = new StringBuilder(); // ① 结算付款特批:提交时高风险/黑名单(须特批附件)或额度超额(hcs_spec_status=need)且未发起时发起 C106-3; - // 发起后挂起等待特批结果,特批通过后不推履约数据(各业务环节推送),仅补推金蝶(驳回不产生履约数据) - if (HcsSyncConstant.SPEC_STATUS_NEED.equals(payment.getHcsSpecStatus())) { + // 发起后挂起等待特批结果,特批通过后不推履约数据(各业务环节推送),仅补推金蝶(驳回不产生履约数据); + // spec-pay.enabled=false(特批不走合同系统)时跳过发起,need 仅作附件要求留痕 + if (HcsSyncConstant.SPEC_STATUS_NEED.equals(payment.getHcsSpecStatus()) && hcsProperties.getSpecPay().isEnabled()) { String specResult = hcsSpecPaySyncSupport.pushSpecPay(payment); // C106-4 更新路径不改状态,统一归一为特批审核中,避免下轮扫描重复发起 if (HcsSyncConstant.SPEC_STATUS_NEED.equals(payment.getHcsSpecStatus())) { diff --git a/blade-third-party-api/blade-bfi-api/src/main/java/org/springblade/thirdparty/bfi/config/BfiFeignClientConfig.java b/blade-third-party-api/blade-bfi-api/src/main/java/org/springblade/thirdparty/bfi/config/BfiFeignClientConfig.java index 72396d90..1c791a1d 100644 --- a/blade-third-party-api/blade-bfi-api/src/main/java/org/springblade/thirdparty/bfi/config/BfiFeignClientConfig.java +++ b/blade-third-party-api/blade-bfi-api/src/main/java/org/springblade/thirdparty/bfi/config/BfiFeignClientConfig.java @@ -69,8 +69,9 @@ public class BfiFeignClientConfig { } @Bean - public Request.Options options() { - return new Request.Options(10, TimeUnit.SECONDS, 60, TimeUnit.SECONDS, true); + public Request.Options options(BfiProperties properties) { + return new Request.Options(properties.getConnectTimeoutSeconds(), TimeUnit.SECONDS, + properties.getReadTimeoutSeconds(), TimeUnit.SECONDS, true); } @Bean diff --git a/blade-third-party-api/blade-bfi-api/src/main/java/org/springblade/thirdparty/bfi/config/BfiProperties.java b/blade-third-party-api/blade-bfi-api/src/main/java/org/springblade/thirdparty/bfi/config/BfiProperties.java index fd0ef5de..a3b59f98 100644 --- a/blade-third-party-api/blade-bfi-api/src/main/java/org/springblade/thirdparty/bfi/config/BfiProperties.java +++ b/blade-third-party-api/blade-bfi-api/src/main/java/org/springblade/thirdparty/bfi/config/BfiProperties.java @@ -64,6 +64,14 @@ public class BfiProperties { * 语言字串,默认 zh_CN */ private String language = "zh_CN"; + /** + * Feign 连接超时秒数,默认 10 秒 + */ + private long connectTimeoutSeconds = 10; + /** + * Feign 读超时秒数,默认 120 秒:金蝶保存类接口偶发超 60 秒,超时后远端可能已生成单据,重推会自动勾稽防重 + */ + private long readTimeoutSeconds = 120; /** * 是否启用 mock 模式(开发调试用):开启后 IBfiClient 走本地桩目录(有桩用桩、无桩透传真实接口,见 BfiClientMock) */ diff --git a/blade-third-party-api/blade-bfi-api/src/main/java/org/springblade/thirdparty/bfi/feign/IBfiClient.java b/blade-third-party-api/blade-bfi-api/src/main/java/org/springblade/thirdparty/bfi/feign/IBfiClient.java index dd983387..e16bc3df 100644 --- a/blade-third-party-api/blade-bfi-api/src/main/java/org/springblade/thirdparty/bfi/feign/IBfiClient.java +++ b/blade-third-party-api/blade-bfi-api/src/main/java/org/springblade/thirdparty/bfi/feign/IBfiClient.java @@ -220,10 +220,11 @@ public interface IBfiClient { * @param accessToken 访问令牌,通过 getToken 获取 * @param pageNo 页码,从1开始 * @param pageSize 页数 - * @param beginBizdate 开始业务日期 - * @param endBizdate 结束业务日期 + * @param beginBizdate 开始业务日期,必传 + * @param endBizdate 结束业务日期,必传 * @param billno 单据编号 - * @param orgNumber 付款人(公司).编码 + * @param orgNumber 付款人(公司).编码,必传,支持多个 + * @param awrpSourceBillno 外部来源单据编号(付款申请金蝶单据号),支持多个,精确过滤付款处理单 * @return 付款处理分页数据 */ @GetMapping(PAY_BILL_QUERY_URL) @@ -233,7 +234,8 @@ public interface IBfiClient { @RequestParam("beigin_bizdate") String beginBizdate, @RequestParam("end_bizdate") String endBizdate, @RequestParam(value = "billno", required = false) List billno, - @RequestParam("org_number") List orgNumber); + @RequestParam("org_number") List orgNumber, + @RequestParam(value = "awrp_sourcebillno", required = false) List awrpSourceBillno); /** * A902-1 客户查询 diff --git a/blade-third-party-api/blade-bfi-api/src/main/java/org/springblade/thirdparty/bfi/pojo/dto/BfiPayBillQueryDTO.java b/blade-third-party-api/blade-bfi-api/src/main/java/org/springblade/thirdparty/bfi/pojo/dto/BfiPayBillQueryDTO.java index 385a2dfa..e18f31ed 100644 --- a/blade-third-party-api/blade-bfi-api/src/main/java/org/springblade/thirdparty/bfi/pojo/dto/BfiPayBillQueryDTO.java +++ b/blade-third-party-api/blade-bfi-api/src/main/java/org/springblade/thirdparty/bfi/pojo/dto/BfiPayBillQueryDTO.java @@ -64,7 +64,11 @@ public class BfiPayBillQueryDTO implements Serializable { */ private List billno; /** - * 付款人(公司).编码 + * 付款人(公司).编码,必传,支持多个 */ private List orgNumber; + /** + * 外部来源单据编号(付款申请金蝶单据号),支持多个,精确过滤付款处理单 + */ + private List awrpSourceBillno; } diff --git a/blade-third-party-api/blade-bfi-api/src/main/java/org/springblade/thirdparty/bfi/service/impl/BfiServiceImpl.java b/blade-third-party-api/blade-bfi-api/src/main/java/org/springblade/thirdparty/bfi/service/impl/BfiServiceImpl.java index e010b0f5..bb6e2c1c 100644 --- a/blade-third-party-api/blade-bfi-api/src/main/java/org/springblade/thirdparty/bfi/service/impl/BfiServiceImpl.java +++ b/blade-third-party-api/blade-bfi-api/src/main/java/org/springblade/thirdparty/bfi/service/impl/BfiServiceImpl.java @@ -294,6 +294,7 @@ public class BfiServiceImpl implements IBfiService { if (param == null || param.getPageNo() == null || param.getPageSize() == null) { throw new BfiException("查询BFI付款处理失败,分页参数不能为空"); } + // 接口必填参数:业务日期范围 + 付款组织编码;awrp_sourcebillno 可选,精确过滤付款处理单 if (StringUtil.isBlank(param.getBeginBizdate()) || StringUtil.isBlank(param.getEndBizdate())) { throw new BfiException("查询BFI付款处理失败,业务日期范围不能为空"); } @@ -302,7 +303,8 @@ public class BfiServiceImpl implements IBfiService { } String accessToken = getAccessToken(); BfiResultVO> result = commonCall("付款处理查询", () -> client.payBillQuery(accessToken, - param.getPageNo(), param.getPageSize(), param.getBeginBizdate(), param.getEndBizdate(), param.getBillno(), param.getOrgNumber())); + param.getPageNo(), param.getPageSize(), param.getBeginBizdate(), param.getEndBizdate(), param.getBillno(), + param.getOrgNumber(), param.getAwrpSourceBillno())); return result.getData(); } diff --git a/blade-third-party-api/blade-hcs-api/src/main/java/org/springblade/hcs/config/HcsProperties.java b/blade-third-party-api/blade-hcs-api/src/main/java/org/springblade/hcs/config/HcsProperties.java index 56580bb0..5998ab9b 100644 --- a/blade-third-party-api/blade-hcs-api/src/main/java/org/springblade/hcs/config/HcsProperties.java +++ b/blade-third-party-api/blade-hcs-api/src/main/java/org/springblade/hcs/config/HcsProperties.java @@ -273,6 +273,13 @@ public class HcsProperties { @Data public static class SpecPay { + /** + * 付款特批走合同合规开关(thirdParty.hcs.spec-pay.enabled,默认 false 不推送合同合规): + * true 时审批通过后发起 C106-3/4、挂起"特批审核"、特批完成前拦截金蝶推送; + * false 时不再发起 C106-3/4、不挂起、不拦截金蝶推送; + * 提交时高风险/黑名单风险校验与「特批附件」必传逻辑保留(risk.check-on-payment) + */ + private boolean enabled = false; /** * 特批单据状态(fd_doc_status,示例 10,§9-U3 待联调) */ diff --git a/doc/docs/hcs合同/2026-10-23-HCS合同管理操作手册.md b/doc/docs/hcs合同/2026-10-23-HCS合同管理操作手册.md index 808af093..518114b7 100644 --- a/doc/docs/hcs合同/2026-10-23-HCS合同管理操作手册.md +++ b/doc/docs/hcs合同/2026-10-23-HCS合同管理操作手册.md @@ -1,11 +1,18 @@ # HCS 合同管理操作手册 -> **文档日期**:2026-10-23 +> **文档日期**:2026-10-23(最近更新:2026-11-01) > **适用系统**:运输板块数字化管理平台(TMS) > **适用对象**:合同经办人、合同负责人、合同审批人、合规管理员 > **文档目的**:介绍合同新增、审批、变更、附件归档及 HCS 合规同步的完整操作方法 > **截图说明**:本文截图取自测试环境,合同编号、客商名称、金额等均为测试数据;不同环境因角色权限、功能开关不同,页面按钮和状态可能略有差异,以实际页面为准。 +## 修改记录 + +| 日期 | 修改内容 | +| --- | --- | +| 2026-11-01 | ① 客商黑名单口径调整:黑名单以金蝶黑名单查询(A801-5)命中为准(含失信类别、列入事由),合同合规系统 C101-1 的黑名单状态不再用于展示与筛选;合同列表/详情"客商风险"改为实时关联客商档案组装,红色标签悬浮可查看甲乙双方风险明细(新增 16、17、18 截图)。② 新增客商风险每日定时刷新任务(每天 02:30,`risk.refresh-enabled` / `risk.refresh-cron`),刷新合同期限内生效合同关联客商并落库客商档案(新增 12.7 节)。③ 归档状态即时同步合规:上传"双章归档文件"后即时调 C103-11 更新合规归档状态(fdArchiveStatus=toarchive)并挂载归档附件;未推送合同首次推送报文自带归档状态(更新 9.4 节)。④ 付款链履约数据推送时点调整:金蝶实际付款门禁——未实付时履约计划(C103-9)与履约记录(C108)均不推送,金蝶付款回写后自动推送(同步更新纯操作版 3.11/4.5)。⑤ 合同推送报文新增业务合同号(fdBusContractNumber,=合同编号)与归档状态(fdArchiveStatus)字段;币种映射样例更新为 RMB/CNY→人民币、USD→美元。⑥ 新增 19 项目立项列表、20 付款申请列表截图;常见问题补充 3 条。 | +| 2026-10-23 | 首次整理。 | + --- ## 1. 先了解合同管理与 HCS @@ -29,6 +36,8 @@ | 合规合同ID | 合同成功推送 HCS 后,由 HCS 返回的合同唯一标识 | | 金蝶合同ID | 合同在 HCS 完成合规及金蝶同步后,回写到 TMS 的金蝶合同编号 | | 特批 | 高风险或黑名单客商合同,需经集团特批后才能完成合规流程 | +| 黑名单 | 客商被列入外部(国资委)黑名单,**以金蝶黑名单查询(A801-5)命中为准**,命中记录含失信类别与列入事由;金蝶分 A 类(禁止交易类)和 B 类(风险警示类) | +| 风险等级 | 合同合规系统(HCS)对客商的风险评级:0=低风险、1=中风险、2=高风险 | | 临时合同 | 用于提前生效的合同,审批通过后有 **30 天** 临时效力 | | 正式合同 | 完成正式审批、可长期履约的合同,是 HCS 合规同步与归档的主要对象 | | 合同变更 | 正式合同内容发生变化时发起的变更审批流程 | @@ -38,7 +47,7 @@ - 页面中带 `*` 的字段为必填项;保存草稿时可先填写部分信息,提交临时合同或正式合同前需补全。 - 合同编号可不填写,系统会按合同负责部门配置的规则自动生成;如业务要求手工编号,可直接填写。 - 临时合同审批通过后有效期为 **30 天**(自提交临时合同审批之日起算),到期前应及时转为正式合同。 -- 高风险、黑名单客商合同,**提交审批前必须上传“特批附件”**,否则系统会阻止提交。 +- 高风险、黑名单客商合同,**提交审批前必须上传“特批附件”**,否则系统会阻止提交。黑名单口径见 12.7 节。 - 合同审批通过后,系统会将合同推送 HCS;正式合同与合同变更是合规同步、归档的主要场景。是否自动推送由环境开关决定,也可在页面手动推送。 - 合同已推送 HCS 后,如需修改合同信息,必须走“变更”流程,**不要重复新增合同**,否则会在合规系统产生重复合同。 - 合同列表默认按创建时间倒序排列。 @@ -150,6 +159,7 @@ flowchart TD | 合同阶段 | 草稿 / 临时合同 / 正式合同 / 已终止 | | 审核状态 | 当前审批状态 | | 归档状态 | 未归档 / 已归档 | +| 客商风险 | 甲方/乙方客商的风险等级与黑名单组合展示(如“低风险、黑名单”);红色标签鼠标悬浮可查看风险明细;数据实时关联客商档案组装 | | 合规状态 | 待推送 / 已推送 / 推送失败;鼠标悬停可查看失败原因 | | 金蝶合同ID | 合规同步完成后回写的金蝶合同编号 | | 当前节点 / 当前处理人 | 当前审批节点及处理人 | @@ -172,8 +182,7 @@ flowchart TD | 临时合同 - 已驳回 | 查看、编辑、复制、重新提交转正 | | 正式合同 - 审批中 | 查看、撤回、通过/驳回(审批人)、流程 | | 正式合同 - 审批通过 / 变更审批通过 | 查看、复制、变更、附件上传、流程、推送合规(未同步时) | -| 正式合同 - 变更审批中 | 查看、通过/驳回(审批人)、撤回变更、流程 | -| 正式合同 - 变更驳回 | 查看、编辑变更内容、重新提交变更 | +| 正式合同 - 变更审批中 / 变更驳回 | 存量数据状态;现变更的审批、撤回、重新提交均在“合同变更”页面操作(见第 10 章) | > 说明: > - “复制”会以当前合同为模板打开新增页面,便于快速新建相似合同。 @@ -317,9 +326,9 @@ flowchart TD | 审批通过 | 审批完成 | | 已驳回 | 审批被驳回,需修改后重新提交 | | 已撤回 | 提交人主动撤回,可修改后重新提交 | -| 变更审批中 | 合同变更审批中 | +| 变更审批中 | 合同变更审批中(现由“合同变更”单跟踪,存量数据仍可能显示) | | 变更审批通过 | 合同变更审批完成 | -| 变更驳回 | 合同变更被驳回,可修改后重新提交变更 | +| 变更驳回 | 合同变更被驳回(现由“合同变更”单跟踪,存量数据仍可能显示) | ### 6.4 其他状态说明 @@ -384,10 +393,14 @@ flowchart TD | --- | --- | | 合规同步状态 | 待推送 / 已推送 / 推送失败 | | 合规合同ID | HCS 返回的合同唯一标识 | -| 客商风险 | 是否黑名单、风险等级 | +| 客商风险 | 甲方/乙方客商风险等级与黑名单组合展示(如“高风险、黑名单”);黑色标签为命中风险,鼠标悬浮可查看明细(失信类别、列入事由、风险等级) | | 特批状态 | 待特批 / 特批中 / 特批通过 / 特批驳回 | | 特批说明 | 特批原因或处理说明 | +![合同列表客商风险悬浮明细](images/18-合同列表-客商风险悬浮.png) + +> 合同列表“客商风险”列:红色标签为高风险/黑名单客商,鼠标悬浮展示甲乙双方风险明细(失信类别、列入事由、风险等级)。 + ### 8.3 用印材料与附件 向下滚动可查看用印材料、计费信息、结算单规则、付款比例设置、附件(佐证材料等)。 @@ -438,25 +451,28 @@ flowchart TD - 上传附件类型为 **双章归档文件** 后,合同归档状态会在审批通过或附件保存后变为“已归档”。 - 双章归档文件属于用印材料,请在“附件位置”中选择“用印材料”。 +- **归档状态即时同步合规**:上传双章归档文件保存后,若合同已推送合规(已有合规合同ID),系统会立即调 C103-11 更新合规侧归档状态为“待归档”(fdArchiveStatus=toarchive),并将归档附件挂载到合规合同的其他附件面板;若合同尚未推送合规,首次推送报文会自带归档状态。合规侧后续的归档流转(归档中/已归档/归档异常)由合规系统自行维护。 --- ## 10. 合同变更 -### 10.1 适用条件 +### 10.1 适用条件与变更单 只有**正式合同**,且审核状态为“审批通过”“变更审批通过”或“变更驳回”时,才允许发起变更。 +合同变更是**独立功能页面**(菜单:合同管理 → 合同变更),以“合同变更单”形式管理:每张变更单有独立编号、变更类型、审批状态和合规同步状态,与原合同分开审批。 + ### 10.2 发起变更 1. 在合同列表找到正式合同。 -2. 点击操作列 **变更**(变更驳回后可点击“重新提交变更”)。 -3. 进入“合同变更”页面。 +2. 点击操作列 **变更**(变更驳回后可点击“重新提交变更”),系统创建一张合同变更单草稿并自动跳转“合同变更”页面。同一合同已有审批中的变更单时不允许再次发起;已有草稿、已撤回或变更驳回的变更单时,直接打开继续编辑。 ![合同变更-基本信息](images/09-合同变更-基本信息.png) > 合同变更页面 +3. 选择 **变更类型**:合同信息变更 / 结算规则变更 / 终止合同。结算规则变更仅调整计费信息与结算单规则,不校验合同名称、结算币种等基本信息。 4. 修改需要变更的内容:基本信息、用印材料、计费信息、结算单规则、付款比例、附件等。 5. 页面会展示风险客商提示;如存在高风险/黑名单客商,变更提交前同样必须上传“特批附件”。 @@ -468,18 +484,21 @@ flowchart TD 1. 在“变更原因”中填写本次变更的原因,最多 500 字。 2. 在“变更材料”区域上传相关附件。 -3. 点击页面底部 **提交**。 -4. 在确认弹窗中点击“确定”。 +3. 点击页面底部 **提交**(从“合同变更”菜单进入时可先 **保存草稿**)。 +4. 在确认弹窗中点击“确定”,变更单变为“审批中”。 ### 10.4 变更审批 -- 提交后审核状态变为“变更审批中”。 -- 审批人可执行“通过”“驳回”“撤回变更”。 -- 审批通过后,审核状态变为“变更审批通过”,系统按合规合同ID更新 HCS;如该合同尚未推送过,则按首次推送处理。 -- 审批驳回后,审核状态变为“变更驳回”,可点击“重新提交变更”重新填写变更原因后再次提交。 +审批在“合同变更”列表页操作;接入 MK 审批的环境,提交后进入 MK 审批流。 + +- 审批人可执行“审批通过”“审批驳回”“撤回”。 +- 审批通过后,变更单回写原合同:原合同状态变为“变更审批通过”,系统按合规合同ID更新 HCS;如该合同尚未推送过,则按首次推送处理。 +- 审批驳回后,变更单状态变为“变更驳回”,可重新编辑后再次提交。 +- 撤回仅限审批中的变更单,撤回后状态变为“已撤回”,可重新编辑后再次提交;撤回不影响原合同状态。 +- 仅草稿、已撤回、变更驳回的变更单允许删除。 > 说明: -> - 变更审批通过后,变更附件会合并到合同附件中,变更原因、变更内容会被清空,变更记录保留。 +> - 变更审批通过后,变更附件会合并到合同附件中,变更原因、变更内容会被清空,变更记录保留在合同详情页底部。 > - 页面中的“终止合同”选项当前版本暂未开放,请选择“合同信息变更”。 --- @@ -513,7 +532,7 @@ flowchart TD | ① 客商风险校验 | 校验甲乙双方是否高风险/黑名单(C101-1) | | ② 附件转传 | 将合同附件转传到合规系统(C103-2) | | ③ 合同判重 | 按合同编号等信息判断合规系统是否已存在(C103-3) | -| ④ 起草 / 更新 | 首次推送起草合同(C103-1);已有合规合同ID时更新合同(C103-11) | +| ④ 起草 / 更新 | 首次推送起草合同(C103-1);已有合规合同ID时更新合同(C103-11)。报文含业务合同号(fdBusContractNumber,取值同合同编号)与归档状态(fdArchiveStatus,已上传双章归档文件时传“待归档”) | | ⑤ 履约计划同步 | 同步合同履约计划/履约规则(C103-9),系统自动创建默认履约计划,无需人工维护 | | ⑥ 特批 | 高风险/黑名单客商合同自动进入特批流程 | @@ -569,8 +588,8 @@ flowchart TD 当合同甲方或乙方为外部客商,且命中以下任一情况时,需要走特批流程: -- 客商为黑名单; -- 客商风险等级为高风险。 +- 客商为黑名单(以金蝶黑名单查询 A801-5 命中为准,见 12.7 节); +- 客商风险等级为高风险(合同合规系统 C101-1 评级)。 ### 12.2 提交前:上传特批附件 @@ -629,6 +648,27 @@ flowchart TD 合同详情页右上角的 **发起特批** 是旧版独立发起入口。当前合规接口已改为随合同推送自动发起特批,正常流程**无需点击该按钮**;点击后如提示接口不存在,不影响合同数据。如特批状态长时间停留在“待特批”,请联系系统管理员重新推送合同。 +### 12.7 客商风险数据与每日刷新 + +合同列表、详情和客商档案展示的“客商风险”来自两个外部系统,每日自动更新到客商档案: + +| 数据 | 来源 | 说明 | +| --- | --- | --- | +| 风险等级 | 合同合规系统(C101-1) | 低风险 / 中风险 / 高风险 | +| 金蝶黑名单 | 金蝶(星瀚)· 国资委黑名单(A801-5) | 命中记录失信类别(A 类禁止交易类 / B 类风险警示类)与列入事由 | + +- **刷新时机**:系统每天凌晨 02:30 自动刷新(合同期限内生效合同的甲乙方关联客商);当天新增的客商在客商档案表单里有实时风险提示,列表数据次日刷新后生效。 +- **展示口径**:黑名单仅指金蝶黑名单命中;风险等级与黑名单组合展示(等级在前,如“高风险、黑名单”);内部组织客商不查询风险,默认“无风险”。 +- **悬浮明细**:红色标签(黑名单/高风险)鼠标悬浮可查看明细,格式:`【客商名称】:被列入外部黑名单(失信类别:xx);列入事由 xx;风险等级:xx`。 + +![客商档案列表客商风险列](images/16-客商档案列表-客商风险.png) + +> 客商档案列表:搜索栏“客商风险”下拉可筛选黑名单/高风险/中风险/低风险;红色标签为命中风险客商。 + +![客商风险悬浮明细](images/17-客商风险悬浮明细.png) + +> 红色标签悬浮明细:失信类别、列入事由、风险等级。 + --- ## 13. 常见问题 @@ -681,6 +721,18 @@ A:说明该合同没有关联外部审批流程实例。请以列表/详情中 A:批量推送只允许“审批通过/变更审批通过”且未同步的合同。请取消勾选不符合条件的合同,或单独对符合条件的合同发起推送。 +**Q13:客商档案/合同列表的“客商风险”列显示“-”是为什么?** + +A:该客商不在任何合同期限内生效合同的关联范围内,尚未被每日刷新任务(每天 02:30)查询过;或客商档案刚创建、数据次日才落库。当天需要确认时,可在客商档案新增/编辑表单填写客商名称,系统会实时查询并显示风险提示。 + +**Q14:客商明明命中了金蝶黑名单,列表为什么只显示风险等级、没有“黑名单”?** + +A:黑名单标签需要金蝶黑名单记录的失信类别/列入事由数据(每日刷新任务落库)。数据缺失时只显示风险等级;等待次日刷新或联系管理员执行刷新后恢复完整展示。 + +**Q15:付款特批通过后,为什么合规系统里迟迟没有履约计划和履约记录?** + +A:付款链的履约数据以金蝶实际付款为门禁:付款审批通过只做本地计划金额累加,**金蝶完成实际付款并回写实付金额后**,系统才自动推送履约计划(C103-9)与履约记录(C108)。实付前属正常等待,不是推送失败。 + --- ## 14. 操作权限速查 @@ -692,12 +744,22 @@ A:批量推送只允许“审批通过/变更审批通过”且未同步的合 | 删除合同 | 草稿状态 | | 复制合同 | 具有合同管理复制权限,复制后为新增草稿 | | 转正式合同 | 临时合同 + 审批通过 | -| 变更 | 正式合同 + 审批通过 / 变更审批通过 | -| 重新提交变更 | 正式合同 + 变更驳回 | +| 变更 | 正式合同 + 审批通过 / 变更审批通过(创建变更单草稿并跳转“合同变更”页面) | +| 重新提交变更 | 正式合同 + 变更驳回(存量数据;现变更单重提在“合同变更”页面操作) | | 附件上传 | 正式合同 + 审批通过 / 变更审批通过 | | 推送合规 | 审批通过 / 变更审批通过 + 未同步 | -| 通过 / 驳回 | 审批中 / 变更审批中 + 审批权限 | -| 撤回 | 审批中(撤回变更仅限变更审批中) | +| 通过 / 驳回 | 审批中 / 变更审批中 + 审批权限(变更的审批现由“合同变更”单操作) | +| 撤回 | 审批中(变更单的撤回在“合同变更”页面操作) | + +**“合同变更”页面(菜单:合同管理 → 合同变更):** + +| 操作 | 所需条件 | +| --- | --- | +| 保存草稿 / 编辑 | 草稿、已撤回、变更驳回的变更单 | +| 提交(提交审批) | 草稿、已撤回、变更驳回的变更单 | +| 撤回 | 变更单审批中 | +| 审批通过 / 审批驳回 | 变更单审批中 + 审批权限 | +| 删除 | 草稿、已撤回、变更驳回的变更单 | --- diff --git a/doc/docs/hcs合同/2026-10-23-HCS合同管理操作手册(纯操作版).assets/01-合同列表.png b/doc/docs/hcs合同/2026-10-23-HCS合同管理操作手册(纯操作版).assets/01-合同列表.png index 7553fe86..326c01e0 100644 Binary files a/doc/docs/hcs合同/2026-10-23-HCS合同管理操作手册(纯操作版).assets/01-合同列表.png and b/doc/docs/hcs合同/2026-10-23-HCS合同管理操作手册(纯操作版).assets/01-合同列表.png differ diff --git a/doc/docs/hcs合同/2026-10-23-HCS合同管理操作手册(纯操作版).assets/16-客商档案列表-客商风险.png b/doc/docs/hcs合同/2026-10-23-HCS合同管理操作手册(纯操作版).assets/16-客商档案列表-客商风险.png new file mode 100644 index 00000000..0ee0f7b8 Binary files /dev/null and b/doc/docs/hcs合同/2026-10-23-HCS合同管理操作手册(纯操作版).assets/16-客商档案列表-客商风险.png differ diff --git a/doc/docs/hcs合同/2026-10-23-HCS合同管理操作手册(纯操作版).assets/17-客商风险悬浮明细.png b/doc/docs/hcs合同/2026-10-23-HCS合同管理操作手册(纯操作版).assets/17-客商风险悬浮明细.png new file mode 100644 index 00000000..7273b7e3 Binary files /dev/null and b/doc/docs/hcs合同/2026-10-23-HCS合同管理操作手册(纯操作版).assets/17-客商风险悬浮明细.png differ diff --git a/doc/docs/hcs合同/2026-10-23-HCS合同管理操作手册(纯操作版).assets/18-合同列表-客商风险悬浮.png b/doc/docs/hcs合同/2026-10-23-HCS合同管理操作手册(纯操作版).assets/18-合同列表-客商风险悬浮.png new file mode 100644 index 00000000..9c631e7d Binary files /dev/null and b/doc/docs/hcs合同/2026-10-23-HCS合同管理操作手册(纯操作版).assets/18-合同列表-客商风险悬浮.png differ diff --git a/doc/docs/hcs合同/2026-10-23-HCS合同管理操作手册(纯操作版).assets/19-项目立项列表.png b/doc/docs/hcs合同/2026-10-23-HCS合同管理操作手册(纯操作版).assets/19-项目立项列表.png new file mode 100644 index 00000000..12ef7911 Binary files /dev/null and b/doc/docs/hcs合同/2026-10-23-HCS合同管理操作手册(纯操作版).assets/19-项目立项列表.png differ diff --git a/doc/docs/hcs合同/2026-10-23-HCS合同管理操作手册(纯操作版).assets/20-付款申请列表.png b/doc/docs/hcs合同/2026-10-23-HCS合同管理操作手册(纯操作版).assets/20-付款申请列表.png new file mode 100644 index 00000000..9cf1a6da Binary files /dev/null and b/doc/docs/hcs合同/2026-10-23-HCS合同管理操作手册(纯操作版).assets/20-付款申请列表.png differ diff --git a/doc/docs/hcs合同/2026-10-23-HCS合同管理操作手册(纯操作版).md b/doc/docs/hcs合同/2026-10-23-HCS合同管理操作手册(纯操作版).md index 2f8efae9..6f3ba19f 100644 --- a/doc/docs/hcs合同/2026-10-23-HCS合同管理操作手册(纯操作版).md +++ b/doc/docs/hcs合同/2026-10-23-HCS合同管理操作手册(纯操作版).md @@ -1,11 +1,18 @@ # HCS 合同管理操作手册(纯操作版) -> 整理日期:2026-10-23 +> 整理日期:2026-10-23(最近更新:2026-11-01) > 适用对象:合同经办人、合同负责人、合同审批人、合规管理员、实施运维。 > 本文只讲**怎么操作**:在哪个页面、点什么按钮、前提是什么、成功的标志是什么、失败了怎么处理。 -> 第 1–3 节为业务操作(合同、项目立项、付款特批);第 4 节为实施运维使用的五类 HCS 定时任务逻辑、接口与配置。 +> 第 1–3 节为业务操作(合同、项目立项、付款特批、客商风险);第 4 节为实施运维使用的定时任务逻辑、接口与配置。 > 完整字段说明、状态字典与流程背景见同目录《2026-10-23-HCS合同管理操作手册》;接口与代码细节见同目录 HCS 相关梳理文档。 +## 修改记录 + +| 日期 | 修改内容 | +|---|---| +| 2026-11-01 | ① 客商黑名单口径调整:黑名单以金蝶黑名单查询(A801-5)命中为准(含失信类别、列入事由),C101-1 黑名单状态不再用于展示与筛选;合同列表/详情“客商风险”改为实时关联客商档案组装,红色标签悬浮查看甲乙双方明细(新增 1.6、3.12 节与截图 16/17/18)。② 新增客商风险每日定时刷新任务(02:30,`risk.refresh-enabled`/`refresh-cron`),第 4 章任务数 5→6。③ 归档状态即时同步合规:上传双章归档文件后即时调 C103-11 传 fdArchiveStatus=toarchive 并挂归档附件(更新 3.9)。④ 付款链履约数据改金蝶实际付款门禁:未实付不推履约计划 C103-9 与履约记录 C108,实付回写后自动推送(更新 3.11、4.5);付款链履约计划重推纳入 `exe-stage.sync-enabled` 控制。⑤ 合同推送报文新增业务合同号(=合同编号)与归档状态字段;币种映射样例更新。⑥ 常见问题补 3 条。 | +| 2026-10-23 | 首次整理。 | + --- 合同系统 @@ -64,6 +71,7 @@ TMS 合同审批通过后,合同会推送到桂物合同合规系统(HCS) - 付款申请收款方为高风险/黑名单客商时,提交前必须上传“特批附件”,否则提交被拦截。 - 付款申请审批通过后系统自动发起付款特批;特批中“当前节点”为“特批审核”,特批未通过前不推送履约数据、不同步金蝶。 - 付款特批驳回后付款申请变“已驳回”,修改后重新提交审批会重新发起特批。 +- 上传“双章归档文件”保存后,已推送合规的合同会即时更新 HCS 归档状态(待归档)并挂载归档附件;未推送合同首次推送自带归档状态。 ### 1.5 推送与环境前提 @@ -92,6 +100,20 @@ TMS 合同审批通过后,合同会推送到桂物合同合规系统(HCS) 2. `thirdParty.hcs.spec-pay.template-id` 已配置,付款用途/性质/类型字典值正确。 3. 收款方为高风险/黑名单时,已上传“特批附件”。 +### 1.6 客商风险与黑名单口径 + +客商风险数据来自两个外部系统,每日自动刷新到客商档案(详见 3.12、4.8 节): + +| 数据 | 来源 | 说明 | +|---|---|---| +| 风险等级 | 合同合规系统(C101-1) | 低风险 / 中风险 / 高风险 | +| 金蝶黑名单 | 金蝶(星瀚)· 国资委黑名单(A801-5) | 命中记录失信类别(A 类禁止交易类 / B 类风险警示类)与列入事由 | + +- **黑名单仅指金蝶黑名单命中**;合同合规系统返回的黑名单状态字段只用于付款提交强制校验,不在列表展示。 +- 合同列表/详情、客商档案的“客商风险”列为等级+黑名单组合展示(等级在前,如“高风险、黑名单”);红色标签悬浮可查看明细(失信类别、列入事由、风险等级)。 +- 内部组织客商不查询风险,默认“无风险”。 +- 数据每天 02:30 刷新(合同期限内生效合同关联客商);当天新增客商以表单实时提示为准,列表次日生效。 + --- ## 2. 一次性准备(实施/管理员操作) @@ -102,7 +124,7 @@ TMS 合同审批通过后,合同会推送到桂物合同合规系统(HCS) | 2 | 来源与操作人 | 运维 | `thirdParty.hcs.src-system`(约定 WLTMS)、`operator-login-name` | 合规侧来源/操作人不符合约定 | | 3 | 合同类别模板映射 | 运维 | `thirdParty.hcs.contract.template-mapping`,为**每个合同类别**配置 HCS 模板ID | 未映射的类别推送必失败,提示“合同类别未配置合规模板ID” | | 4 | 附件下载基址 | 运维 | `thirdParty.hcs.attachment.file-download-base`;保持 `transfer-enabled=true` | 合同有附件时转传失败 | -| 5 | 币种映射 | 运维 | `thirdParty.hcs.contract.currency-name-mapping`(如 RMB→人民币) | 币种传错或为空 | +| 5 | 币种映射 | 运维 | `thirdParty.hcs.contract.currency-name-mapping`(样例:RMB/CNY→人民币、USD→美元) | 币种传错或为空 | | 6 | 风险校验开关 | 运维 | `thirdParty.hcs.contract.require-risk-check` 保持 `true` | 关闭后高风险合同不会触发特批 | | 7 | 履约计划同步 | 运维 | `thirdParty.hcs.exe-stage.sync-enabled`,一期默认 `false` | 不推送 C103-9 履约计划,不影响合同主流程 | | 8 | 推送开关 | 运维 | 按需开启 `push.immediate-enabled`(项目/合同即时)、`push.project/contract/payment.sync-enabled`(定时扫描) | 只能靠手动按钮推送;付款审批即时特批不受 `push.immediate-enabled` 控制 | @@ -112,10 +134,11 @@ TMS 合同审批通过后,合同会推送到桂物合同合规系统(HCS) | 12 | 付款特批模板与字典 | 运维 | `thirdParty.hcs.spec-pay.template-id`、`payment-purpose`、`payment-nature`、`payment-type` | 付款特批发起失败 | | 13 | 付款履约记录 | 运维 | `thirdParty.hcs.perf-record.sync-enabled`(一期默认 `false`);开启时同时配置 `perf-record.xform-id` | 开启但未配模板ID时履约记录推送失败 | | 14 | 付款额度校验 | 运维 | 如需“超额自动转特批”,开启 `thirdParty.hcs.credit.check-enabled` | 关闭时超额付款不会自动进入特批 | +| 15 | 客商风险每日刷新 | 运维 | `thirdParty.hcs.risk.refresh-enabled` 上线置 `true`(默认 `false`)、`refresh-cron` 默认每天 02:30 | 客商档案/合同列表“客商风险”列无数据或长期不更新 | > 模板映射的中文键在 Nacos YAML 中必须使用括号写法,例如 `"[承运商合同]": "模板ID"`,否则配置无法正确绑定。 > -> 当前 Nacos 样例只映射了“承运商合同”。客户合同、补充协议如果也要推送,必须在上线前逐类确认并补齐映射。 +> 当前 Nacos 样例已映射“承运商合同”“客户合同”。补充协议等其他类别如果也要推送,必须在上线前逐类确认并补齐映射。 --- @@ -242,10 +265,12 @@ TMS 合同审批通过后,合同会推送到桂物合同合规系统(HCS) |---|---| | 合规同步状态 | 待推送 / 已推送 / 推送失败;失败时可悬停查看原因 | | 合规合同ID | HCS 返回的合同唯一标识,推送成功后回填 | -| 客商风险 | 是否黑名单、风险等级 | +| 客商风险 | 甲方/乙方客商等级+黑名单组合展示;红色标签悬浮查看明细(失信类别、列入事由、风险等级),口径见 1.6 | | 特批状态 | 待特批 / 特批中 / 特批通过 / 特批驳回 | | 特批说明 | 特批原因或处理说明 | +![合同列表客商风险列:红色标签悬浮展示甲乙双方风险明细(失信类别、列入事由、风险等级)](./2026-10-23-HCS合同管理操作手册(纯操作版).assets/18-合同列表-客商风险悬浮.png) + **推送成功的判断标准**:合规状态为“已推送”,且“合规合同ID”不为空。 > 金蝶合同ID由 HCS 完成金蝶同步后回写,可能存在延迟;暂时为空可稍后刷新列表查看。“已推送”只代表 HCS 已受理,不代表特批和金蝶同步已经完成。 @@ -326,7 +351,7 @@ TMS 合同审批通过后,合同会推送到桂物合同合规系统(HCS) ### 3.8 合同变更并更新 HCS -**页面:合同管理 → 合同管理 → 变更** +**入口:合同管理列表操作列 → 变更;变更单在独立页面“合同变更”中填写与审批(菜单:合同管理 → 合同变更)** ![合同变更页面:在正式合同基础上修改基本信息、附件、计费、结算和付款比例](./2026-10-23-HCS合同管理操作手册(纯操作版).assets/09-合同变更-基本信息.png) @@ -334,23 +359,24 @@ TMS 合同审批通过后,合同会推送到桂物合同合规系统(HCS) 前提:合同阶段为“正式合同”,审核状态为“审批通过”“变更审批通过”或“变更驳回”。 -1. 在列表点操作列 **变更**(变更驳回后点“重新提交变更”)。 -2. 修改需要变更的内容:基本信息、用印材料、计费信息、结算单规则、付款比例、附件等。 -3. 填写 **变更原因**,最多 500 字。 -4. 在“变更材料”区域上传相关附件。 -5. 点页面底部 **提交**,在确认弹窗点“确定”。 -6. 审核状态变为 **变更审批中**。 +1. 在合同管理列表点操作列 **变更**(变更驳回后点“重新提交变更”),系统创建一张合同变更单草稿并自动跳转到“合同变更”页面。同一合同已有审批中的变更单时不允许再次发起;已有草稿、已撤回或变更驳回的变更单时,直接打开继续编辑。 +2. 选择 **变更类型**:合同信息变更 / 结算规则变更 / 终止合同。结算规则变更仅调整计费信息与结算单规则。 +3. 修改需要变更的内容:基本信息、用印材料、计费信息、结算单规则、付款比例、附件等。 +4. 填写 **变更原因**,最多 500 字。 +5. 在“变更材料”区域上传相关附件。 +6. 点页面底部 **提交**(从“合同变更”菜单进入时可先 **保存草稿**),在确认弹窗点“确定”,变更单变为 **审批中**。 -**审批人操作:** +**审批人操作(在“合同变更”列表页):** -- 通过:审核状态变为 **变更审批通过**,系统按合规合同ID更新 HCS(C103-11);如该合同从未推送过,则按首次推送处理。 -- 驳回:审核状态变为 **变更驳回**,可点“重新提交变更”重新填写并提交。 -- 撤回变更:仅变更审批中可撤回,撤回后恢复为“审批通过”。 +- 审批通过:变更单审批通过后回写原合同,原合同状态变为 **变更审批通过**,系统按合规合同ID更新 HCS(C103-11);如该合同从未推送过,则按首次推送处理。 +- 审批驳回:变更单状态变为 **变更驳回**,可重新编辑后再次提交。 +- 撤回:仅审批中可撤回,撤回后变更单状态变为 **已撤回**,可重新编辑后再次提交;撤回不影响原合同状态。 +- 仅草稿、已撤回、变更驳回的变更单允许删除。 **变更结果:** - 变更附件合并到合同附件,变更原因、变更内容被清空。 -- 变更记录保留,可在详情页底部查看。 +- 变更记录保留,可在合同详情页底部查看。 ![合同详情变更记录:展示变更日期、经办人、变更类型、变更内容、变更原因和状态](./2026-10-23-HCS合同管理操作手册(纯操作版).assets/08-合同详情-变更记录.png) @@ -382,6 +408,7 @@ TMS 合同审批通过后,合同会推送到桂物合同合规系统(HCS) - 上传附件类型为 **双章归档文件**,且附件位置选择“用印材料”时,合同审批通过或附件保存后归档状态变为 **已归档**。 - 其他附件类型不会改变归档状态。 +- **即时同步合规**:双章归档文件保存后,已推送合规的合同会立即调 C103-11 更新合规侧归档状态为“待归档”(fdArchiveStatus=toarchive)并挂载归档附件;尚未推送的合同在首次推送报文中自带归档状态。合规侧后续归档流转由合规系统自行维护。 ![合同详情附件区:可查看、下载用印材料和佐证材料](./2026-10-23-HCS合同管理操作手册(纯操作版).assets/07-合同详情-附件佐证材料.png) @@ -477,7 +504,7 @@ TMS 合同审批通过后,合同会推送到桂物合同合规系统(HCS) | 提示 | 含义 | 后续 | |---|---|---| -| 审批通过,已发起付款特批,特批通过后将自动推送履约数据并同步金蝶 | 特批已发起 | 列表“当前节点”显示“特批审核”,等待结果 | +| 审批通过,已发起付款特批,特批通过后将自动推送履约数据并同步金蝶 | 特批已发起 | 列表“当前节点”显示“特批审核”,等待结果;履约数据在金蝶实际付款后才推送(见“特批通过后”) | | 审批通过,付款特批待发起,可在详情页重推合规 | 特批尚未发起(并发跳过或前置条件不满足) | 当前页面没有重推按钮;联系实施检查模板、合同合规ID后重推 | | 审批通过,但发起付款特批/推送合规失败:xxx,可稍后重推合规 | 审批已生效,特批发起或推送失败 | 按原因修复后联系实施重推,**不要重复审批** | | 审批通过,已同步金蝶:xxx / 审批通过,但自动同步金蝶失败:xxx | 未走特批,直接同步金蝶的结果 | 金蝶失败按提示原因处理 | @@ -497,8 +524,8 @@ TMS 合同审批通过后,合同会推送到桂物合同合规系统(HCS) **特批通过后:** -1. 当前节点恢复“审批通过”。 -2. 系统自动补推履约计划(C103-9)和履约记录(C108,若开启),并自动同步金蝶。 +1. 当前节点恢复“审批通过”,系统自动同步金蝶付款申请。 +2. **金蝶实际付款后**,系统才自动推送履约计划(C103-9,受 `exe-stage.sync-enabled` 控制)和履约记录(C108,若开启):付款审批通过只做本地计划金额累加,未实付时推送结果提示“金蝶未实际付款,待付款后推送履约计划与履约记录”,属正常等待,不是推送失败。 3. 金蝶单据号、金蝶单据状态稍后回填,可刷新列表查看。 **特批驳回后:** @@ -525,25 +552,56 @@ HCS 正常情况下通过 `paymentLegal/result` 回传结果。长时间停留 任一条件不满足时,审批仍会成功,但特批发起失败;按审批提示或日志修复后重推。 +### 3.12 客商档案客商风险(查看与筛选) + +**页面:客商管理 → 客商档案(`/vehicle/customer-archive`)** + +客商风险数据每天 02:30 自动刷新(合同期限内生效合同的甲乙方关联客商),供客商档案展示、筛选,以及合同/付款环节风险管控使用。 + +![客商档案列表:展开搜索后可见“客商风险”下拉筛选,列表客商风险列为等级+黑名单组合标签](./2026-10-23-HCS合同管理操作手册(纯操作版).assets/16-客商档案列表-客商风险.png) + +**查看规则:** + +| 客商情况 | 显示 | 颜色 | +|---|---|---| +| 等级 + 黑名单命中(组合展示,等级在前) | 高风险、黑名单 / 中风险、黑名单 / 低风险、黑名单 | 红 | +| 仅黑名单命中(无等级数据) | 黑名单 | 红 | +| 仅高风险 | 高风险 | 红 | +| 中风险 | 中风险 | 黄 | +| 低风险 | 低风险 | 绿 | +| 内部组织客商 | 无风险(默认,不查询) | 绿 | +| 尚未查询到数据 | - | — | + +**操作:** + +1. 筛选:搜索栏点 **展开**,“客商风险”下拉选黑名单/高风险/中风险/低风险后点 **查询**。 +2. 查明细:红色标签 **鼠标悬浮**,展示失信类别、列入事由、风险等级。 +3. 当天新增客商:在新增/编辑客商表单填写客商名称后失焦,系统实时查询并在名称下方显示红色风险提示(悬浮见明细);列表落库数据次日刷新后生效。 + +![客商风险悬浮明细:被列入外部黑名单(失信类别、列入事由)与风险等级](./2026-10-23-HCS合同管理操作手册(纯操作版).assets/17-客商风险悬浮明细.png) + +> 合同列表“客商风险”列与本页同口径,实时关联客商档案组装(见 3.5 节截图)。数据未刷新或金蝶黑名单类别/事由缺失时只显示等级段,等待次日刷新或联系运维执行刷新。 + --- ## 4. 定时任务与接口(运维视角) -HCS 推送由五个独立定时任务和一组手动接口组成。五个任务各有自己的开关与 cron,互不影响;定时任务、手动接口和审批即时推送共用同一套推送逻辑。 +HCS 推送由五个推送定时任务、一个客商风险每日刷新任务和一组手动接口组成。五个推送任务各有自己的开关与 cron,互不影响;定时任务、手动接口和审批即时推送共用同一套推送逻辑。客商风险刷新任务独立于推送框架(不抢推送锁、不走合规状态回写),详见 4.8。 -### 4.1 五类定时任务总览 +### 4.1 定时任务总览 | 任务 | bizType | 扫描对象 | 推送链路(HCS 接口) | 独立开关 | 默认 cron | 手动触发 | |---|---|---|---|---|---|---| | 项目立项 C102 | `project` | 项目立项:审批通过/变更审批通过,待推送/失败未超限 | C102-3 判重 → C102-1 新建 / C102-2 更新 | `push.project.sync-enabled`(默认 `false`) | `0 */5 * * * ?` | `POST /hcs/sync/project/{id}`、`/hcs/sync/project`、`/hcs/sync/project/batch` | -| 合同 C103 | `contract` | 合同:审批通过/变更审批通过,待推送/失败未超限;同轮补查特批结果 | C101-1 风险 → C103-2 附件 → C103-3 判重 → C103-1 起草 / C103-11 更新 → C103-9 履约规则(开关)→ 高风险挂特批 | `push.contract.sync-enabled`(默认 `false`) | `0 1/5 * * * ?` | `POST /hcs/sync/contract/{id}`、`/hcs/sync/contract`、`/hcs/sync/contract/batch` | -| 付款链 C106/C108 | `payment` | 付款申请:审批通过,待推送/失败未超限 | C106-3/C106-4 特批(`hcs_spec_status=need` 时)→ C103-9 履约规则重推 → C108-1/C108-2 履约记录(开关) | `push.payment.sync-enabled`(默认 `false`) | `0 2/5 * * * ?` | `POST /hcs/sync/payment/{id}`、`/hcs/sync/payment` | +| 合同 C103 | `contract` | 合同:审批通过/变更审批通过,待推送/失败未超限;同轮补查特批结果 | C101-1 风险 → C103-2 附件 → C103-3 判重 → C103-1 起草 / C103-11 更新(含业务合同号、归档状态)→ C103-9 履约规则(开关)→ 高风险挂特批 | `push.contract.sync-enabled`(默认 `false`) | `0 1/5 * * * ?` | `POST /hcs/sync/contract/{id}`、`/hcs/sync/contract`、`/hcs/sync/contract/batch` | +| 付款链 C106/C108 | `payment` | 付款申请:审批通过,待推送/失败未超限 | C106-3/C106-4 特批(`hcs_spec_status=need` 时)→ 金蝶实际付款门禁 → C103-9 履约规则重推(开关)→ C108-1/C108-2 履约记录(开关) | `push.payment.sync-enabled`(默认 `false`) | `0 2/5 * * * ?` | `POST /hcs/sync/payment/{id}`、`/hcs/sync/payment` | | 开票登记 C111 | `invoice-application` | 开票申请:审批通过,待推送/失败未超限 | C111-1 判重 → C111-2 新增 → C111-3 更新(金蝶已开票/已有ID) | `push.invoice-application.sync-enabled`(默认 `false`) | `0 3/5 * * * ?` | `POST /hcs/sync/invoice-application/{id}`、`/hcs/sync/invoice-application` | | 收票单 C109 | `invoice-receipt` | 收票单:审批通过,待推送/失败未超限 | C109-1 新建 / C109-2 更新 | `push.invoice-receipt.sync-enabled`(默认 `false`) | `0 4/5 * * * ?` | `POST /hcs/sync/invoice-receipt/{id}`、`/hcs/sync/invoice-receipt` | +| 客商风险刷新(C101-1 + A801-5) | — | 合同期限内生效合同的甲乙方关联客商 | 逐个 C101-1 风险等级 + 批量 A801-5 金蝶黑名单,落库客商档案 | `risk.refresh-enabled`(默认 `false`) | `0 30 2 * * ?` | 无手动接口(可临时改 cron 或调 `refreshCustomerRiskForActiveContracts()`) | -> - **即时推送**:项目立项、合同审批通过后立即推送(受 `push.immediate-enabled` 控制);付款申请审批通过后立即发起特批/推送履约数据(**不受**该开关控制);开票登记、收票单没有即时推送,只能靠定时扫描或手动接口。 -> - 五个扫描开关默认全部 `false`,按业务灰度打开;手动接口不受开关限制。 -> - 只有合同扫描任务额外承担“特批结果兜底查询”,其他任务没有这个动作。 +> - **即时推送**:项目立项、合同审批通过后立即推送(受 `push.immediate-enabled` 控制);付款申请审批通过后立即发起付款特批/推送合规(**不受**该开关控制,但履约计划与履约记录要等金蝶实际付款后才推送);开票登记、收票单没有即时推送,只能靠定时扫描或手动接口。 +> - 五个推送扫描开关默认全部 `false`,按业务灰度打开;客商风险刷新开关 `risk.refresh-enabled` 同样默认 `false`(上线需置 `true`);手动接口不受推送开关限制。 +> - 只有合同扫描任务额外承担“特批结果兜底查询”,其他推送任务没有这个动作。 ### 4.2 公共执行框架 @@ -592,10 +650,10 @@ HCS 推送由五个独立定时任务和一组手动接口组成。五个任务 **推送逻辑**: 1. **特批挂起门禁**:`hcs_spec_status=processing` 的合同跳过推送,等待特批结果;手动重推直接报“合同特批审核中,禁止重推”。 -2. **客商风险 C101-1**(`contract.require-risk-check=true`):甲乙双方分别查询;客商档案匹配不到或为内部组织时跳过该方,不阻断;任一方高风险或黑名单只做标记,**不阻断推送**,推送成功后自动转特批。 +2. **客商风险 C101-1**(`contract.require-risk-check=true`):甲乙双方分别查询;客商档案匹配不到或为内部组织时跳过该方,不阻断;任一方高风险(C101-1 评级)或金蝶黑名单命中(A801-5,`HcsCreditCheckSupport` 公共判定)只做标记,**不阻断推送**,推送成功后自动转特批。 3. **附件转传 C103-2**(`attachment.transfer-enabled=true`):附件转传失败会导致整次推送失败。 -4. **首次推送**:无合规合同ID时先调 C103-3 判重(`fdNumber=合同编号`);命中直接回填已有ID并置已推送,未命中调 C103-1 起草;起草成功后立即回填合规合同ID,再继续后续步骤。 -5. **变更推送**:已有合规合同ID时调 C103-11 更新。 +4. **首次推送**:无合规合同ID时先调 C103-3 判重(`fdNumber=合同编号`);命中直接回填已有ID并置已推送,未命中调 C103-1 起草;起草成功后立即回填合规合同ID,再继续后续步骤。报文含业务合同号(`fdBusContractNumber`,取值同合同编号)与归档状态(`fdArchiveStatus`,已上传双章归档文件时传 `toarchive` 待归档)。 +5. **变更推送**:已有合规合同ID时调 C103-11 更新,报文同样携带业务合同号与归档状态;已推送合同上传双章归档文件后也会即时单独触发一次 C103-11 归档状态更新(见 3.9 节)。 6. **履约规则 C103-9**(`exe-stage.sync-enabled=true`):按履约计划行级新增/更新/删除;无可同步行时返回“履约规则无待同步行”;同步失败会把整条合同置 `failed`。由于第 4 步已先回填合规ID,下一轮重推会走 C103-11 更新,不会重复起草。 7. **高风险挂起特批**:风险命中时置 `hcs_spec_status=processing`、当前节点“特批审核”,等待 HCS 特批结果。 @@ -628,9 +686,10 @@ HCS 推送由五个独立定时任务和一组手动接口组成。五个任务 **推送逻辑**: 1. `hcs_spec_status=need`:无特批流程ID(`hcs_spec_fd_id`)时调 C106-3 发起特批,已有特批流程ID时调 C106-4 更新。C106-3 依赖 `spec-pay.template-id` 与关联合同的合规合同ID;成功后回填特批流程ID、置 `processing`、当前节点“特批审核”。 -2. 关联合同已推送合规(合同 `hcs_fd_id` 非空)时,调 C103-9 重推履约规则;无待同步行自动跳过。该动作**不受** `exe-stage.sync-enabled` 控制,与合同任务不同。 -3. `perf-record.sync-enabled=false`:直接置整体 `synced` 结束;`true`:无履约记录ID调 C108-1 新建,已有调 C108-2 更新。 -4. 整体合规状态 `synced` 只代表本轮推送完成,**不代表特批已通过**;特批中(`processing`)的付款也可能显示已推送。 +2. **金蝶实际付款门禁**:付款审批通过只做本地计划金额累加,`paid_amount` 为空或 ≤0 时履约计划与履约记录均不推送,整体照常回写 `synced`,结果说明为“金蝶未实际付款,待付款后推送履约计划与履约记录”;金蝶付款回写推进后会重新置待同步,由扫描任务自动触发再次推送。 +3. 关联合同已推送合规(合同 `hcs_fd_id` 非空)且通过实付门禁时,调 C103-9 重推履约规则;无待同步行自动跳过。该动作受 `exe-stage.sync-enabled` 控制,与合同链、收款链一致。 +4. `perf-record.sync-enabled=false`:直接置整体 `synced` 结束;`true`:无履约记录ID调 C108-1 新建,已有调 C108-2 更新。 +5. 整体合规状态 `synced` 只代表本轮推送完成,**不代表特批已通过、也不代表已实际付款**;特批中(`processing`)或未实付的付款也可能显示已推送。 **特批结果**:HCS 通过 `paymentLegal/result` 回调,或用 `/hcs/spec-pay/confirm` 人工确认。通过后恢复“审批通过”并重新触发金蝶自动同步;驳回后付款审批状态置 `returned`(已驳回),需修改后重新提交审批、重新发起特批。 @@ -686,7 +745,22 @@ HCS 推送由五个独立定时任务和一组手动接口组成。五个任务 | `POST /hcs/sync/invoice-receipt/{id}` | 单条重推 | 仅 `approved` 可推;先重置 `unsynced`、失败次数清零 | | `POST /hcs/sync/invoice-receipt` | 按扫描逻辑推一轮 | 不检查开关、不抢任务锁;**无批量 ID 接口** | -### 4.8 配置项 +### 4.8 客商风险每日刷新任务(C101-1 + A801-5) + +**任务类**:`CustomerRiskRefreshTask`;**开关**:`risk.refresh-enabled`(默认 `false`,上线需置 `true`);**调度**:`risk.refresh-cron`(默认 `0 30 2 * * ?`,每天 02:30,错开金蝶客商同步 02:00)。 + +**刷新范围**:合同期限内(起止日期或临时期限覆盖当天)且审核通过、未终止的合同,其甲乙方关联客商;内部组织客商跳过(默认无风险)。 + +**执行逻辑**: + +1. 一次 SQL 取期限内生效合同的甲乙方名称,按名称反查客商档案。 +2. 外部客商按 **200/批**:每批调一次 A801-5 金蝶黑名单批量查询 + 逐个调 C101-1 风险等级查询(该接口仅支持单编号)。 +3. 批内结果立即落库 `blade_customer_archive`(`hcs_risk_level`、`kingdee_black_status`、`hcs_risk_time`、`kingdee_black_category`、`kingdee_black_reason`)并留痕 `blade_hcs_check_log`;**批间互不影响,单批失败不中断其余批次**。 +4. 该任务不走推送框架:不抢推送任务锁、不改单据合规状态、无手动触发接口;临时补刷可把 `refresh-cron` 改为近时刻执行后改回,或调用 `HcsCreditCheckSupport#refreshCustomerRiskForActiveContracts()`。 + +**结果用途**:客商档案/合同列表“客商风险”列展示与筛选、红色标签悬浮明细、合同推送与付款提交的风险判定数据源。查询逐次留痕,日志关键字“定时刷新客商风险”。 + +### 4.9 配置项 > 下表键名省略公共前缀 `thirdParty.hcs.`。例如 `push.immediate-enabled` 的完整键为 `thirdParty.hcs.push.immediate-enabled`。 @@ -708,7 +782,7 @@ HCS 推送由五个独立定时任务和一组手动接口组成。五个任务 | `attachment.file-download-base` | 空 | TMS 文件下载基址 | 有附件时转传失败 | | `master-data.ttl-minutes` | `120` | 模板/分类/币种缓存时长 | 主数据变更最长延迟 120 分钟生效 | -**五个任务开关与 cron** +**五个推送任务与风险刷新开关、cron** | 任务 | 开关(默认) | cron 键 | 默认 cron | |---|---|---|---| @@ -717,16 +791,17 @@ HCS 推送由五个独立定时任务和一组手动接口组成。五个任务 | 付款链 | `push.payment.sync-enabled`(`false`) | `push.payment.cron` | `0 2/5 * * * ?` | | 开票登记 | `push.invoice-application.sync-enabled`(`false`) | `push.invoice-application.cron` | `0 3/5 * * * ?` | | 收票单 | `push.invoice-receipt.sync-enabled`(`false`) | `push.invoice-receipt.cron` | `0 4/5 * * * ?` | +| 客商风险刷新 | `risk.refresh-enabled`(`false`,上线置 `true`) | `risk.refresh-cron` | `0 30 2 * * ?` | -> 五个开关默认全关,按业务灰度打开;cron 不配置时使用代码默认值(每 5 分钟,分钟位 0–4 错峰)。 +> 五个推送开关默认全关,按业务灰度打开;客商风险刷新上线需单独开启。推送 cron 不配置时使用代码默认值(每 5 分钟,分钟位 0–4 错峰)。 **各任务业务配置** | 配置键 | 默认/样例 | 影响任务 | 说明 | |---|---|---|---| | `project.template-code` | `template_15x0og` | 项目立项 C102-1 | 未配置时推送失败 | -| `contract.template-mapping` | 样例仅 `[承运商合同]` | 合同 C103-1 | 合同类别未映射必失败;中文键必须写成 `"[承运商合同]"` | -| `contract.currency-name-mapping` | `RMB→人民币`(另一环境样例 `CNY`) | 合同 C103-1 | 币种传错或为空;上线前与合规方确认取值 | +| `contract.template-mapping` | 样例含 `[承运商合同]`、`[客户合同]` | 合同 C103-1 | 合同类别未映射必失败;中文键必须写成 `"[承运商合同]"` | +| `contract.currency-name-mapping` | `RMB`/`CNY`→人民币,`USD`→美元 | 合同 C103-1 | 币种传错或为空;上线前与合规方确认取值 | | `contract.require-risk-check` | `true` | 合同 C101-1 | 关闭后不做风险校验,高风险合同不会触发特批 | | `exe-stage.sync-enabled` | `false` | 合同 C103-9 | 关闭时合同任务不推履约规则 | | `exe-stage.fund-type` | `byAmount` | 合同 C103-9 | 金额类型传错会被合规校验拒绝 | @@ -745,20 +820,22 @@ HCS 推送由五个独立定时任务和一组手动接口组成。五个任务 > 不同环境样例中的币种名称映射可能不同(`CNY` 与 `人民币`),上线前必须与合规方确认 HCS 主数据实际取值。 -### 4.9 运维注意 +### 4.10 运维注意 -- **开关灰度**:五个扫描开关默认全关且互相独立;项目立项/合同的即时推送受 `push.immediate-enabled` 控制,付款审批即时特批不受该开关控制;手动接口始终可用,不受开关限制。 +- **开关灰度**:五个推送扫描开关默认全关且互相独立;项目立项/合同的即时推送受 `push.immediate-enabled` 控制,付款审批即时特批不受该开关控制;客商风险刷新开关 `risk.refresh-enabled` 上线需单独置 `true`;手动接口始终可用,不受推送开关限制。 - **先看存量再开扫描**:扫描不区分“从未推送”和“推送失败”,开关一开,所有审批通过且未同步的历史单据都会进入扫描范围(每轮最多 50 条)。上线前先确认待推送数据量。 - **修复根因优先**:失败单据每 5 分钟自动重试,达到 `push.max-retry`(默认 5)后停止;人工重推会清零次数,但不修复配置/数据会反复失败。 - **付款依赖合同**:C106-3 要求关联合同已有合规合同ID,C103-9 也以合同 `hcs_fd_id` 为前提;合同未推送合规时付款特批/履约规则相关步骤会失败或跳过。 +- **付款履约数据看实付**:付款链履约计划/履约记录以金蝶实际付款为门禁,未实付时整体状态照常回写 `synced`,不要把“金蝶未实际付款,待付款后推送…”当失败处理。 +- **客商风险刷新依赖合同范围**:刷新范围是合同期限内生效合同的关联客商,期限外客商风险列显示“-”属正常;金蝶黑名单类别/事由来自 A801-5,缺失时列表只显示等级段。客商黑名单与风险等级专项说明见《2026-10-30-客商黑名单与风险等级运维手册(运维侧)》(doc/docs/业务流程/)。 - **开票必配币种映射**:`invoice.currency-id-mapping` 的 `RMB` 必须配置为 HCS 币种 fdId,否则开票登记每轮都会失败。 - **收票合同关联**:回溯不到合同且未人工补录 `hcs_contract_id` / `hcs_contract_no` 时不会失败,但合同字段为空;需要完整合同信息时先补录再重推。 -- **模板映射逐个类别确认**:样例只配了承运商合同;客户合同、补充协议未配置时推送必失败。Nacos YAML 中文键必须写成 `"[承运商合同]"`。 +- **模板映射逐个类别确认**:样例只配了承运商合同、客户合同;补充协议未配置时推送必失败。Nacos YAML 中文键必须写成 `"[承运商合同]"`。 - **附件下载基址必配**:合同有附件时,`attachment.file-download-base` 为空会导致附件转传失败。 - **生产禁用 Mock**:`thirdParty.hcs.mock-enabled` 生产环境必须为 `false`。 - **主数据刷新**:模板映射调整后,可调用 `POST /hcs/master-data/refresh` 刷新模板/分类/币种缓存并校验映射。 - **多实例与手动接口**:定时任务按 bizType 抢 Redis 锁(TTL 240 秒),抢不到本轮跳过;手动接口不抢锁,避免与定时任务同一时刻执行。 -- **日志排查**:服务日志按单据编号、合规ID、`hcs` 关键字检索;本地日志目录为 `F:\home\tms\blade-transport`。 +- **日志排查**:服务日志按单据编号、合规ID、`hcs` 关键字检索;客商风险刷新日志按“定时刷新客商风险”检索;本地日志目录为 `F:\home\tms\blade-transport`。 --- @@ -775,7 +852,7 @@ HCS 推送由五个独立定时任务和一组手动接口组成。五个任务 | 提示“合同类别 [xxx] 未配置合规模板ID” | Nacos 模板映射缺该类合同 | 运维补齐 `contract.template-mapping` 后重推 | | 提示附件转传失败 | 下载基址为空、附件不可下载、文件超限或 HCS 附件接口异常 | 检查 `attachment.file-download-base` 和附件,修正后重推 | | 提示 C101-1 客商风险查询失败 | HCS 风险接口异常/无响应 | 联系 HCS 侧确认接口,恢复后重推 | -| 高风险客商没有触发特批 | 客商名称与档案不匹配导致风险校验被跳过,或 HCS 返回的风险等级不是高风险/黑名单 | 核对甲乙方名称与客商档案;在 HCS 侧确认客商风险等级和黑名单状态 | +| 高风险客商没有触发特批 | 客商名称与档案不匹配导致风险校验被跳过,或风险等级不是高风险且未命中金蝶黑名单 | 核对甲乙方名称与客商档案;黑名单以金蝶黑名单命中(A801-5)为准,核对客商档案金蝶黑名单字段与 HCS 风险等级 | | 提示 C103-1/C103-11 合同发起/更新失败 | HCS 业务校验不通过 | 按返回 msg 修正模板、字段或主数据后重推 | | 特批状态一直是“待特批” | 存量数据或推送未成功,未进入特批流程 | 联系管理员重新推送;推送成功后会自动转“特批中” | | 特批中想重推 | 特批流程未结束 | 不允许重推,等待 HCS 回传或人工确认结果 | @@ -799,6 +876,9 @@ HCS 推送由五个独立定时任务和一组手动接口组成。五个任务 | 付款审批提示“已发起付款特批”但迟迟没有金蝶单据 | 特批仍在审批,特批通过后才同步金蝶 | 等待特批结果;长时间无结果联系实施人工确认 | | 付款特批发起失败(模板未配/合同未推送) | `spec-pay.template-id` 为空或关联合同无合规ID | 运维修复后重推;不要重复审批 | | 付款特批中能否重复审批 | 特批中审核状态已是“审批通过” | 不需要也不能重复审批;等待 HCS 结果 | +| 客商档案/合同列表“客商风险”列显示“-” | 客商不在合同期限内生效合同关联范围,或档案刚创建未被每日刷新任务查询过 | 等次日 02:30 刷新;当天可在客商表单按名称实时查询风险提示 | +| 客商命中金蝶黑名单但列表没有“黑名单” | 黑名单展示依赖金蝶记录的失信类别/列入事由(每日刷新落库),数据缺失时只显示等级段 | 等待次日刷新或联系运维执行刷新;确认 `risk.refresh-enabled` 已开启 | +| 付款特批通过后合规里没有履约计划/履约记录 | 付款链履约数据以金蝶实际付款为门禁,未实付不推送 | 正常等待金蝶付款回写后自动推送;整体状态显示“金蝶未实际付款,待付款后推送履约计划与履约记录”属正常 | --- @@ -825,6 +905,8 @@ HCS 推送由五个独立定时任务和一组手动接口组成。五个任务 | 付款申请表单 | 附件类型“特批附件” | 高风险/黑名单收款方提交前必传 | | 付款申请列表 | 行内“通过” | 审批通过后自动发起付款特批 | | 付款申请列表 | “当前节点”列 | 查看“特批审核/审批通过/已驳回”判断特批进度 | +| 客商档案列表 | 搜索栏“客商风险”筛选 | 按黑名单/高风险/中风险/低风险筛选客商(数据每日 02:30 刷新) | +| 客商档案列表 | 客商风险列红色标签悬浮 | 查看失信类别、列入事由、风险等级明细 | | 运维专用 | `POST /hcs/sync/project/{id}` | 单条重推项目立项 | | 运维专用 | `POST /hcs/sync/project/batch` | 按勾选ID批量重推项目立项 | | 运维专用 | `POST /hcs/spec-pay/confirm` | 人工确认付款特批结果(参数 `id`、`result`;页面无按钮) | @@ -838,4 +920,4 @@ HCS 推送由五个独立定时任务和一组手动接口组成。五个任务 --- -> 本文基于 2026-10-23 代码与页面现状整理;按钮、提示和环境开关以实际页面为准。临时合同推送、旧版“发起特批”入口、变更页“终止合同”未开放属于当前版本现状,后续版本调整时请同步核对。 +> 本文基于 2026-10-23 代码与页面现状整理,2026-11-01 按最新实现更新(客商风险口径、归档即时同步、付款履约实付门禁、客商风险刷新任务);按钮、提示和环境开关以实际页面为准。临时合同推送、旧版“发起特批”入口、变更页“终止合同”未开放属于当前版本现状,后续版本调整时请同步核对。 diff --git a/doc/docs/hcs合同/images/01-合同列表.png b/doc/docs/hcs合同/images/01-合同列表.png index 7553fe86..326c01e0 100644 Binary files a/doc/docs/hcs合同/images/01-合同列表.png and b/doc/docs/hcs合同/images/01-合同列表.png differ diff --git a/doc/docs/hcs合同/images/16-客商档案列表-客商风险.png b/doc/docs/hcs合同/images/16-客商档案列表-客商风险.png new file mode 100644 index 00000000..0ee0f7b8 Binary files /dev/null and b/doc/docs/hcs合同/images/16-客商档案列表-客商风险.png differ diff --git a/doc/docs/hcs合同/images/17-客商风险悬浮明细.png b/doc/docs/hcs合同/images/17-客商风险悬浮明细.png new file mode 100644 index 00000000..7273b7e3 Binary files /dev/null and b/doc/docs/hcs合同/images/17-客商风险悬浮明细.png differ diff --git a/doc/docs/hcs合同/images/18-合同列表-客商风险悬浮.png b/doc/docs/hcs合同/images/18-合同列表-客商风险悬浮.png new file mode 100644 index 00000000..9c631e7d Binary files /dev/null and b/doc/docs/hcs合同/images/18-合同列表-客商风险悬浮.png differ diff --git a/doc/docs/hcs合同/images/19-项目立项列表.png b/doc/docs/hcs合同/images/19-项目立项列表.png new file mode 100644 index 00000000..12ef7911 Binary files /dev/null and b/doc/docs/hcs合同/images/19-项目立项列表.png differ diff --git a/doc/docs/hcs合同/images/20-付款申请列表.png b/doc/docs/hcs合同/images/20-付款申请列表.png new file mode 100644 index 00000000..9cf1a6da Binary files /dev/null and b/doc/docs/hcs合同/images/20-付款申请列表.png differ diff --git a/doc/docs/业务流程/2026-10-14-金蝶(星瀚)BFI全链路梳理(前端-后端-定时任务·业务域汇总与操作手册).assets/bfi-effect-a301-2-公告同步对照.png b/doc/docs/业务流程/2026-10-14-金蝶(星瀚)BFI全链路梳理(前端-后端-定时任务·业务域汇总与操作手册).assets/bfi-effect-a301-2-公告同步对照.png deleted file mode 100644 index 4b16d08d..00000000 Binary files a/doc/docs/业务流程/2026-10-14-金蝶(星瀚)BFI全链路梳理(前端-后端-定时任务·业务域汇总与操作手册).assets/bfi-effect-a301-2-公告同步对照.png and /dev/null differ diff --git a/doc/docs/业务流程/2026-10-14-金蝶(星瀚)BFI全链路梳理(前端-后端-定时任务·业务域汇总与操作手册).assets/bfi-effect-a301-3-认领推送对照.png b/doc/docs/业务流程/2026-10-14-金蝶(星瀚)BFI全链路梳理(前端-后端-定时任务·业务域汇总与操作手册).assets/bfi-effect-a301-3-认领推送对照.png deleted file mode 100644 index 2ecd50fa..00000000 Binary files a/doc/docs/业务流程/2026-10-14-金蝶(星瀚)BFI全链路梳理(前端-后端-定时任务·业务域汇总与操作手册).assets/bfi-effect-a301-3-认领推送对照.png and /dev/null differ diff --git a/doc/docs/业务流程/2026-10-14-金蝶(星瀚)BFI全链路梳理(前端-后端-定时任务·业务域汇总与操作手册).assets/bfi-effect-a301-3-金蝶认领结果.png b/doc/docs/业务流程/2026-10-14-金蝶(星瀚)BFI全链路梳理(前端-后端-定时任务·业务域汇总与操作手册).assets/bfi-effect-a301-3-金蝶认领结果.png deleted file mode 100644 index 022c9a96..00000000 Binary files a/doc/docs/业务流程/2026-10-14-金蝶(星瀚)BFI全链路梳理(前端-后端-定时任务·业务域汇总与操作手册).assets/bfi-effect-a301-3-金蝶认领结果.png and /dev/null differ diff --git a/doc/docs/业务流程/2026-10-14-金蝶(星瀚)BFI全链路梳理(前端-后端-定时任务·业务域汇总与操作手册).assets/tms-a301-1-收款流水列表.png b/doc/docs/业务流程/2026-10-14-金蝶(星瀚)BFI全链路梳理(前端-后端-定时任务·业务域汇总与操作手册).assets/tms-a301-1-收款流水列表.png new file mode 100644 index 00000000..c663dc22 Binary files /dev/null and b/doc/docs/业务流程/2026-10-14-金蝶(星瀚)BFI全链路梳理(前端-后端-定时任务·业务域汇总与操作手册).assets/tms-a301-1-收款流水列表.png differ diff --git a/doc/docs/业务流程/2026-10-14-金蝶(星瀚)BFI全链路梳理(前端-后端-定时任务·业务域汇总与操作手册).assets/tms-先票后款-付款结算单发票登记.png b/doc/docs/业务流程/2026-10-14-金蝶(星瀚)BFI全链路梳理(前端-后端-定时任务·业务域汇总与操作手册).assets/tms-先票后款-付款结算单发票登记.png new file mode 100644 index 00000000..b5402258 Binary files /dev/null and b/doc/docs/业务流程/2026-10-14-金蝶(星瀚)BFI全链路梳理(前端-后端-定时任务·业务域汇总与操作手册).assets/tms-先票后款-付款结算单发票登记.png differ diff --git a/doc/docs/业务流程/2026-10-14-金蝶(星瀚)BFI全链路梳理(前端-后端-定时任务·业务域汇总与操作手册).assets/tms-先票后款-收票登记列表.png b/doc/docs/业务流程/2026-10-14-金蝶(星瀚)BFI全链路梳理(前端-后端-定时任务·业务域汇总与操作手册).assets/tms-先票后款-收票登记列表.png new file mode 100644 index 00000000..740654da Binary files /dev/null and b/doc/docs/业务流程/2026-10-14-金蝶(星瀚)BFI全链路梳理(前端-后端-定时任务·业务域汇总与操作手册).assets/tms-先票后款-收票登记列表.png differ diff --git a/doc/docs/业务流程/2026-10-14-金蝶(星瀚)BFI全链路梳理(前端-后端-定时任务·业务域汇总与操作手册).assets/tms-先票后款-收票登记表单.png b/doc/docs/业务流程/2026-10-14-金蝶(星瀚)BFI全链路梳理(前端-后端-定时任务·业务域汇总与操作手册).assets/tms-先票后款-收票登记表单.png new file mode 100644 index 00000000..dba51689 Binary files /dev/null and b/doc/docs/业务流程/2026-10-14-金蝶(星瀚)BFI全链路梳理(前端-后端-定时任务·业务域汇总与操作手册).assets/tms-先票后款-收票登记表单.png differ diff --git a/doc/docs/业务流程/2026-10-14-金蝶(星瀚)BFI全链路梳理(前端-后端-定时任务·业务域汇总与操作手册).assets/tms-客商供应商编码.png b/doc/docs/业务流程/2026-10-14-金蝶(星瀚)BFI全链路梳理(前端-后端-定时任务·业务域汇总与操作手册).assets/tms-客商供应商编码.png new file mode 100644 index 00000000..c231ebf1 Binary files /dev/null and b/doc/docs/业务流程/2026-10-14-金蝶(星瀚)BFI全链路梳理(前端-后端-定时任务·业务域汇总与操作手册).assets/tms-客商供应商编码.png differ diff --git a/doc/docs/业务流程/2026-10-14-金蝶(星瀚)BFI全链路梳理(前端-后端-定时任务·业务域汇总与操作手册).assets/tms-认领记录页.png b/doc/docs/业务流程/2026-10-14-金蝶(星瀚)BFI全链路梳理(前端-后端-定时任务·业务域汇总与操作手册).assets/tms-认领记录页.png new file mode 100644 index 00000000..1b51c52e Binary files /dev/null and b/doc/docs/业务流程/2026-10-14-金蝶(星瀚)BFI全链路梳理(前端-后端-定时任务·业务域汇总与操作手册).assets/tms-认领记录页.png differ diff --git a/doc/docs/业务流程/2026-10-14-金蝶(星瀚)BFI全链路梳理(前端-后端-定时任务·业务域汇总与操作手册).md b/doc/docs/业务流程/2026-10-14-金蝶(星瀚)BFI全链路梳理(前端-后端-定时任务·业务域汇总与操作手册).md index 6eeac324..72a7b3e6 100644 --- a/doc/docs/业务流程/2026-10-14-金蝶(星瀚)BFI全链路梳理(前端-后端-定时任务·业务域汇总与操作手册).md +++ b/doc/docs/业务流程/2026-10-14-金蝶(星瀚)BFI全链路梳理(前端-后端-定时任务·业务域汇总与操作手册).md @@ -1,23 +1,30 @@ # 金蝶(星瀚)BFI 全链路梳理:前端入口 → 后端链路 → 定时任务(业务域汇总 + 操作手册) -> 整理日期:2026-10-14 -> 代码基线:`tms-api` 当前 master(`805ca9d3`,2026-10-13),全量按当前代码重新核实(上一版总纲基线为 09-24 `50573e7`)。 +> 整理日期:2026-10-14(2026-10-31 修订) +> 代码基线:`tms-api` 分支 feature/20261031-bfi-收票收款改造(`933fb37d`,2026-10-31)。 > 本文替代《2026-09-27-金蝶(星瀚)BFI接口调用全链路梳理(按业务域汇总·更新版)》作为新总纲: > - **组织方式改为三层**:每个业务域按「前端入口(页面/按钮)→ 后端链路(触发/顺序/口径/落库)→ 定时任务」梳理; -> - 新增**操作手册**(第 11 节):按业务时序给出端到端操作步骤与异常处理操作; -> - 自 09-27 以来代码有 40+ 提交(09-28~10-13),**收票回写缺陷已修、调试残留已清、新增锁单/解锁、审批自动同步、failed 重推、票池镜像读回切、全票池定时同步、A801-5 黑名单、A801-2/A902 附件接线、收款认领重构**等,接口清单、分录口径、状态机均有实质变化,全文按当前代码重写。 +> - 新增**操作手册**(第 11 节):按业务时序给出端到端操作步骤与异常处理操作。 + +## 修改记录 + +| 日期 | 代码基线 | 修订内容 | +|---|---|---| +| 2026-10-14 | master `805ca9d3`(10-13) | 初版总纲,替代 09-27 版;全量按代码重写 | +| 2026-10-15 | master | 增补(更新要点、入参映射表交叉引用等) | +| 2026-10-31 | feature/20261031-bfi-收票收款改造 `933fb37d`(10-31) | ① **第 5 节收票域整体重写**:收票登记改「先票后款」两段式(收票登记只推 A303-2 终态 pushed、A302-6 指定应付移到付款结算单侧,A303-2 报文按发票明细行组装、往来户=销方供应商代码、结算组织页面手选、币别固定 CNY);② **第 6 节收款认领整体重写**:数据源 A301-2→A301-1,A301-2/A301-3 推送链路与定时任务整体下线,认领只登记本地并推送合同合规(C103-9/C107-1/C108-1,作废推 C107-3);③ **2 节客商域**:新增 A901-1 供应商编码查询落库、A905-3 行名行号查询(收款账户联行号带出/保存校验);④ **3 节付款域**:金蝶实际付款(A103-2 paid 回写)后才推送 HCS 付款履约计划/履约记录;⑤ 1.2 接口清单、1.3 防重键、8 全景、9 状态机、10 定时任务、11 操作手册、12 风险清单同步更新;⑥ 补充 TMS 侧新页面截图(收票登记先票后款表单、付款结算单发票登记、收款流水 A301-1 列表、认领记录、客商供应商编码),下线 A301-2/A301-3 金蝶侧旧效果截图 | --- ## 目录 -- [0. 本次更新要点(09-27 → 10-14)](#0-本次更新要点09-27--10-14) +- [0. 本次更新要点(10-14 → 10-31 与 09-27 → 10-14)](#0-本次更新要点10-14--10-31-与-09-27--10-14) - [1. 集成总览与公共机制](#1-集成总览与公共机制) -- [2. 业务域一:客商档案(A902-1 / A902-2 / A801-5 黑名单)](#2-业务域一客商档案a902-1--a902-2--a801-5-黑名单) +- [2. 业务域一:客商档案(A902-1 / A902-2 / A901-1 供应商 / A905-3 行名行号 / A801-5 黑名单)](#2-业务域一客商档案a902-1--a902-2--a901-1-供应商--a905-3-行名行号--a801-5-黑名单) - [3. 业务域二:财务应付与付款(A102-2 / A102-4 / A103-1 / A103-3 / A103-2 / A103-4)](#3-业务域二财务应付与付款) - [4. 业务域三:财务应收与开票(A302-2 / A302-4 / A303-4 / A303-1 / A303-5)](#4-业务域三财务应收与开票) -- [5. 业务域四:收票与金蝶票池(A801-1 / A303-2 / A302-6 / A801-3 / A801-2 / A902 附件)](#5-业务域四收票与金蝶票池) -- [6. 业务域五:收款认领(A301-2 / A301-3 / A301-1)](#6-业务域五收款认领a301-2--a301-3--a301-1) +- [5. 业务域四:收票与金蝶票池(先票后款两段式)](#5-业务域四收票与金蝶票池先票后款两段式) +- [6. 业务域五:收款流水与认领(A301-1 数据源 + 合同合规推送)](#6-业务域五收款流水与认领a301-1-数据源--合同合规推送) - [7. 业务域六:公共取数(A801-4 汇率)](#7-业务域六公共取数a801-4-汇率) - [8. 全景:接口依赖与先后顺序](#8-全景接口依赖与先后顺序) - [9. 状态机汇总](#9-状态机汇总) @@ -27,7 +34,22 @@ --- -## 0. 本次更新要点(09-27 → 10-14) +## 0. 本次更新要点(10-14 → 10-31 与 09-27 → 10-14) + +### 0.1 本次更新要点(10-14 → 10-31,本次修订) + +| # | 变化 | 说明 | +|---|---|---| +| 1 | **★★收票登记改「先票后款」两段式(禅道 #15850)** | 收票登记不再关联结算单/合同:从票池选票 → 手选结算组织(承办部门)→ 推 A303-2 生成金蝶收票单即**终态 pushed**(删除 A102-4 应付预检与推送时立即 A302-6);A302-6 指定应付移到**付款结算单侧**——结算单编辑/发票登记认领收票单(候选=审批通过+未认领,全额认领),A102-2 应付单推送成功后逐单指定;A801-3「指定补偿」升级为重试引擎(pushed+已认领→补发 A302-6;未认领→跳过不置 failed)。专项文档:`2026-10-31-先票后款改造方案V2….md` | +| 2 | **★A303-2 报文按先票后款重写** | 主表:往来户=发票销方的金蝶供应商代码(登记时尽力回填、推送前强校验,取自客商档案 `supplier_code`);结算组织=页面手选部门沿部门树上溯平台公司金蝶组织编码;币别固定 CNY。分录:**逐行取票池镜像发票明细**(商品编码=物料/税收分类编码、"V+整数税率"、单价统一 4 位小数、分录候选键=发票号码_seq) | +| 3 | **★全额认领放开匹配合计上限、收票状态按覆盖判定** | 结算单认领收票单不再校验「发票匹配合计≤结算金额」;结算单收票状态按 `已匹配金额 ≥ 结算金额` 判 completed(覆盖即完成,允许超额) | +| 4 | **★★收款认领链路整体下线 A301-3,数据源 A301-2→A301-1** | 收款流水菜单数据源改为 **A301-1 收款单查询**(手动同步,窗口默认 60 天,全公司口径);认领只登记本地(额度上限=收款金额 actrecamt),**不再推送金蝶**;认领事务提交后推合同合规 C103-9→C107-1→C108-1,作废推 C107-3;`ReceiptClaimPushTask` 定时、`/push-kingdee`、`/query-kingdee-result` 端点、A301-2 公告同步全部删除。专项文档:`2026-10-29-收款流水菜单改造….md`、`2026-10-29-收款流水认领操作流程….md` | +| 5 | **★A901-1 供应商查询启用** | `bd_supplier/query` 按统一社会信用代码查金蝶供应商编码:客商新增/金蝶同步复查回填 `supplier_code`,手动端点 `GET /customer-archive/query-supplier-code`;先票后款 A303-2 往来户取值的数据前提 | +| 6 | **★A905-3 行名行号查询启用** | `bd_bebank/query`:客商收款信息开户行失焦按行名精确匹配带出联行号(`GET /customer-archive/query-bank`);保存前按行号反查校验(`GET /customer-archive/query-bank-by-number`,查不到拦截保存) | +| 7 | **★金蝶实际付款后才推 HCS 付款履约计划/记录** | A103-2 回写付款推进(paid 金额变化)时置付款申请 `hcsSyncStatus=unsynced`,由 HCS 推送任务在**金蝶实付门禁**(paidAmount>0)通过后推 C103-9/C108-1(`HcsPaymentSyncSupport`);合同链首推时点不变 | +| 8 | **收票域辅助端点** | `GET /invoice-receipt/invoice-detail-query?invoiceNo=`(表单带出发票明细,镜像缺明细时按票号回源);`GET /invoice-receipt/claim-candidates?keyword=`(付款结算单选收票单候选,审批通过+未被认领,limit 200);`POST /formal-settlement/invoice-assign-kingdee`(发票登记保存后手动触发指定应付) | + +### 0.2 历史更新要点(09-27 → 10-14,初版总纲) | # | 变化 | 说明 | |---|---|---| @@ -66,14 +88,14 @@ iPaaS 网关(/gwzh/BFI,不直连金蝶) 金蝶星瀚 /ierp/kapi/... 反向:金蝶删单 ──▶ POST /blade-transport/kingdee-bfi/unLockNotifyBusiData(免登 skip-url) -定时:blade-transport task/ 包 7 个任务(Spring @Scheduled,全库无 PowerJob/XxlJob 接线) +定时:blade-transport task/ 包金蝶相关任务 5 类(Spring @Scheduled,全库无 PowerJob/XxlJob 接线) ``` - 无 Feign 重试(NEVER_RETRY)、无 fallback;失败抛 `BfiException`,批量循环逐条 catch,单条失败不中断整批。 - 令牌:Redis `blade:third:bfi:token`,TTL=expires_in−120s;`setIfAbsent` 写入(已有缓存不覆盖)。 - 配置前缀两套并存(读同一批 Nacos 键):bfi-api `third-party.bfi.*`(baseUrl/认证/令牌),transport `thirdparty.bfi.*`(业务开关/口径/映射)。样例:`doc/nacos/third-party-api.yaml`。 -### 1.2 接口清单总表(2026-10-14,含在用状态) +### 1.2 接口清单总表(2026-10-31,含在用状态) 来源:`blade-bfi-api/.../feign/IBfiClient.java`(URL 可被 `thirdParty.bfi.*Url` 覆盖)。 @@ -81,35 +103,37 @@ iPaaS 网关(/gwzh/BFI,不直连金蝶) |---|---|---|---| | A100-1 | POST /ierp/kapi/oauth2/getToken | 获取令牌 | ✅ 自动(每次调用前) | | A100-2 / A100-3 | verifyToken / withdrawToken | 校验/撤回令牌 | ❌ 已实现无调用 | -| A102-2 | POST …/awrp/ap/ap_finapbill/save | 财务应付单保存 | ✅ 正式结算单(应付)推送 | -| A102-4 | GET …/awrp/ap/ap_finapbill/query | 财务应付单查询 | ✅ 预检 / 硬校验 / 批量预热 | +| A901-1 | POST …/basedata/bd_supplier/query | 供应商查询 | ✅ **10-28 新增启用**:按信用代码查金蝶供应商编码(客商建档/同步复查回填 supplier_code、手动端点 query-supplier-code) | +| A905-3 | POST …/basedata/bd_bebank/query | 行名行号查询 | ✅ **10-31 新增启用**:客商收款信息开户行带出联行号 + 保存前按行号校验 | +| A102-2 | POST …/awrp/ap/ap_finapbill/save | 财务应付单保存 | ✅ 正式结算单(应付=付款结算单)推送;推送成功后触发本单认领收票单的 A302-6 指定 | +| A102-4 | GET …/awrp/ap/ap_finapbill/query | 财务应付单查询 | ✅ 付款申请硬校验 / 预检 / 批量预热;**收票链路已不再依赖**(先票后款删除 A102-4 预检) | | A103-1 | POST …/awrp/ap/ap/ap_finapbill/push | 应付下推生成付款申请 | ✅ 付款申请推送(单分录) | -| A103-2 | GET …/awrp/ap/ap_payapply/query | 付款申请单查询 | ✅ 推送预检 + 状态回写 + 消失解锁计数 | +| A103-2 | GET …/awrp/ap/ap_payapply/query | 付款申请单查询 | ✅ 推送预检 + 状态回写 + 消失解锁计数;paid 回写后触发 HCS 付款履约计划/记录推送(金蝶实付门禁) | | A103-3 | POST …/awrp/ap/ap/ap_payapply/save | 预付款申请保存 | ✅ 预付款直推 | | A103-4 | GET …/awrp/cas/cas_paybill/query | 付款处理查询 | ✅ 付款记录回写(含 voucher_json) | -| A301-1 | GET …/awrp/cas/cas_recbill/queryRecBill | 收款单查询 | ✅ 认领结果回写(手动) | -| A301-2 | GET …/awrp/cas/cas_claimannounce/query | 收款认领公告查询 | ✅ 流水同步(手动 + 定时认领推送前置重查) | -| A301-3 | POST …/awrp/cas/cas/cas_claimbill/claimbillPush | 收款认领结果推送 | ✅ 定时 + 手动双入口,按流水归集 | +| A301-1 | GET …/awrp/cas/cas_recbill/queryRecBill | 收款单查询 | ✅ **角色变更**:收款流水菜单数据源(手动同步,窗口默认 60 天);原「认领结果回写」用途随 A301-3 下线作废 | +| A301-2 | GET …/awrp/cas/cas_claimannounce/query | 收款认领公告查询 | ❌ **10-29 下线**(数据源改 A301-1;代码与 `claimAnnounce.*` 配置已删除) | +| A301-3 | POST …/awrp/cas/cas/cas_claimbill/claimbillPush | 收款认领结果推送 | ❌ **10-29 下线**(认领只登记本地+推合同合规;`ReceiptClaimPushTask`、`/push-kingdee` 已删除) | | A302-2 | POST …/awrp/ar/ar_finarbill/save | 财务应收单保存 | ✅ 正式结算单(应收)推送 | | A302-4 | GET …/awrp/ar/ar_finarbill/query | 财务应收单查询 | ✅ 预检 / 硬校验 / 定时勾稽纠偏 | -| A302-6 | POST …/awrp/ap/ap/ap_invoice/assign | 收票指定应付 | ✅ 收票推送第二步 | +| A302-6 | POST …/awrp/ap/ap/ap_invoice/assign | 收票指定应付 | ✅ **触发方变更**:由付款结算单侧执行(A102-2 推送成功后逐单指定 / 发票登记推送 / 手动端点 invoice-assign-kingdee / A801-3 指定补偿);收票登记推送链不再调用 | | A303-1 | POST …/awrp/sim/sim_original_bill/save | 开票申请单新增 | ✅ 按发票张次逐张推送 | -| A303-2 | POST …/awrp/ap/ap_invoice/cusBatchSave | 收票单批量保存 | ✅ 收票登记推送 | +| A303-2 | POST …/awrp/ap/ap_invoice/cusBatchSave | 收票单批量保存 | ✅ 收票登记推送(**先票后款**:报文按发票明细行组装,终态 pushed) | | A303-4 | GET …/awrp/sim/sim_original_bill/query | 开票申请单查询 | ✅ 批量 billno 幂等预检 / 附件补传反查星瀚内码 | | A303-5 | POST /ierp/kapi/app/sim/openApi | 开票发票查询(票易通) | ✅ 定时回写开票结果 | -| A801-1 | POST …/awrp/rim/rim_invoice/query | 全票池发票查询(rim 契约) | ✅ 定时全量同步 + 票号回源 + 保存/认领闸门 + 附件回源 | -| A801-2 | POST …/awrp/bosext/attachment/download | 进项发票文件下载 | ✅ **已接线**:附件查看/下载流式代理 | -| A801-3 | GET …/awrp/ap/ap_invoice/apinvoicequery | 收票单查询 | ✅ 推送预检 + 定时回写(防重键已修复=登记 id) | +| A801-1 | POST …/awrp/rim/rim_invoice/query | 全票池发票查询(rim 契约) | ✅ 定时全量同步 + 票号回源 + 保存/认领闸门 + 附件回源 + **发票明细带出(items_json,先票后款分录取数来源)** | +| A801-2 | POST …/awrp/bosext/attachment/download | 进项发票文件下载 | ✅ 已接线:附件查看/下载流式代理 | +| A801-3 | GET …/awrp/ap/ap_invoice/apinvoicequery | 收票单查询 | ✅ 推送预检 + 定时回写(防重键=登记 id);「指定补偿」升级为重试引擎(pushed+已认领→补发 A302-6) | | A801-4 | GET …/awrp/base/bd_exrate_tree/Forexquery | 汇率查询 | ✅ 结算页面取汇率 | -| A801-5 | POST …/awrp/bdm/bdm_blacklist_config/blacklistquery | 黑名单查询 | ✅ **新增启用**:客商提审校验 | -| A902 | POST /ierp/kapi/v2/frame/attachment/uploadFile | 附件上传 | ✅ **已接线**:4 条推送链路联带上传 | +| A801-5 | POST …/awrp/bdm/bdm_blacklist_config/blacklistquery | 黑名单查询 | ✅ 客商提审校验 | +| A902 | POST /ierp/kapi/v2/frame/attachment/uploadFile | 附件上传 | ✅ 已接线:4 条推送链路联带上传 | | — | POST /API_lockAction_Router | lockBusiData 单据锁定 | ✅ 推送成功后锁单({updateCount,updateResultList} 应答) | | — | (回调)POST /kingdee-bfi/unLockNotifyBusiData | 解锁/删单通知(unLockNotifyBusiData) | ✅ 金蝶回调本系统(免登放行) | -> 仍未实现:A901-1 供应商查询、A913 项目、A101-1/2 合同、A102-1/A302-1 暂估单(合同/项目走合同合规系统)。 -> 查询时间窗约束:A103-2/A103-4/A102-4/A302-4/A303-4/A301-1 ≤ 1 个月;A902-1 ≤ 6 个月;A301-2 无时间窗参数(按认领人+分页,时间窗口取 `thirdParty.bfi.claimAnnounce.months` 默认 6)。 +> 仍未实现:A913 项目、A101-1/2 合同、A102-1/A302-1 暂估单(合同/项目走合同合规系统);A901-1 已实现(查询回填,无推送)。 +> 查询时间窗约束:A103-2/A103-4/A102-4/A302-4/A303-4/A301-1 ≤ 1 个月(A301-1 同步按业务日期窗口切片拉取,窗口总天数取 `thirdParty.bfi.recBillSync.days` 默认 60);A301-2/A301-3 已下线。 -### 1.3 防重键(候选键)总表(2026-10-14 口径) +### 1.3 防重键(候选键)总表(2026-10-31 口径) 金蝶侧按「来源系统 + 来源单据号」幂等(`awrp_src_sys` + `awrp_src_code`)。组装:`ExternalSrcCodeSupport.buildSrcCode`(下划线拼接、去空白、去重保序)。 @@ -120,11 +144,13 @@ iPaaS 网关(/gwzh/BFI,不直连金蝶) | 付款申请 A103-1 | `paymentNo` | 配置 `0KzXs` | 固定 `paymentNo-1`(单分录);勾稽键 `awrp_finapbill_entryid`=主结算单应付防重键(本地组装) | A103-2 | | 预付款申请 A103-3 | 付款申请号口径 | `0KzXs` | `paymentNo-1` | A103-2 | | 开票申请 A303-1 | billno:单张=`application_no`,多张=`application_no-张次序号`;`awrp_src_con_no`=首张结算单应收防重键 | `0KzXs` | 开票商品行 id | A303-4(批量 billno) | -| 收票单 A303-2 | **收票登记 id** | TMS收票登记 | 与 A102-2 分录同源同口径 | A801-3(预检与回写同键) | -| 收款认领 A301-3 | **分摊结算单号下划线拼接**(重试复用同一编码)★改 | WLTMS | 单条:seq/e_corebillno=第一张结算单**金蝶内码** | A301-1(sourcebillnumber 匹配) | +| 收票单 A303-2 | **收票登记 id** | TMS收票登记 | 先票后款:逐行镜像明细,`awrp_entry_code`=发票号码_seq(重推幂等) | A801-3(预检与回写同键) | +| ~~收款认领 A301-3~~ | ~~分摊结算单号下划线拼接~~ | WLTMS | — | **10-29 下线**:认领只登记本地+推合同合规,不再推送金蝶 | | 客户 A902-2 | `number`=TMS 客商编号(KS+6位);`societycreditcode` 为金蝶侧候选键 | awrp_datasourcesys=WLTMS | — | A902-1 | -### 1.4 公共档案取数(应付/应收/收票复用 `KingdeeApBillSyncSupport`) +### 1.4 公共档案取数(应付/应收复用 `KingdeeApBillSyncSupport`;收票先票后款另见第 5.3 节) + +> ★收票 A303-2 先票后款改造后不再复用「结算明细拆分」口径:往来户=销方供应商代码(`supplier_code`,A901-1 落库)、结算组织=页面手选部门派生、币别固定 CNY、分录=镜像发票明细行(详见 5.3)。下表为应付/应收(A102-2/A302-2)共用口径。 | 取数项 | 规则(当前代码) | |---|---| @@ -186,22 +212,31 @@ iPaaS 网关(/gwzh/BFI,不直连金蝶) --- -## 2. 业务域一:客商档案(A902-1 / A902-2 / A801-5 黑名单) +## 2. 业务域一:客商档案(A902-1 / A902-2 / A901-1 供应商 / A905-3 行名行号 / A801-5 黑名单) -**定位**:全局数据前提。其他链路的往来户编码(`customer_code`)来自 `blade_customer_archive`;金蝶客户编码 = TMS 客商编号。 +**定位**:全局数据前提。其他链路的往来户编码(`customer_code`)与先票后款销方供应商代码(`supplier_code`)均来自 `blade_customer_archive`;金蝶客户编码 = TMS 客商编号。 ### 2.1 前端入口 | 页面 | 操作 | 后端端点 | 触发时机 | |---|---|---|---| -| 客商档案(`/vehicle/customer-archive`) | 新增/编辑表单录统一社会信用代码(满 18 位防抖) | `GET /bfi/customer/query-by-credit-code`(A902-1) | 回填名称/法人/地址/经营范围等并锁定;金蝶状态 A/B 阻断保存 | +| 客商档案(`/vehicle/customer-archive`) | 新增/编辑表单录统一社会信用代码(满 18 位防抖) | `GET /bfi/customer/query-by-credit-code`(A902-1) | 回填名称/法人/地址/经营范围等并锁定;金蝶状态 A/B 阻断保存;**同时按信用代码查金蝶供应商编码回填 `supplier_code`(A901-1)** | +| 同上 | 表单「供应商编码」字段 | `GET /customer-archive/query-supplier-code?creditCode=`(A901-1) | 修改信用代码后即时回显金蝶供应商编码(只读展示,系统自动获取) | +| 同上 | 收款信息「开户行」失焦 | `GET /customer-archive/query-bank?bankName=`(A905-3) | 行名精确匹配金蝶行名行号档案,**带出联行号** | +| 同上 | 收款信息保存前校验 | `GET /customer-archive/query-bank-by-number?cnapsCode=`(A905-3) | 按联行号反查金蝶行名行号档案,查不到拦截保存(接口异常上抛,与「查不到」区分) | | 同上 | 行内「同步金蝶」链接(仅 approved 且 failed 显示) | `POST /customer-archive/sync-kingdee`(A902-2) | 失败人工重推 | | 同上 | 行内「审批通过」 | `POST /customer-archive/approve` → 内部 `syncKingdee(id)` | **提审校验 A801-5 黑名单;通过后自动推送 A902-2** | +![客商档案表单:基础信息含「供应商编码」字段(金蝶供应商编码,系统自动获取,A901-1 按信用代码回填);收款信息 tab 维护开户行/联行号(A905-3 行名带出、保存前校验)](./2026-10-14-金蝶(星瀚)BFI全链路梳理(前端-后端-定时任务·业务域汇总与操作手册).assets/tms-客商供应商编码.png) + ### 2.2 后端链路 **A902-1 客户查询(3 入口)**:前端回填 / 单条推送前复查(`syncKingdee` 先查一次,C→exist 终态、A/B→failed)/ 定时批量复查(50 信用代码/批翻页取全)。 +**A901-1 供应商查询(★10-28 新增)**:`CustomerArchiveServiceImpl.queryKingdeeSupplierNumber` 按「统一社会信用代码 + enable=启用」调 `bd_supplier/query` 取金蝶供应商 `number`。落库三个入口:① 客商新增建档(提交前);② 金蝶同步复查回填(`fillSupplierCode`,查到才覆盖);③ 手动端点 `GET /customer-archive/query-supplier-code?creditCode=`(前端修改信用代码后即时回显)。查不到/异常返回 null 不阻断——该字段是**先票后款 A303-2 往来户的数据前提**(收票登记销方供应商代码按销方名称匹配本地客商档案 `supplier_code`)。 + +**A905-3 行名行号查询(★10-31 新增)**:`bd_bebank/query` 查金蝶行名行号档案。两个端点:`GET /customer-archive/query-bank?bankName=`(已审核+启用档案按行名查,pageSize=10,本地全称精确比对取第一条,**失焦带出联行号**,异常返回 null);`GET /customer-archive/query-bank-by-number?cnapsCode=`(按行号精确比对,**保存前校验**,查不到返回 null 由前端拦截保存、接口异常直接上抛)。后端保存仅校验联行号非空/长度,真实性校验由前端调此接口完成。 + **A801-5 黑名单(★新增,提审拦截)**:`CustomerArchiveServiceImpl.submitApproval` → 取客商各发票 `taxNo` 去重大写 → `queryBlacklist("1", taxNos)`(enable=1,pageSize 上限 2000,循环翻页取全)→ **A 类硬拦截**;B 类默认放行告警(`thirdparty.bfi.blacklist.blockRiskWarning=true` 时拦截);其他类别放行告警;**查询异常上抛阻断提审**。 **A902-2 客户保存(3 入口收敛 `pushCustomerToKingdee`)**:审批通过自动(失败不影响审批)/ 人工重推(synced/exist 幂等返回)/ 定时重推(pending=approved 且 none/failed)。先 A902-1 复查(金蝶已有 C 直接置 exist 跳过)再保存。只推新增建档(审批后变更不重推)。参数要点:`entry_groupstandard` 按 `customerKind` internal→1101 / 其余→1102;`entry_bank` 取 `blade_customer_receipt_account`(账号/开户行/CNY/默认 + **真实联行号 cnaps_code**,硬编码已删);`bizfunction` 固定 `1,2,3,4`。 @@ -265,6 +300,7 @@ iPaaS 网关(/gwzh/BFI,不直连金蝶) ![预付款对照:TMS 进度预付申请(240.12)下推金蝶生成「其他付款申请」FKSQ-00000219(进项货付款申请),分录预付款金额、往来户、结算方式对应;附件区可见 A902 联带上传的文件](./2026-10-14-金蝶(星瀚)BFI全链路梳理(前端-后端-定时任务·业务域汇总与操作手册).assets/bfi-effect-a103-3-预付款对照.png) - **A103-2 回写状态映射**:`Alreadypay`→paid(终)>`billstatus=D`→closed(终)>`Inpayment`→paying>其余 synced;`entry[].e_paidamt` 汇总→paid_amount(缺失且 paid 兜底=申请金额);paid 联动履约(预结算垫资/结算付款记录);**failed 纳入勾稽纠偏**(查到即纠正 synced);**查不到→消失解锁计数**(仅已生成/付款中参与,连续 2 轮→解锁置 failed)。 +- **★HCS 付款履约计划/记录改金蝶实付后推送(10-31)**:A103-2 回写付款推进(首次付款或 paid 金额变化)时将付款申请 `hcs_sync_status` 置回 unsynced,由 HCS 推送任务重新编排;`HcsPaymentSyncSupport` 内置**金蝶实付门禁**——`paidAmount<=0` 直接跳过标记 synced,`paidAmount>0` 才推 C103-9 付款履约计划与 C108-1/2 履约记录(原「未付款跳过履约记录」分支合并为统一门禁)。合同链履约计划首推时点不变;收款认领链的 C103-9 累加的是收款计划行,与本门禁互不影响。 - **A103-4 付款记录**:按付款组织编码分组查询(`org_number` 优先取 A103-2 返回值),窗口最近 1 个月,`sourcebillid`==kingdee_bill_id 优先、`sourcebillnumber` 兜底;upsert `blade_payment_application_record`(actpayamt/paid_date/整单 JSON→voucher_json);清理 MOCK-PAY- 模拟记录。 - **A902 附件**:推送成功事务提交后联带上传(`KingdeeAttachmentSyncSupport.syncUserAttachments`),失败不污染主单状态,重推补传。 - **异常落库**:手动/自动同步失败统一独立事务落 `failed` + 原因(截 500 字),由「手动重推/failed 重推任务」续推。 @@ -311,33 +347,47 @@ iPaaS 网关(/gwzh/BFI,不直连金蝶) --- -## 5. 业务域四:收票与金蝶票池 +## 5. 业务域四:收票与金蝶票池(先票后款两段式) -### 5.1 架构现状(★10-05 回切后终态) +> ★10-31 先票后款改造(禅道 #15850):收票登记拆分为纯「收票」动作(不关联结算单/合同,推 A303-2 即终点);「发票跟结算单的指定」移到**付款结算单**侧完成(认领收票单 → 应付单生成后 A302-6 指定应付)。方案细节见《2026-10-31-先票后款改造方案V2(收票登记A303-2+付款结算单A102-2+A302-6指定应付·实现流程与逻辑)》。 + +### 5.1 架构现状(★10-31 先票后款改造后终态) ``` 【数据面】InvoiceReceiptKingdeeSyncTask 定时(默认关,每30分钟) A801-1 全票池增量同步(incrementDays=30)→ invoiceTypes 白名单下推远端逐票种切片轮询 - → 2.5 万窗口硬上限触顶二分时间片递归 → 镜像 blade_kingdee_invoice_pool 批量 upsert(无变化跳写) + → 2.5 万窗口硬上限触顶二分时间片递归 → 镜像 blade_kingdee_invoice_pool 批量 upsert(含 items_json 明细,无变化跳写) → refreshBoundReceipts:未绑定存量登记补绑(已绑定零写入) 手动端点:POST /invoice-receipt/sync-invoice-pool(同一方法+同一把锁) -【选票面】收票登记表单 / 结算单认领弹窗(KingdeeInvoicePoolDialog) - GET /invoice-receipt/invoice-pool、GET /formal-settlement/invoice-pool - → 本地镜像真分页;仅「票号精确查 + 无认领/登记过滤 + 本地空页」时窄查 A801-1 回源一次再本地重查 +【选票面】 + GET /invoice-receipt/invoice-pool 收票登记选票弹窗:本地镜像真分页;仅「票号精确查+无过滤+本地空页」窄查 A801-1 回源 + GET /invoice-receipt/invoice-detail-query ★新增 选票后带出发票明细行(镜像 items_json 优先,缺则按票号回源,仍无走票池窄查) + GET /invoice-receipt/claim-candidates ★新增 付款结算单「选择收票单」候选:审批通过+未被有效应付结算单认领,keyword 模糊,limit 200 行过滤:票号非空 + amountUseStatus=0 + availableAmount>0 -【闸门】收票登记保存 persistMirrorFromRemote(按票号精确单查+可用性复核+FOR UPDATE upsert) - 结算单认领确认 assertInvoicesClaimable(★10-05 补上:逐票窄查+可用复核,红冲/作废/被占用在此拦截) +【先票——收票登记】(不关联结算单/合同) + 选票 → 表单带出票面+明细 → 手选结算组织(承办部门)→ 保存草稿(销方供应商代码尽力回填 queryCustomerCode) + → 提交审批 → 审批通过(receipt 开关自动 / 工具栏「批量同步」手动) + → 前置校验:审批通过 + assigned 不重推 + 已绑镜像(checkStatus=2)+ 镜像明细齐备(缺则回源)+ 结算组织必选 + (★删除原 A102-4 应付预检与「恰好分摊 1 张应付结算单」要求) + → A303-2 收票单保存 → 终态【pushed】→ A902 附件联带上传;缺票种编码按票号窄查 A801-1 补填 -【推送链】审批通过自动推送(receipt 开关)/ 工具栏「批量同步」 - A102-4 预检(应付已生成)→ A801-3 幂等预检(防重键=收票登记 id) - → A303-2 收票单保存(付款方式固定现购)→ pushed → A302-6 指定应付 → assigned(终态) - → A902 附件联带上传;缺票种编码按票号窄查 A801-1 补填 +【后款——付款结算单(正式结算单应付 Tab)】 + 路径A 编辑器/新增:发票信息「选择收票单」(claim-candidates)→ 全额认领(匹配金额=发票全额, + ★放开「发票匹配合计≤结算金额」上限校验) + 路径B 审批后:列表行内「发票登记」→ 选收票单保存认领 → 弹窗「是否将发票匹配数据推送金蝶?」→ 确认调 A302-6 + 关系同步:rebuildInvoices / claimInvoices → syncSettlementReceiptRelations + (按发票号匹配审批通过收票登记 + 一票一结算单校验 + 重建 InvoiceReceiptSettlement + 刷新结算单收票状态) + 结算单审批通过 → A102-2 应付单推送成功 → assignClaimedReceipts:本单认领且收票单 pushed 的逐单 A302-6 → assigned(终态) + (未推送/已指定跳过;单笔失败仅记日志不阻断结算同步) + 手动端点:POST /formal-settlement/invoice-assign-kingdee(前置 payable+approved+synced) -【回写面】PaymentKingdeeResultTask.syncKingdeeReceivedInvoiceResult(默认关,每10分钟)/ 手动端点 - A801-3 按「收票登记 id」匹配(★缺陷已修)→ 发票状态异常回收置 failed - / pushed 补偿重发 A302-6 / assigned 缺单号补登;窗口内查不到不改状态 +【回写面】PaymentKingdeeResultTask.syncKingdeeReceivedInvoiceResult(独立开关,每10分钟)/ 手动端点 + A801-3 按「收票登记 id」匹配 → 发票状态异常(作废2/红冲3/部分红冲7/全部红冲8)回收置 failed + → pushed+已认领(应付防重键恰好可组装)→ 补发 A302-6 → assigned(★指定补偿升级为重试引擎) + → pushed+未认领 → 跳过不再置 failed(认领后由结算单侧补指定) + → assigned 缺单号补登;窗口内查不到不改状态 【附件面】GET /invoice-receipt/invoice-pool/attachment/download?invoiceNo=(A801-2 流式代理) 镜像优先、实时回源兜底;pdfurl 优先 taxPdfUrl 兜底,都缺退 downloadUrl(fileType=4 判 OFD) @@ -348,78 +398,114 @@ iPaaS 网关(/gwzh/BFI,不直连金蝶) | 页面 | 操作 | 后端端点 | 说明 | |---|---|---|---| | 收票登记(`/payment/invoice-receipt`) | 工具栏「批量同步」 | `POST /invoice-receipt/push-kingdee`(ids) | 单笔失败不中断整批,逐单报因 | -| 收票登记表单 | 发票号码放大镜(选票弹窗) | `GET /invoice-receipt/invoice-pool` | 镜像分页 + 回源;确认/保存前防重校验 | -| 收票登记表单 | 附件 查看/下载 | `GET /invoice-receipt/invoice-receipt/invoice-pool/attachment/download`(A801-2 代理) | 120s blob;PDF 优先 | +| 收票登记表单 | 发票号码放大镜(选票弹窗) | `GET /invoice-receipt/invoice-pool` | 镜像分页 + 回源;保存前防重校验 | +| 收票登记表单 | 选票后自动带出「发票明细」区块 | `GET /invoice-receipt/invoice-detail-query?invoiceNo=` | A303-2 分录取数预览(货物名称/数量/单价/金额/税率/商品编码) | +| 收票登记表单 | 手选「结算组织(合同承办部门)」必填 | 部门树选择 | 推送时沿部门树上溯平台公司取金蝶组织编码 | +| 收票登记表单 | 「结算信息」区 | 无调用(只读) | 展示认领关系:「暂无认领数据(认领关系由付款结算单侧维护)」 | | 收票登记 | 行内「审批通过」 | `POST /invoice-receipt/approve` | receipt 开关开启时自动推送 | -| 正式结算单编辑器 | 发票认领区「查询可认领发票」/「确认识领」 | `GET /formal-settlement/invoice-pool`、`POST /formal-settlement/claim-invoices` | 确认时走认领闸门;未保存单仅本地暂存 | -| 共享弹窗 `KingdeeInvoicePoolDialog` | 行附件 查看/下载 | 同 A801-2 代理 | 票池弹窗、结算认领行、付款申请发票附件行、收票附件表 4 处复用 | +| 收票登记 | 附件 查看/下载 | `GET /invoice-receipt/invoice-pool/attachment/download`(A801-2 代理) | 120s blob;PDF 优先 | +| 付款结算单编辑器(应付) | 发票信息「选择收票单」 | `GET /invoice-receipt/claim-candidates` + `POST /formal-settlement/claim-invoices` | 候选=审批通过+未认领收票登记;**全额认领**(匹配金额=发票金额,不校验合计上限);保存同步收票侧关系 | +| 付款结算单列表 | 行内「发票登记」(审批通过行) | `POST /formal-settlement/claim-invoices` + `POST /formal-settlement/invoice-assign-kingdee` | 认领收票单 → 弹窗确认推送指定应付 | +| 共享弹窗 `KingdeeInvoicePoolDialog` | 行附件 查看/下载 | 同 A801-2 代理 | 票池弹窗、付款申请发票附件行、收票附件表等复用 | -### 5.3 后端要点 +![收票登记表单(先票后款改造后):发票号码从金蝶票据池选票带出票面;新增「结算组织(合同承办部门)」手选必填;「发票明细」区块按票带出发票明细行(A303-2 分录取数来源);「结算信息」区只读——认领关系由付款结算单侧维护](./2026-10-14-金蝶(星瀚)BFI全链路梳理(前端-后端-定时任务·业务域汇总与操作手册).assets/tms-先票后款-收票登记表单.png) -**金蝶侧效果**: +![付款结算单「发票登记」弹窗(先票后款路径B):结算基本信息(结算单号/结算总金额/已匹配发票金额/金蝶同步状态)+ 认领收票单(选择收票单 → 发票号/开票单位(销方)/发票金额/匹配结算单金额)→ 保存认领后可推送指定应付](./2026-10-14-金蝶(星瀚)BFI全链路梳理(前端-后端-定时任务·业务域汇总与操作手册).assets/tms-先票后款-付款结算单发票登记.png) + +### 5.3 后端要点(当前口径) + +**A303-2 主表取值(★先票后款重写)**: + +| 字段 | 取值 | +|---|---| +| awrp_src_code | 收票登记 id(重推幂等) | +| receivablessupp_number 往来户 | 登记回填的 `supplier_code`(销方名称→本地客商档案匹配,`customer_code` 兜底);缺失推送时实时再查一次,仍缺失**报错阻断**。供应商编码由 A901-1 在客商建档/同步复查时落库 | +| org_number 结算组织 | 页面手选部门 `kingdeeOrgNumberByDept`(沿部门树上溯唯一平台公司取金蝶组织编码) | +| currency_number 结算币别 | 固定 **CNY**(不挂合同后的默认,外币票暂无取值来源) | +| asstacttype 往来类型 | 固定 `bd_customer` | +| invoiceno/receivedate/issuedate | 登记发票号 / 推送当日 / 镜像开票日期 | +| biztype | 固定 `PUR`(采购);销方=镜像开票单位、购方=受票单位;`isincludetax=true`;awrp_src_type=TMS收票登记 | + +**A303-2 分录(逐行取镜像 `items_json`,明细缺失/解析失败拦截)**: + +| 字段 | 取值 | +|---|---| +| e_pricetaxtotal | 明细金额+税额(合计为 0 拦截) | +| quantity / price | 明细数量(≤0 兜底 1)/ 明细单价(**统一 4 位小数 HALF_UP**,空按价税合计÷数量反算) | +| material_number / e_taxclass_number | 行商品编码 goodsCode(数电票行商品编码即税收分类编码;缺失拦截) | +| taxrateid_number | `V`+整数税率(13%/13/0.13→V13;**缺失或 0/免税行拦截**提示人工) | +| e_amount / e_tax | 明细金额 / 税额(空不送) | +| awrp_entry_code | **发票号码_seq**(分录候选键,重推幂等) | + +![收票登记列表:批量同步/新增/导出;付款方(受票单位)/收款方(开票单位)列随新表单口径展示](./2026-10-14-金蝶(星瀚)BFI全链路梳理(前端-后端-定时任务·业务域汇总与操作手册).assets/tms-先票后款-收票登记列表.png) + +**状态机与关键规则**: + +- 状态机:`unsynced → pushed`(A303-2 成功,**收票推送终点**)→ `assigned`(仅由付款结算单侧 A302-6 成功后)→ failed(A303-2 失败 / A801-3 发票状态异常回收 / 指定失败回收 / 自动路径前置拦截)。 +- 关系同步 `syncSettlementReceiptRelations`:按行发票号匹配审批通过收票登记 → **一票一结算单校验**(已挂其他有效应付结算单抛「一张收票单只能被一张付款结算单认领」)→ 删旧建新(认领金额=发票全额)→ 刷新结算单收票状态。 +- **收票状态按覆盖判定**:结算单已匹配发票金额 `>=` 结算金额即 completed(允许超额覆盖),否则 partial。 +- **反向操作不同步金蝶**:结算单作废/认领移除不调 A302-6 解除指定,需金蝶侧人工解除后重推(已拍板)。 +- 历史兼容:历史已 assigned 单据不受影响;收票登记编辑未传 settlements 时不动既有分摊关系(关系只由结算单侧维护)。 +- 镜像比对集含 `itemsJson`(先票后款后明细是分录唯一取数来源,明细变化必须跟随票池刷新);全票池同步细节不变(2.5 万上限、单调二分、轻量比对跳写)。 + +**金蝶侧效果(历史推送数据,供对照)**: ![TMS 收票管理列表「金蝶状态」列:已指定应付(终态)/推送失败(带失败原因)/未推送,金蝶单据号 AVT-202608-000396 悬浮展示](./2026-10-14-金蝶(星瀚)BFI全链路梳理(前端-后端-定时任务·业务域汇总与操作手册).assets/bfi-effect-收票金蝶状态列表.png) ![A902 附件上传金蝶侧效果:金蝶财务应付单详情「附件」区收到 TMS 推送单据联带上传的文件;付款计划区同时可见下推生成的付款计划(外部系统来源标识=防重键)](./2026-10-14-金蝶(星瀚)BFI全链路梳理(前端-后端-定时任务·业务域汇总与操作手册).assets/bfi-effect-a902-附件上传金蝶侧.png) -![A801-2 发票查看/下载入口:正式结算单详情「发票信息」区认领的发票行提供 查看/下载(走后端 A801-2 流式代理,PDF 版式优先);付款信息区同时回显付款申请的金蝶单号](./2026-10-14-金蝶(星瀚)BFI全链路梳理(前端-后端-定时任务·业务域汇总与操作手册).assets/bfi-effect-a801-2-发票查看下载.png) - -- 推送计划校验:approved、状态≠assigned、已绑定镜像、`checkStatus=2`、**恰好分摊 1 张审批通过应付结算单(一票一应付)**、有结算明细;不满足写 `blockedReason` 不改状态。 -- A801-3 预检命中且状态正常→跳过 A303-2 直接续推指定(断点恢复);发票状态异常(作废2/红冲3/部分红冲7/全部红冲8)→failed 报人工。 -- 落库:`kingdee_received_invoice_no`、状态机 `unsynced→pushed→assigned/failed` →`kingdee_push_status`(唯一金蝶状态来源);终态/失败写 `blade_invoice_receipt_record`;pushed/assigned 禁止本地作废。 -- 全票池同步细节:单查询窗口 MAX_POOL_PAGES×500=2.5 万上限,`updateTime` 单调二分不重不漏;镜像 upsert 轻量比对(流水号/价税合计/税额/可用金额/使用状态)无变化跳写。 +![A801-2 发票查看/下载入口:结算单详情「发票信息」区认领的发票行提供 查看/下载(走后端 A801-2 流式代理,PDF 版式优先);付款信息区同时回显付款申请的金蝶单号](./2026-10-14-金蝶(星瀚)BFI全链路梳理(前端-后端-定时任务·业务域汇总与操作手册).assets/bfi-effect-a801-2-发票查看下载.png) --- -## 6. 业务域五:收款认领(A301-2 / A301-3 / A301-1) +## 6. 业务域五:收款流水与认领(A301-1 数据源 + 合同合规推送) -**定位**:业务先行认领。金蝶司库第一轮自动认领到客户维度 → TMS 二次认领到结算单维度 → **本地认领完成后**(定时或手动)回推金蝶生成收款单 → 手动查询回写结果。★09-28 重构后:认领与推送解耦,推送按流水归集、定时+手动双入口。 +> ★10-29 改造:数据源 A301-2 → **A301-1 收款单查询**,A301-3 推送金蝶链路整体下线,认领只登记本地并推送**合同合规系统**。金蝶收款单在认领前已存在(A301-1 即金蝶收款处理单),因此**不再向金蝶回写任何数据**。方案与操作细节见《2026-10-29-收款流水菜单改造(数据源A301-2改A301-1-认领只登记本地并推送合规C107-1与C107-3作废同步)》《2026-10-29-收款流水认领操作流程(收款金额本地额度·单条批量与作废)》。 ### 6.1 链路总览 ``` -阶段一 收款流水页「流水同步」(手动) - POST /receipt-flow/sync ▶ A301-2 认领公告查询(claimusers=Nacos thirdParty.bfi.claimAnnounce.claimusers, - ★不再按登录用户姓名;未配置则以空值调用会查不到数据) - ▶ 全量翻页(窗口 months=6)▶ 按 detail_serial_no 幂等 upsert blade_kingdee_receipt_flow +阶段一 收款流水页「手动同步流水」(手动,无定时任务) + POST /receipt-flow/sync ▶ A301-1 queryRecBill + ▶ 业务日期窗口=当天倒推 thirdParty.bfi.recBillSync.days(默认 60 天,支持热更新) + ▶ 按平台公司金蝶组织编码过滤,全量翻页(pageSize 200,窗口切片规避单查 1 个月上限) + ▶ 一张金蝶收款单=一条流水(幂等键 detail_serial_no=收款单号),金蝶单号/单据状态随同步刷新 + ▶ 前置:组织管理维护「是否平台公司=是」+ 金蝶组织编码,缺失直接报错拦截 -阶段二 流水详情/批量认领(手动) - POST /receipt-flow/claim ▶ 本地落库(claim + 分摊 + 流水 claimed_amount/claim_status 刷新 + 结算单收款汇总) - ▶ 本地完成即结束,**不调金蝶**(认领本地化★) +阶段二 流水详情/批量认领(手动,只登记本地) + POST /receipt-flow/claim ▶ 本地落库(认领主单+结算单分摊+流水 claimed_amount/claim_status 刷新+结算单收款汇总) + ▶ 认领上限=收款金额 actrecamt(unmatchamountrec 未匹配金额仅参考展示,不参与额度控制) + ▶ 不调金蝶(A301-3 下线★) -阶段三 推送金蝶(定时 ReceiptClaimPushTask 默认关每10分钟 / 手动流水行内「推送金蝶」) - pushClaimToKingdee ▶ 按认领人逐人重查 A301-2 → 扫「认领完成未推送」流水按流水归集 - ▶ 逐流水:awrp_src_code=分摊结算单号拼接(重试复用幂等)→ A301-3 推送 - (分录单条:e_receivableamt=全部未推送金额;e_corebillno=第一张结算单金蝶内码,缺内码拦截不推) - ▶ 成功回写 kingdee_bill_no(回执 keys.awrp_src_code 占位)+ kingdee_bill_status=pushed +阶段三 认领事务提交后推合同合规(afterCommit,失败只落标记不回滚) + HcsClaimSyncSupport.pushClaimToHcs ▶ C104-2 按明细流水号定位合规侧流水 ID + ▶ 逐合同:C103-9 履约计划金额累加 → C107-1 认领记录(返回 fdId 落库 blade_receipt_claim_hcs_record)→ C108-1 履约流水 + ▶ 开关:exe-stage.sync-enabled 门禁 C103-9/C107-1;perf-record.sync-enabled 门禁 C108-1 -阶段四 认领记录页「查询金蝶认领结果」(手动) - POST /receipt-claim-record/query-kingdee-result ▶ A301-1 收款单查询(今天回溯 30 天) - ▶ sourcebillnumber==awrp_src_code 匹配 ▶ 真实收款单号覆盖占位单号 + 状态推进(C→approved,D→received 终态) +阶段四 作废(认领记录页,仅本人+已认领记录) + POST /receipt-claim-record/void ▶ 本地回退金额/状态 → afterCommit 逐条推 C107-3(fdStatus=1 已作废,按落库 fdId) + ▶ 单条失败只记日志不回滚作废;无合规记录(曾推送失败)静默跳过;作废不生成金蝶冲销单 ``` ### 6.2 前端入口 | 页面 | 操作 | 后端端点 | 说明 | |---|---|---|---| -| 收款流水(`/payment/receipt-flow`) | 工具栏「流水同步」 | `POST /receipt-flow/sync`(A301-2) | 权限 `receipt_flow_sync` | -| 同上 | 流水详情/批量认领表单「确认」 | `POST /receipt-flow/claim` | 选择审批通过应收结算单按比例分摊;已推送金蝶的认领禁止作废(后端硬校验) | -| 同上 | 行内「推送金蝶」 | `POST /receipt-flow/push-kingdee?flowId=` | 确认弹窗后按流水归集推送 | -| 认领记录(`/payment/receipt-claim-record`) | 工具栏「查询金蝶认领结果」 | `POST /receipt-claim-record/query-kingdee-result`(A301-1) | 手动;窗口内查不到不改状态 | +| 收款流水(`/payment/receipt-flow`) | 工具栏「手动同步流水」 | `POST /receipt-flow/sync`(A301-1) | 权限 `receipt_flow_sync`;前置组织金蝶编码 | +| 同上 | 流水详情/批量认领「确认」 | `POST /receipt-flow/claim` | 应收+审批通过+有剩余可认领金额的结算单;多选须同项目/组织/收付款方;批量认领须同付款人+对方账号 | +| 认领记录(`/payment/receipt-claim-record`) | 行内「作废」 | `POST /receipt-claim-record/void` | 仅本人+已认领;确认提示含「同步作废合同合规系统认领记录」 | + +![收款流水列表(A301-1 数据源):收款单号即金蝶收款单号(RV-…)、收款金额为认领上限、未匹配金额仅参考;工具栏仅「手动同步流水/批量认领」——原「推送金蝶」按钮与推送状态搜索已随 A301-3 下线删除](./2026-10-14-金蝶(星瀚)BFI全链路梳理(前端-后端-定时任务·业务域汇总与操作手册).assets/tms-a301-1-收款流水列表.png) + +![认领记录列表:原「查询金蝶认领结果」按钮已删除(金蝶状态随 A301-1 同步刷新,无需回写);作废操作同步作废合同合规系统认领记录](./2026-10-14-金蝶(星瀚)BFI全链路梳理(前端-后端-定时任务·业务域汇总与操作手册).assets/tms-认领记录页.png) ### 6.3 后端要点 -**金蝶侧效果**: - -![A301-2 对照:金蝶「认领公告列表」(RLTZ 通知单,待认领/部分认领)与 TMS 收款流水「手动同步流水」拉取的同批公告对照](./2026-10-14-金蝶(星瀚)BFI全链路梳理(前端-后端-定时任务·业务域汇总与操作手册).assets/bfi-effect-a301-2-公告同步对照.png) - -![A301-3 对照:TMS 认领记录(RLTZ-20260612-0073 认领 828.00)推送后,金蝶「认领处理列表」生成认领单 RL-20260929-0058(已确认,认领通知单 0073 对应)](./2026-10-14-金蝶(星瀚)BFI全链路梳理(前端-后端-定时任务·业务域汇总与操作手册).assets/bfi-effect-a301-3-认领推送对照.png) - -![A301-3 金蝶侧认领结果详情:收款认领结果行「核心单据编号」=结算单星瀚内码(2586341365516352512,即 e_corebillno 传金蝶内码的落地证据)](./2026-10-14-金蝶(星瀚)BFI全链路梳理(前端-后端-定时任务·业务域汇总与操作手册).assets/bfi-effect-a301-3-金蝶认领结果.png) - -- 推送闸门:已推送(pushed)流水跳过不重推;`billid`(认领公告 id)缺失拦截「请先同步金蝶收款流水」;结算单金蝶内码缺失拦截不推。 -- 幂等:`awrp_src_code` 重试复用同一编码;作废认领不再生成本地冲单(金蝶单据状态保留原值)。 -- A301-3 字段待金蝶确认项仍在:`recpaytype_number`(常量当前值 "999",注释示例 101 待确认)、`paymenttype`、`recpayer`(金蝶付款人 master id)。 +- **字段映射与表结构**:`blade_kingdee_receipt_flow` 新增 `org_number/open_org_number`(收款人/核算组织编码)、`match_amount_rec/match_amount_pay/unmatch_amount_pay/settle_amount/unsettle_amount`;`unclaimed_amount` 改名 `unmatch_amount_rec`;`bill_id/payer_id/kingdee_claim_status/claim_me/push_status/push_fail_reason/pushed_time/awrp_src_code` 等 A301-2/A301-3 专属列废弃保留(存量仅展示)。 +- **单据状态映射(随同步刷新)**:A 暂存/B 已提交→pushed、C 已审核→approved、D 已收款→received、G 已退单/E 变更中/其它→failed(需人工在金蝶核查)。 +- **认领额度**:流水维度「累计认领 ≤ 收款金额」;结算单维度「累计认领 ≤ 结算总应收含税金额」。金蝶侧已核销(未匹配金额=0)不影响认领。 +- **合规推送失败不重试**:`blade_receipt_claim_hcs_record` 落 failed+原因,人工在合规侧补录(业务拍板,无定时补偿)。 +- **下线清单**:`pushClaimToKingdee` 等推送方法、`ReceiptClaimPushTask`(10 分钟定时)、`/receipt-flow/push-kingdee`、`/receipt-claim-record/query-kingdee-result`、`awrpSrcCode` 生成、Nacos `claimAnnounce.*/receiptClaimPush.*` 配置全部删除。 --- @@ -459,20 +545,22 @@ iPaaS 网关(/gwzh/BFI,不直连金蝶) ▼ 定时 A302-4 勾稽纠偏 + A303-5 开票结果 ─▶ invoiced(终态,发票文件落库) -【收票主线】 定时 A801-1 全票池同步 ─▶ 镜像表 ◀── 选票读镜像(票号未命中窄查回源) - │ 收票登记保存(闸门)/ 结算认领确认(闸门★新增) +【收票主线·先票后款】 定时 A801-1 全票池同步(含明细 items_json)─▶ 镜像表 ◀── 选票/明细/候选读镜像(票号未命中窄查回源) + │ 收票登记保存(闸门);付款结算单认领收票单(syncSettlementReceiptRelations 关系同步) ▼ - 收票登记审批 ─A102-4 预检─A801-3 预检(登记id)─▶ A303-2 保存 ─▶ pushed ─▶ A302-6 ─▶ assigned - │(本期不锁单;A902 附件联带上传) + 收票登记审批 ─A801-3 预检(登记id)─▶ A303-2 保存(销方供应商代码/CNY/明细行)─▶ pushed(推送终点) + │(本期不锁单;A902 附件联带上传;★不再 A102-4 预检、不再推送时立即 A302-6) ▼ - 定时 A801-3 回写(异常回收/指定补偿/单号补登,按登记 id 匹配★已修) + 付款结算单 A102-2 应付单推送成功 ─▶ 逐单 A302-6 指定应付(手动端点/发票登记弹窗确认)─▶ assigned(终态) + ▼ + 定时 A801-3 回写(异常回收/pushed+已认领补指定/单号补登,按登记 id 匹配) -【收款认领主线】 手动 A301-2 公告同步 ─▶ blade_kingdee_receipt_flow - │ 页面认领(本地化,不调金蝶) +【收款流水与认领主线】 手动 A301-1 收款单同步 ─▶ blade_kingdee_receipt_flow(金蝶单号/状态随同步刷新) + │ 页面认领(只登记本地,上限=收款金额;不调金蝶) ▼ - 定时/手动 A301-3 按流水归集推送(e_corebillno=结算单金蝶内码)─▶ pushed + 认领事务提交后推合同合规:C103-9 → C107-1(fdId 落库)→ C108-1 ▼ - 手动 A301-1 收款单查询 ─▶ 真实收款单号覆盖 + approved/received(终) + 作废:本地回退 + C107-3(fdStatus=1);金蝶侧不回写 【横切】 审批通过 AFTER_COMMIT 自动同步(4 开关)│ 推送成功→lockBusiData(3 链路) 金蝶删单→unLockNotifyBusiData 回调还原 │ 锁失败定时/手动补偿 @@ -485,50 +573,53 @@ iPaaS 网关(/gwzh/BFI,不直连金蝶) |---|---|---| | 1 | 客商档案先于一切推送(本地 customer_code 必须存在) | 查不到直接报错;金蝶侧审核状态不校验(软依赖) | | 2 | 客商提审先过 A801-5 黑名单 | A 类命中/查询异常即阻断提审 | -| 3 | A102-2 先于 A103-1 与 A303-2 | A102-4 硬校验/预检 | -| 4 | A302-2 先于 A303-1 与 A301-3(e_corebillno 需金蝶内码) | A302-4 硬校验;内码缺失拦截不推 | -| 5 | A303-2 先于 A302-6 | 同一次推送内串行 | -| 6 | 推送先于回写(A103-2/A103-4、A302-4 勾稽、A303-5、A801-3、A301-1) | 查询键由推送链写入本地 | +| 3 | A102-2 先于 A103-1 | A102-4 硬校验/预检 | +| 4 | A302-2 先于 A303-1 | A302-4 硬校验 | +| 5 | A303-2(收票单)先于 A302-6(指定应付);A102-2(应付单)先于 A302-6 | 收票推送终点 pushed;A302-6 由付款结算单侧执行(应付推送成功后逐单指定 / 发票登记弹窗确认 / 手动端点 / A801-3 定时补偿) | +| 6 | 推送先于回写(A103-2/A103-4、A302-4 勾稽、A303-5、A801-3) | 查询键由推送链写入本地 | | 7 | 锁单在推送成功之后、金蝶删单回调可还原 | lockAfterPush 事务提交后执行;unLockNotifyBusiData 免登回调 | | 8 | A801-4 汇率查询先于 A102-2/A302-2(如需携带汇率) | 页面联动维护,未维护不传 | +| 9 | 客商 A901-1 供应商编码先于收票 A303-2 推送(销方供应商代码) | 销方名称匹配本地客商档案 supplier_code;缺失推送前强校验拦截 | **断链恢复语义**:本地失败/远端未落单→unsynced 或 failed 可重推;远端成功本地回滚→重推时预检幂等回填(A102-4/A302-4/A103-2/A303-4/A801-3/awrp_src_code 复用);推送成功后状态滞后→定时回写;批量语义→整批预热+逐单独立事务+单条失败不中断。 --- -## 9. 状态机汇总(2026-10-14) +## 9. 状态机汇总(2026-10-31) | 表.字段 | 状态集 | 迁移 | 写入方 | |---|---|---|---| | `blade_formal_settlement.kingdee_sync_status` | unsynced / synced / **failed**(★新增写入口) | unsynced→(A102-2/A302-2 成功)→synced;推送异常→failed(独立事务);failed 可手动/定时重推、A302-4 勾稽纠偏回 synced;金蝶删单回调→voided + unsynced 还原 | FormalSettlementServiceImpl | | `blade_payment_application.kingdee_status` | unsynced / synced / paying / paid(终) / closed(终) / failed | 同前版映射;failed 可纠偏回 synced;**连续 2 轮查不到→消失解锁置 failed**;金蝶删单回调→failed+清单号(不作废源单) | PaymentApplicationServiceImpl + 定时 | | `blade_invoice_application.kingdee_status` | unsynced / synced / invoiced(终) / **failed**(★新增写入口) | unsynced→(A303-1 全部张次成功)→synced→(A303-5 新发票)→invoiced;推送异常→failed;金蝶删单回调→作废还原 | InvoiceApplicationServiceImpl + 定时 | -| `blade_invoice_receipt.kingdee_push_status` | unsynced / pushed / assigned(终) / failed | 同前版;A801-3 回写匹配键已修复为登记 id(异常回收/补偿/补登生效) | InvoiceReceiptServiceImpl + 定时 | -| `blade_customer_archive.kingdee_status` | none / synced / failed / exist(终) | 同前版;提审新增 A801-5 黑名单拦截(不产生状态) | CustomerArchiveServiceImpl + 定时 | +| `blade_invoice_receipt.kingdee_push_status` | unsynced / pushed / assigned(终) / failed | **★先票后款语义变更**:unsynced→(A303-2 成功)→pushed(**收票推送终点**);pushed→(付款结算单侧 A302-6 成功:应付推送后逐单指定/发票登记确认/手动端点/A801-3 指定补偿)→assigned;failed(A303-2 失败 / A801-3 发票状态异常回收 / 指定失败回收 / 前置拦截);pushed/assigned 禁止本地作废 | InvoiceReceiptServiceImpl + FormalSettlementServiceImpl + 定时 | +| `blade_customer_archive.kingdee_status` | none / synced / failed / exist(终) | 同前版;提审新增 A801-5 黑名单拦截(不产生状态);新增 `supplier_code` 字段(A901-1 查询回填,不产生状态) | CustomerArchiveServiceImpl + 定时 | | `blade_kingdee_bill_lock.lock_status` ★新增 | failed / locked / unlocked | 先落 failed 占位→lockBusiData 成功→locked;补偿重试(try_count<5)或金蝶解锁/删单回调→unlocked | KingdeeBillLockServiceImpl + 定时 + 手动端点 | -| `blade_receipt_claim.kingdee_bill_status` | none / pushed / approved / received(终) / failed / voided | none→(A301-3)→pushed→(A301-1: C)→approved→(D)→received;已推送禁止作废 | ReceiptFlowServiceImpl + ReceiptClaimRecordServiceImpl | -| `blade_kingdee_receipt_flow.claim_status` | unclaimed / partial / claimed | 认领分摊→partial/claimed;全部作废→unclaimed;公告同步不覆盖 | ReceiptFlowServiceImpl | +| `blade_receipt_claim.kingdee_bill_no/kingdee_bill_status` | pushed / approved / received / failed | **★语义变更**:认领时从流水冗余快照(A301-1 同步值),认领不再推送金蝶、状态不再推进,仅随流水同步反映金蝶侧进展;作废→本地回退+推 C107-3 | ReceiptFlowServiceImpl + ReceiptClaimRecordServiceImpl | +| `blade_kingdee_receipt_flow.claim_status` | unclaimed / partial / claimed | 认领分摊→partial/claimed(上限=收款金额);作废回退重算;A301-1 同步不覆盖本地认领字段 | ReceiptFlowServiceImpl | +| `blade_kingdee_receipt_flow.kingdee_bill_status` | pushed / approved / received / failed | 随 A301-1 手动同步刷新:A/B→pushed、C→approved、D→received、G 退单/E 变更中/其它→failed | ReceiptFlowServiceImpl(sync) | +| `blade_receipt_claim_hcs_record.push_status` ★新增 | success / failed / voided | 认领事务提交后推 C107-1:成功落合规 fdId;失败落标记(无自动重试);作废推 C107-3 成功后置 voided | HcsClaimSyncSupport | --- ## 10. 定时任务与配置开关汇总 -全部在 `blade-transport/task/`,Spring `@Scheduled`,统一 single-flight(`KingdeeSyncLockSupport`,`blade.lock.enabled=false` 降级直跑)。 +全部在 `blade-transport/task/`,Spring `@Scheduled`,统一 single-flight(`KingdeeSyncLockSupport`,`blade.lock.enabled=false` 降级直跑)。**收款认领推送任务 `ReceiptClaimPushTask` 已随 A301-3 下线删除(10-29)。** | 任务类 | 方法 | 覆盖业务(接口) | cron 键(默认) | 开关键(代码默认) | 手动端点 | |---|---|---|---|---|---| -| PaymentKingdeeResultTask | syncKingdeePaymentResult | ① A103-2/A103-4 付款结果 ② A302-4 应收勾稽 ③ A303-5 开票结果 | `thirdParty.bfi.payResult.cron`(`0 */10 * * * ?`) | `thirdParty.bfi.payResult.syncEnabled`(false;**Nacos 样例 dev/prod 已显式 true**) | `/payment-application/sync-kingdee-result`、`/formal-settlement/sync-kingdee-ar-result`、`/invoice-application/sync-kingdee-invoice-result` | -| PaymentKingdeeResultTask | syncKingdeeReceivedInvoiceResult | A801-3 收票结果回写 | `thirdParty.bfi.receivedInvoiceResult.cron`(每10分钟) | `…receivedInvoiceResult.syncEnabled`(false) | `/invoice-receipt/sync-kingdee-result` | -| CustomerKingdeeSyncTask | syncKingdeeCustomerData | A902-1 复查 + A902-2 重推 + 金蝶停用联动 | `thirdParty.bfi.customer.cron`(`0 0 2 * * ?`) | `…customer.syncEnabled`(false;KS000067 残留已移除) | `/customer-archive/sync-kingdee` | -| **ReceiptClaimPushTask** ★新增 | pushClaimToKingdee | A301-3 认领推送(按流水归集,前置重查 A301-2) | `thirdParty.bfi.receiptClaimPush.cron`(每10分钟) | `…receiptClaimPush.syncEnabled`(false) | `/receipt-flow/push-kingdee` | -| **InvoiceReceiptKingdeeSyncTask** ★新增 | syncKingdeeInvoicePool | A801-1 全票池瘦身同步 + 绑定回填 | `thirdParty.bfi.invoiceReceipt.cron`(每30分钟) | `…invoiceReceipt.syncEnabled`(false);增量窗口 `…incrementDays`(30);票种白名单 `…invoiceTypes` | `/invoice-receipt/sync-invoice-pool` | -| **KingdeeAutoSyncRetryTask** ★新增 | retryKingdeeFailed | 4 域 failed 单据重推(结算/付款/开票/收票;不扫 unsynced;无次数上限,靠幂等预检防重) | `thirdParty.bfi.autoSync.retryCron`(每10分钟) | `…autoSync.retryEnabled`(false) | 各域 `/sync-kingdee` | -| **KingdeeBillLockRetryTask** ★新增 | retryFailedBillLocks | 锁单失败补偿(failed 且 try_count<5 重调 lockBusiData) | `thirdParty.bfi.billLock.retryCron`(每5分钟) | `…billLock.retryEnabled`(false) | `/formal-settlement/retry-kingdee-bill-lock` | +| PaymentKingdeeResultTask | syncKingdeePaymentResult | ① A103-2/A103-4 付款结果 ② A302-4 应收勾稽 ③ A303-5 开票结果 | `thirdParty.bfi.payResult.cron`(`0 1/10 * * * ?`) | `thirdParty.bfi.payResult.sync-enabled`(false;**Nacos 样例 dev/prod 已显式 true**) | `/payment-application/sync-kingdee-result`、`/formal-settlement/sync-kingdee-ar-result`、`/invoice-application/sync-kingdee-invoice-result` | +| PaymentKingdeeResultTask | syncKingdeeReceivedInvoiceResult | A801-3 收票结果回写(异常回收/**pushed+已认领补指定**/单号补登) | `thirdParty.bfi.receivedInvoiceResult.cron`(`0 8/10 * * * ?`,与付款回写独立调度) | `thirdParty.bfi.receivedInvoiceResult.sync-enabled`(false) | `/invoice-receipt/sync-kingdee-result` | +| CustomerKingdeeSyncTask | syncKingdeeCustomerData | A902-1 复查 + A902-2 重推 + 金蝶停用联动 | `thirdParty.bfi.customer.cron`(`0 0 2 * * ?`) | `thirdParty.bfi.customer.sync-enabled`(false) | `/customer-archive/sync-kingdee` | +| InvoiceReceiptKingdeeSyncTask | syncKingdeeInvoicePool | A801-1 全票池瘦身同步(含明细 items_json)+ 绑定回填 | `thirdParty.bfi.invoiceReceipt.cron`(`0 16/30 * * * ?`) | `thirdParty.bfi.invoiceReceipt.sync-enabled`(false);增量窗口 `…incrementDays`(30);票种白名单 `…invoiceTypes` | `/invoice-receipt/sync-invoice-pool` | +| KingdeeAutoSyncRetryTask | retryKingdeeFailed | 4 域 failed 单据重推(结算/付款/开票/收票;不扫 unsynced;无次数上限,靠幂等预检防重);收票重推不再依赖 A102-4 | `thirdParty.bfi.autoSync.retryCron`(`0 5/10 * * * ?`) | `thirdParty.bfi.autoSync.retry-enabled`(false) | 各域 `/sync-kingdee` | +| KingdeeBillLockRetryTask | retryFailedBillLocks | 锁单失败补偿(failed 且 try_count<5 重调 lockBusiData) | `thirdParty.bfi.billLock.retryCron`(`0 2/5 * * * ?`) | `thirdParty.bfi.billLock.retry-enabled`(false) | `/formal-settlement/retry-kingdee-bill-lock` | **审批自动同步开关(监听器,非定时)**:`thirdParty.bfi.autoSync.settlement / payment / invoiceApply / receipt`(默认全 false)。 -**其他业务键**:`thirdParty.bfi.claimAnnounce.claimusers`(A301-2 认领人,**必配**)、`…claimAnnounce.months`(6)、`thirdparty.bfi.blacklist.blockRiskWarning`(false)、`thirdparty.bfi.material.*`(物料映射)。 +**其他业务键**:`thirdParty.bfi.recBillSync.days`(★新增,A301-1 同步窗口天数,默认 60)、`thirdparty.bfi.blacklist.blockRiskWarning`(false)、`thirdparty.bfi.material.*`(物料映射)。 +**已删除配置**:`thirdParty.bfi.claimAnnounce.*`(A301-2 认领人/窗口)、`thirdParty.bfi.receiptClaimPush.*`(认领推送定时)——代码已不再读取,Nacos 中可清理。 -> ⚠️ Nacos 样例(`doc/nacos/`)仅显式配置了 `payResult.syncEnabled=true`;`receiptClaimPush/invoiceReceipt` 样例仍为 false;`customer/receivedInvoiceResult/autoSync/billLock` 键未出现在样例中——生效值取决于各环境实际 Nacos,缺省走代码 false。**开启生产定时前逐项核对第 12 节风险清单。** +> ⚠️ Nacos 样例(`doc/nacos/`)仅显式配置了 `payResult.sync-enabled=true`;`customer/receivedInvoiceResult/autoSync/billLock/invoiceReceipt` 键未出现在样例中——生效值取决于各环境实际 Nacos,缺省走代码 false。**开启生产定时前逐项核对第 12 节风险清单。** --- @@ -540,7 +631,7 @@ iPaaS 网关(/gwzh/BFI,不直连金蝶) |---|---|---| | 1 | 部门金蝶组织编码 | 部门档案维护 `kingdee_dept_code`;未维护的部门推送组织取数会直接拦截 | | 2 | 主数据 SQL | 执行 `doc/sql/transport/kingdee_master_data_init_20260924.sql`(计量单位档案等);认领链路执行 `blade_receipt_flow_claim_20260924.sql`;10-13 迁移脚本见 `doc/sql/transport/lqj/` | -| 3 | Nacos 业务配置 | `thirdParty.bfi.claimAnnounce.claimusers`(认领人,与金蝶公告 claimusers 一致)、`thirdParty.bfi.invoiceReceipt.invoiceTypes`(票池同步票种白名单)、`thirdparty.bfi.material.*`(物料映射,缺省有默认值但建议显式配置) | +| 3 | Nacos 业务配置 | `thirdParty.bfi.recBillSync.days`(A301-1 同步窗口天数,默认 60,可选)、`thirdParty.bfi.invoiceReceipt.invoiceTypes`(票池同步票种白名单)、`thirdparty.bfi.material.*`(物料映射,缺省有默认值但建议显式配置) | | 4 | 定时开关按需开启 | 见第 10 节表:付款结果回写样例已开;其余按环境显式开启 `syncEnabled/retryEnabled` | | 5 | 审批自动同步开关 | `thirdParty.bfi.autoSync.settlement/payment/invoiceApply/receipt` 按域开启;未开启时走页面「同步金蝶」手动推送 | | 6 | 客商收款账户 | 维护 `blade_customer_receipt_account` 联行号(cnaps_code)——付款申请/预付款/客商推送均取真实联行号,缺失拦截 | @@ -570,17 +661,22 @@ iPaaS 网关(/gwzh/BFI,不直连金蝶) → 等 A303-5 定时回写发票结果(或手动端点)→ 状态 invoiced → 正式结算单详情「开票发票」区查看/下载发票文件 -⑤ 收票(进项) - 收票登记表单:发票号放大镜从金蝶票池选票(镜像读,票号未命中自动回源)→ 保存(重查闸门校验可用性) - → 分摊到一张审批通过应付结算单 → 审批通过(autoSync.receipt 开启时自动推送;或「批量同步」) - → A303-2 保存→pushed→A302-6 指定应付→assigned(终态) - → 等 A801-3 定时回写(pushed 断点自动补偿重发 A302-6) +⑤ 收票(进项·先票后款两段式) + 【先票】收票登记表单:发票号放大镜从金蝶票池选票(镜像读,票号未命中自动回源)→ 表单带出发票明细 + → 手选结算组织(合同承办部门)→ 保存(重查闸门校验可用性;销方供应商代码自动回填) + → 提交审批 → 审批通过(autoSync.receipt 开启时自动推送;或「批量同步」) + → A303-2 保存(往来户=销方供应商代码/币别 CNY/分录=发票明细行)→ pushed(收票推送终点) -⑥ 收款认领 - 收款流水页「流水同步」拉取金蝶认领公告 → 流水详情/批量认领:选审批通过应收结算单按比例分摊(本地落库) - → 「推送金蝶」(行内手动,或等 ReceiptClaimPushTask 定时)→ 金蝶生成收款单 - → 认领记录页「查询金蝶认领结果」回写真实收款单号与状态(approved/received) - 注意:已推送金蝶的认领不可作废,需金蝶侧人工处置 + 【后款】付款结算单(应付)编辑或「发票登记」:选择收票单(候选=审批通过+未认领)→ 全额认领 + → 结算单审批通过 → A102-2 应付单推送成功 → 逐单 A302-6 指定应付 → 收票单 assigned(终态) + → 卡在 pushed 不用手动处理:被认领后由 A801-3 定时补偿自动补发指定 + +⑥ 收款流水与认领(认领本地化,不推金蝶) + 收款流水页「手动同步流水」拉取金蝶收款单(A301-1,窗口默认 60 天;前置组织金蝶编码) + → 流水详情/批量认领:选审批通过应收结算单填分摊金额(上限=收款金额,本地落库) + → 认领事务提交后自动推合同合规(C103-9→C107-1→C108-1,失败只落标记) + → 需撤销时认领记录页「作废」:本地回退金额 + 自动同步合规 C107-3 + 注意:金蝶侧认领/收款处理由财务在金蝶直接操作,TMS 只做本地认领登记与合规同步 ⑦ 查看附件 金蝶票池/认领发票/收票附件的「查看/下载」统一走后端 A801-2 代理(PDF 版式优先) @@ -597,13 +693,19 @@ iPaaS 网关(/gwzh/BFI,不直连金蝶) | 付款单在金蝶被删且无回调 | A103-2 连续 2 轮(约 20 分钟)查不到 | 自动消失解锁置 failed,按 failed 重推 | | 断链重推(远端已落单本地未同步) | 重推时预检命中 | 自动幂等回填单号跳过重复推送,无需人工清库 | | 收票单发票状态异常 | 定时回写发现红冲/作废 → failed「已回收推送状态」 | 人工在金蝶侧处理后重新登记/推送 | +| 收票推送报「供应商代码/往来户」类错误 | 销方名称未匹配到客商档案 `supplier_code`(A901-1 未回填) | 先完成客商建档/金蝶同步(A901-1 回填供应商编码),或用 `GET /customer-archive/query-supplier-code` 核对后重推 | +| 收票推送报免税/零税率行拦截 | `taxrateid_number` 无法映射(0 税率/免税行) | 人工核实票面,与金蝶约定编码后放开(有意拦截) | +| 收票推送报结算组织/平台公司类错误 | 手选部门沿部门树上溯不到带金蝶组织编码的平台公司 | 管理员维护组织档案金蝶编码后重推 | +| 收票单卡在 pushed 未变 assigned | 未被付款结算单认领,或认领它的结算单应付单未推送成功 | 在付款结算单做「发票登记」/编辑认领收票单,并确认该结算单已 synced;A801-3 补偿只处理「已认领」的 pushed 单 | +| 认领收票单被拦「一张收票单只能被一张付款结算单认领」 | 该收票登记已被其他有效应付结算单认领 | 先在原结算单移除认领(金蝶侧人工解除指定)再认领 | | 票池选不到票 | 镜像无数据 | 检查全票池同步开关与 `invoiceTypes` 白名单;或 `POST /invoice-receipt/sync-invoice-pool` 手动同步;票号精确查会自动回源 | -| 收款流水同步为空 | A301-2 查不到公告 | 检查 Nacos `claimAnnounce.claimusers` 是否与金蝶公告认领人一致 | +| 收款流水同步为空/报组织编码错误 | 组织管理未维护「平台公司」金蝶编码,或窗口内无数据 | 维护组织金蝶编码后重试;需要更早数据时调大 `thirdParty.bfi.recBillSync.days`(支持热更新) | +| 认领/作废合规推送失败 | C103-9/C107-1/C108-1/C107-3 返回异常 | 查 `blade_receipt_claim_hcs_record` 失败原因,人工在合规侧补录(无自动重试);作废主流程不受影响 | | 定时任务不跑/重复跑 | — | 核对 Nacos 开关键名(第 10 节);single-flight 锁保证多副本/手动不并发,`blade.lock.enabled=false` 时无锁 | --- -## 12. 风险与待确认清单(2026-10-14) +## 12. 风险与待确认清单(2026-10-31) ### 12.1 已关闭(旧总纲遗留,当前代码已核实修复) @@ -615,27 +717,35 @@ iPaaS 网关(/gwzh/BFI,不直连金蝶) | 客商推送 bankNumber 硬编码 / A103-3 e_bebank_number 取名称字段 | ✅ 均改取客商收款账户联行号 | | 结算/开票失败不落 failed | ✅ 统一落 failed(03b34310) | | 结算单认领无保存重查闸门 | ✅ 已实现 assertInvoicesClaimable | +| 收票推送依赖「恰好分摊 1 张应付结算单」+ A102-4 预检 | ✅ 已随先票后款改造删除(10-31,收票登记不再挂结算单) | +| 收款认领推送金蝶(A301-3 定时+手动) | ✅ 链路整体下线(10-29),认领只登记本地+推合同合规 | -### 12.2 仍需关注 / 联调确认 +### 12.2 仍需关注 / 联调确认(2026-10-31) | # | 事项 | 证据/说明 | |---|---|---| -| 1 | **A103-1 勾稽键口径**:代码传本地组装的应付防重键,`PaymentApplicationServiceImpl:1431-1443` 注释仍写「A102-4 反查 awrp_entry_code_p、禁止自行拼接」——注释与代码矛盾,金蝶侧付款计划分录标识是否等于防重键需联调实测 | 高优先 | -| 2 | **项目编码 999 残留**:全部推送链路(应付/应收/付款/预付款/开票申请)仍传测试值 999;A103-3「取项目档案真实编码」的逻辑已注释停用(详见入参映射表 14-#2) | 上线前处理 | -| 3 | **计量单位不送**:分录 DTO 有字段无赋值——需与金蝶确认不送计量单位是否影响单据校验 | 联调确认 | -| 4 | **A301-3 字段待金蝶确认**:`recpaytype_number`(101)/`paymenttype`/`recpayer` 取值口径 | 联调确认 | -| 5 | **A301-2 claimusers 未配置风险**:缺省空串调 A301-2 查不到数据(无报错),需在 Nacos 必配 | 配置项 | -| 6 | **其他链路不校验客商 kingdee_status**:客商未推送/未审核也会带编码推单(软依赖) | 既有风险 | -| 7 | **正式结算单作废未接 checkEditable**:仅靠前端锁状态标签禁用,接口层可绕过 | 建议补拦截 | -| 8 | **failed 重推无次数上限**:`KingdeeAutoSyncRetryTask` 每 10 分钟扫全量 failed,持续失败的单据会反复调金蝶(靠预检防重但不封顶) | 观察线上量 | -| 9 | **付款申请前端单笔「同步金蝶」按钮未绑定**(`handleSyncRow` 死代码):单笔入口仅剩审批自动同步 | 前端待补 | -| 10 | **旅客运输/代收车船税单据体**:DTO 已建未接(仅 freights 接入) | 按需迭代 | -| 11 | **方案 vs 代码差异(沿旧总纲)**:付款多次下推未实现(单分录单次);附件已落地(A902 4 链路)✅;收款流水同步已有定时(receiptClaimPush)✅;黑名单 A801-5 已实现 ✅ | — | +| 1 | **A103-1 勾稽键口径**:代码传本地组装的应付防重键,`PaymentApplicationServiceImpl` 注释仍写「A102-4 反查 awrp_entry_code_p、禁止自行拼接」——注释与代码矛盾,金蝶侧付款计划分录标识是否等于防重键需联调实测 | 高优先 | +| 2 | **项目编码 999 残留**:应付/应收/付款/预付款/开票申请链路仍传测试值 999(收票 A303-2 无项目字段) | 上线前处理 | +| 3 | **应付/应收计量单位不送**:分录 DTO 有字段无赋值——需与金蝶确认;收票 A303-2 明细行送镜像数量/单价 | 联调确认 | +| 4 | **先票后款免税/零税率行**:`taxrateid_number` 无法映射,推送时报错人工核实(如业务出现需与金蝶约定编码后放开) | 有意拦截 | +| 5 | **先票后款币别固定 CNY**:收票登记不挂合同后 `currency_number` 固定 CNY,出现外币进项票需补充取值来源 | 上线前确认 | +| 6 | **A303-2 明细时点**:分录取镜像最新值,登记→推送间金蝶侧修正明细行会被跟随;红冲/作废由回写回收兜底 | 联调观察 | +| 7 | **收票反向操作不同步金蝶**:结算单作废/认领移除不调 A302-6 解除指定,需金蝶侧人工解除后重推(已拍板) | 操作约定 | +| 8 | **销方供应商代码依赖客商档案**:`queryCustomerCode` 只按销方名称匹配本地 `blade_customer_archive.supplier_code`(A901-1 建档/同步时落库),不实时调金蝶;客商档案缺数据会推送拦截 | 数据前提 | +| 9 | **A301-1 明细流水号同源性**:`bei_detailid` 须与原 A301-2 `detailid`、合规侧 `fdDetailSerialNumber` 同值,否则 C104-2 定位失败 → C107-1 跳过(需联调真实报文核对) | 高优先 | +| 10 | **合规履约计划金额不回退**:认领 C103-9 累加,作废仅同步 C107-3,不回退履约计划行金额(与改造前口径一致) | 既有口径 | +| 11 | **其他链路不校验客商 kingdee_status**:客商未推送/未审核也会带编码推单(软依赖) | 既有风险 | +| 12 | **正式结算单作废未接 checkEditable**:仅靠前端锁状态标签禁用,接口层可绕过 | 建议补拦截 | +| 13 | **failed 重推无次数上限**:`KingdeeAutoSyncRetryTask` 每 10 分钟扫全量 failed,持续失败的单据会反复调金蝶(靠预检防重但不封顶) | 观察线上量 | +| 14 | **付款申请前端单笔「同步金蝶」按钮未绑定**(`handleSyncRow` 死代码):单笔入口仅剩审批自动同步 | 前端待补 | +| 15 | **旅客运输/代收车船税单据体**:DTO 已建未接(仅 freights 接入) | 按需迭代 | --- ## 附:文档关系 -- 本文为新总纲(基线 2026-10-13 `805ca9d3`,2026-10-15 增补),替代 09-27 版;单链路细节仍以 `doc/docs/业务流程/` 对应专项文档为深入参考,冲突处以本文 + 当前代码为准。 +- 本文为新总纲(基线 2026-10-31 `933fb37d`,分支 feature/20261031-bfi-收票收款改造),替代 09-27 版;单链路细节仍以 `doc/docs/业务流程/` 对应专项文档为深入参考,冲突处以本文 + 当前代码为准。 - **各推送接口的逐字段入参取值映射(页面字段→表字段/配置对照)见《2026-10-15-金蝶(星瀚)BFI推送入参取值映射表(页面字段-表字段-配置对照).md》**,其第 14 节含与本文记载不一致的代码级核对结论。 -- 09-27 版中被本文修订的关键结论:A801-3 回写缺陷、票池「实时回源无定时」架构、收款认领「全手动无定时」、分录调试覆盖、物料映射常量、failed 落库口径、A801-5/A801-2/A902「未实现」状态。 +- **收票先票后款专项**:《2026-10-31-先票后款改造方案V2(收票登记A303-2+付款结算单A102-2+A302-6指定应付·实现流程与逻辑)》。 +- **收款流水改造专项**:《2026-10-29-收款流水菜单改造(数据源A301-2改A301-1-认领只登记本地并推送合规C107-1与C107-3作废同步)》《2026-10-29-收款流水认领操作流程(收款金额本地额度·单条批量与作废)》《2026-10-16-收款流水多次认领与作废口径梳理》。 +- 09-27/10-14 版中被本文修订的关键结论:**A301-2/A301-3 链路已下线(10-14 版第 6 节口径作废)**、收票推送终点 pushed + A302-6 后置(10-14 版「同一次推送内串行」作废)、A901-1/A905-3 已实现(10-14 版「未实现」作废)、HCS 付款履约计划推送时点改为金蝶实付后。 diff --git a/doc/docs/业务流程/2026-10-14-金蝶(星瀚)BFI对接操作手册(纯操作版).assets/03-正式结算单-同步金蝶.png b/doc/docs/业务流程/2026-10-14-金蝶(星瀚)BFI对接操作手册(纯操作版).assets/03-正式结算单-同步金蝶.png deleted file mode 100644 index 0b7c3764..00000000 Binary files a/doc/docs/业务流程/2026-10-14-金蝶(星瀚)BFI对接操作手册(纯操作版).assets/03-正式结算单-同步金蝶.png and /dev/null differ diff --git a/doc/docs/业务流程/2026-10-14-金蝶(星瀚)BFI对接操作手册(纯操作版).assets/04-付款管理-批量同步.png b/doc/docs/业务流程/2026-10-14-金蝶(星瀚)BFI对接操作手册(纯操作版).assets/04-付款管理-批量同步.png index ff938893..2e44fa8f 100644 Binary files a/doc/docs/业务流程/2026-10-14-金蝶(星瀚)BFI对接操作手册(纯操作版).assets/04-付款管理-批量同步.png and b/doc/docs/业务流程/2026-10-14-金蝶(星瀚)BFI对接操作手册(纯操作版).assets/04-付款管理-批量同步.png differ diff --git a/doc/docs/业务流程/2026-10-14-金蝶(星瀚)BFI对接操作手册(纯操作版).assets/08-收款流水-同步与推送.png b/doc/docs/业务流程/2026-10-14-金蝶(星瀚)BFI对接操作手册(纯操作版).assets/08-收款流水-同步与推送.png deleted file mode 100644 index 80280656..00000000 Binary files a/doc/docs/业务流程/2026-10-14-金蝶(星瀚)BFI对接操作手册(纯操作版).assets/08-收款流水-同步与推送.png and /dev/null differ diff --git a/doc/docs/业务流程/2026-10-14-金蝶(星瀚)BFI对接操作手册(纯操作版).assets/08-收款流水-手动同步流水.png b/doc/docs/业务流程/2026-10-14-金蝶(星瀚)BFI对接操作手册(纯操作版).assets/08-收款流水-手动同步流水.png new file mode 100644 index 00000000..c663dc22 Binary files /dev/null and b/doc/docs/业务流程/2026-10-14-金蝶(星瀚)BFI对接操作手册(纯操作版).assets/08-收款流水-手动同步流水.png differ diff --git a/doc/docs/业务流程/2026-10-14-金蝶(星瀚)BFI对接操作手册(纯操作版).assets/09-认领记录-列表.png b/doc/docs/业务流程/2026-10-14-金蝶(星瀚)BFI对接操作手册(纯操作版).assets/09-认领记录-列表.png new file mode 100644 index 00000000..1b51c52e Binary files /dev/null and b/doc/docs/业务流程/2026-10-14-金蝶(星瀚)BFI对接操作手册(纯操作版).assets/09-认领记录-列表.png differ diff --git a/doc/docs/业务流程/2026-10-14-金蝶(星瀚)BFI对接操作手册(纯操作版).assets/09-认领记录-查询金蝶认领结果.png b/doc/docs/业务流程/2026-10-14-金蝶(星瀚)BFI对接操作手册(纯操作版).assets/09-认领记录-查询金蝶认领结果.png deleted file mode 100644 index 60b481e2..00000000 Binary files a/doc/docs/业务流程/2026-10-14-金蝶(星瀚)BFI对接操作手册(纯操作版).assets/09-认领记录-查询金蝶认领结果.png and /dev/null differ diff --git a/doc/docs/业务流程/2026-10-14-金蝶(星瀚)BFI对接操作手册(纯操作版).assets/11-预结算单列表.png b/doc/docs/业务流程/2026-10-14-金蝶(星瀚)BFI对接操作手册(纯操作版).assets/11-预结算单列表.png deleted file mode 100644 index 5809ec06..00000000 Binary files a/doc/docs/业务流程/2026-10-14-金蝶(星瀚)BFI对接操作手册(纯操作版).assets/11-预结算单列表.png and /dev/null differ diff --git a/doc/docs/业务流程/2026-10-14-金蝶(星瀚)BFI对接操作手册(纯操作版).assets/13-收票登记-先票后款表单.png b/doc/docs/业务流程/2026-10-14-金蝶(星瀚)BFI对接操作手册(纯操作版).assets/13-收票登记-先票后款表单.png new file mode 100644 index 00000000..157b45d5 Binary files /dev/null and b/doc/docs/业务流程/2026-10-14-金蝶(星瀚)BFI对接操作手册(纯操作版).assets/13-收票登记-先票后款表单.png differ diff --git a/doc/docs/业务流程/2026-10-14-金蝶(星瀚)BFI对接操作手册(纯操作版).assets/15-客商档案-供应商编码.png b/doc/docs/业务流程/2026-10-14-金蝶(星瀚)BFI对接操作手册(纯操作版).assets/15-客商档案-供应商编码.png new file mode 100644 index 00000000..c231ebf1 Binary files /dev/null and b/doc/docs/业务流程/2026-10-14-金蝶(星瀚)BFI对接操作手册(纯操作版).assets/15-客商档案-供应商编码.png differ diff --git a/doc/docs/业务流程/2026-10-14-金蝶(星瀚)BFI对接操作手册(纯操作版).assets/16-付款结算单列表-发票登记与已指定应付列.png b/doc/docs/业务流程/2026-10-14-金蝶(星瀚)BFI对接操作手册(纯操作版).assets/16-付款结算单列表-发票登记与已指定应付列.png new file mode 100644 index 00000000..3f09ccf6 Binary files /dev/null and b/doc/docs/业务流程/2026-10-14-金蝶(星瀚)BFI对接操作手册(纯操作版).assets/16-付款结算单列表-发票登记与已指定应付列.png differ diff --git a/doc/docs/业务流程/2026-10-14-金蝶(星瀚)BFI对接操作手册(纯操作版).assets/16b-付款结算单列表-指定失败原因悬浮.png b/doc/docs/业务流程/2026-10-14-金蝶(星瀚)BFI对接操作手册(纯操作版).assets/16b-付款结算单列表-指定失败原因悬浮.png new file mode 100644 index 00000000..fe3a1768 Binary files /dev/null and b/doc/docs/业务流程/2026-10-14-金蝶(星瀚)BFI对接操作手册(纯操作版).assets/16b-付款结算单列表-指定失败原因悬浮.png differ diff --git a/doc/docs/业务流程/2026-10-14-金蝶(星瀚)BFI对接操作手册(纯操作版).assets/17-付款结算单-发票登记弹窗.png b/doc/docs/业务流程/2026-10-14-金蝶(星瀚)BFI对接操作手册(纯操作版).assets/17-付款结算单-发票登记弹窗.png new file mode 100644 index 00000000..06d117b0 Binary files /dev/null and b/doc/docs/业务流程/2026-10-14-金蝶(星瀚)BFI对接操作手册(纯操作版).assets/17-付款结算单-发票登记弹窗.png differ diff --git a/doc/docs/业务流程/2026-10-14-金蝶(星瀚)BFI对接操作手册(纯操作版).assets/18-选择收票单弹窗-分页候选.png b/doc/docs/业务流程/2026-10-14-金蝶(星瀚)BFI对接操作手册(纯操作版).assets/18-选择收票单弹窗-分页候选.png new file mode 100644 index 00000000..e977143b Binary files /dev/null and b/doc/docs/业务流程/2026-10-14-金蝶(星瀚)BFI对接操作手册(纯操作版).assets/18-选择收票单弹窗-分页候选.png differ diff --git a/doc/docs/业务流程/2026-10-14-金蝶(星瀚)BFI对接操作手册(纯操作版).assets/19-查看页发票信息-指定应付状态与重新推送.png b/doc/docs/业务流程/2026-10-14-金蝶(星瀚)BFI对接操作手册(纯操作版).assets/19-查看页发票信息-指定应付状态与重新推送.png new file mode 100644 index 00000000..43593740 Binary files /dev/null and b/doc/docs/业务流程/2026-10-14-金蝶(星瀚)BFI对接操作手册(纯操作版).assets/19-查看页发票信息-指定应付状态与重新推送.png differ diff --git a/doc/docs/业务流程/2026-10-14-金蝶(星瀚)BFI对接操作手册(纯操作版).assets/20-付款管理-指定失败原因悬浮.png b/doc/docs/业务流程/2026-10-14-金蝶(星瀚)BFI对接操作手册(纯操作版).assets/20-付款管理-指定失败原因悬浮.png new file mode 100644 index 00000000..c1ea0b07 Binary files /dev/null and b/doc/docs/业务流程/2026-10-14-金蝶(星瀚)BFI对接操作手册(纯操作版).assets/20-付款管理-指定失败原因悬浮.png differ diff --git a/doc/docs/业务流程/2026-10-14-金蝶(星瀚)BFI对接操作手册(纯操作版).assets/21-结算调整单列表-各状态操作入口.png b/doc/docs/业务流程/2026-10-14-金蝶(星瀚)BFI对接操作手册(纯操作版).assets/21-结算调整单列表-各状态操作入口.png new file mode 100644 index 00000000..90e1fb45 Binary files /dev/null and b/doc/docs/业务流程/2026-10-14-金蝶(星瀚)BFI对接操作手册(纯操作版).assets/21-结算调整单列表-各状态操作入口.png differ diff --git a/doc/docs/业务流程/2026-10-14-金蝶(星瀚)BFI对接操作手册(纯操作版).assets/22-结算调整单新增-选单回带与费用录入.png b/doc/docs/业务流程/2026-10-14-金蝶(星瀚)BFI对接操作手册(纯操作版).assets/22-结算调整单新增-选单回带与费用录入.png new file mode 100644 index 00000000..550f31a2 Binary files /dev/null and b/doc/docs/业务流程/2026-10-14-金蝶(星瀚)BFI对接操作手册(纯操作版).assets/22-结算调整单新增-选单回带与费用录入.png differ diff --git a/doc/docs/业务流程/2026-10-14-金蝶(星瀚)BFI对接操作手册(纯操作版).assets/23-结算调整单新增-金蝶冲销预检提示.png b/doc/docs/业务流程/2026-10-14-金蝶(星瀚)BFI对接操作手册(纯操作版).assets/23-结算调整单新增-金蝶冲销预检提示.png new file mode 100644 index 00000000..deda7005 Binary files /dev/null and b/doc/docs/业务流程/2026-10-14-金蝶(星瀚)BFI对接操作手册(纯操作版).assets/23-结算调整单新增-金蝶冲销预检提示.png differ diff --git a/doc/docs/业务流程/2026-10-14-金蝶(星瀚)BFI对接操作手册(纯操作版).assets/24-结算调整单重新推送-高危二次确认.png b/doc/docs/业务流程/2026-10-14-金蝶(星瀚)BFI对接操作手册(纯操作版).assets/24-结算调整单重新推送-高危二次确认.png new file mode 100644 index 00000000..461ac6c2 Binary files /dev/null and b/doc/docs/业务流程/2026-10-14-金蝶(星瀚)BFI对接操作手册(纯操作版).assets/24-结算调整单重新推送-高危二次确认.png differ diff --git a/doc/docs/业务流程/2026-10-14-金蝶(星瀚)BFI对接操作手册(纯操作版).assets/25-结算调整单审批通过二次确认.png b/doc/docs/业务流程/2026-10-14-金蝶(星瀚)BFI对接操作手册(纯操作版).assets/25-结算调整单审批通过二次确认.png new file mode 100644 index 00000000..6d94b468 Binary files /dev/null and b/doc/docs/业务流程/2026-10-14-金蝶(星瀚)BFI对接操作手册(纯操作版).assets/25-结算调整单审批通过二次确认.png differ diff --git a/doc/docs/业务流程/2026-10-14-金蝶(星瀚)BFI对接操作手册(纯操作版).assets/26-结算调整单金蝶生成失败原因悬浮.png b/doc/docs/业务流程/2026-10-14-金蝶(星瀚)BFI对接操作手册(纯操作版).assets/26-结算调整单金蝶生成失败原因悬浮.png new file mode 100644 index 00000000..22f02a7b Binary files /dev/null and b/doc/docs/业务流程/2026-10-14-金蝶(星瀚)BFI对接操作手册(纯操作版).assets/26-结算调整单金蝶生成失败原因悬浮.png differ diff --git a/doc/docs/业务流程/2026-10-14-金蝶(星瀚)BFI对接操作手册(纯操作版).assets/27-正式结算单列表-调整后单标识.png b/doc/docs/业务流程/2026-10-14-金蝶(星瀚)BFI对接操作手册(纯操作版).assets/27-正式结算单列表-调整后单标识.png new file mode 100644 index 00000000..6e6bdedb Binary files /dev/null and b/doc/docs/业务流程/2026-10-14-金蝶(星瀚)BFI对接操作手册(纯操作版).assets/27-正式结算单列表-调整后单标识.png differ diff --git a/doc/docs/业务流程/2026-10-14-金蝶(星瀚)BFI对接操作手册(纯操作版).assets/28-正式结算单列表-原单已调整标识.png b/doc/docs/业务流程/2026-10-14-金蝶(星瀚)BFI对接操作手册(纯操作版).assets/28-正式结算单列表-原单已调整标识.png new file mode 100644 index 00000000..b1dcb78f Binary files /dev/null and b/doc/docs/业务流程/2026-10-14-金蝶(星瀚)BFI对接操作手册(纯操作版).assets/28-正式结算单列表-原单已调整标识.png differ diff --git a/doc/docs/业务流程/2026-10-14-金蝶(星瀚)BFI对接操作手册(纯操作版).assets/31-预付结算单列表.png b/doc/docs/业务流程/2026-10-14-金蝶(星瀚)BFI对接操作手册(纯操作版).assets/31-预付结算单列表.png new file mode 100644 index 00000000..12fa37e3 Binary files /dev/null and b/doc/docs/业务流程/2026-10-14-金蝶(星瀚)BFI对接操作手册(纯操作版).assets/31-预付结算单列表.png differ diff --git a/doc/docs/业务流程/2026-10-14-金蝶(星瀚)BFI对接操作手册(纯操作版).assets/32-付款申请表单-进度预付.png b/doc/docs/业务流程/2026-10-14-金蝶(星瀚)BFI对接操作手册(纯操作版).assets/32-付款申请表单-进度预付.png new file mode 100644 index 00000000..cef411e6 Binary files /dev/null and b/doc/docs/业务流程/2026-10-14-金蝶(星瀚)BFI对接操作手册(纯操作版).assets/32-付款申请表单-进度预付.png differ diff --git a/doc/docs/业务流程/2026-10-14-金蝶(星瀚)BFI对接操作手册(纯操作版).assets/33-付款申请-选择预付结算单弹窗.png b/doc/docs/业务流程/2026-10-14-金蝶(星瀚)BFI对接操作手册(纯操作版).assets/33-付款申请-选择预付结算单弹窗.png new file mode 100644 index 00000000..d0d1b6cc Binary files /dev/null and b/doc/docs/业务流程/2026-10-14-金蝶(星瀚)BFI对接操作手册(纯操作版).assets/33-付款申请-选择预付结算单弹窗.png differ diff --git a/doc/docs/业务流程/2026-10-14-金蝶(星瀚)BFI对接操作手册(纯操作版).assets/34-付款申请表单-项目预付.png b/doc/docs/业务流程/2026-10-14-金蝶(星瀚)BFI对接操作手册(纯操作版).assets/34-付款申请表单-项目预付.png new file mode 100644 index 00000000..b231f3dc Binary files /dev/null and b/doc/docs/业务流程/2026-10-14-金蝶(星瀚)BFI对接操作手册(纯操作版).assets/34-付款申请表单-项目预付.png differ diff --git a/doc/docs/业务流程/2026-10-14-金蝶(星瀚)BFI对接操作手册(纯操作版).assets/35-项目预付核销分区.png b/doc/docs/业务流程/2026-10-14-金蝶(星瀚)BFI对接操作手册(纯操作版).assets/35-项目预付核销分区.png new file mode 100644 index 00000000..ee55bc41 Binary files /dev/null and b/doc/docs/业务流程/2026-10-14-金蝶(星瀚)BFI对接操作手册(纯操作版).assets/35-项目预付核销分区.png differ diff --git a/doc/docs/业务流程/2026-10-14-金蝶(星瀚)BFI对接操作手册(纯操作版).assets/36-选择项目预付单弹窗.png b/doc/docs/业务流程/2026-10-14-金蝶(星瀚)BFI对接操作手册(纯操作版).assets/36-选择项目预付单弹窗.png new file mode 100644 index 00000000..054d808f Binary files /dev/null and b/doc/docs/业务流程/2026-10-14-金蝶(星瀚)BFI对接操作手册(纯操作版).assets/36-选择项目预付单弹窗.png differ diff --git a/doc/docs/业务流程/2026-10-14-金蝶(星瀚)BFI对接操作手册(纯操作版).assets/37-付款结算单编辑页-结算合计三口径.png b/doc/docs/业务流程/2026-10-14-金蝶(星瀚)BFI对接操作手册(纯操作版).assets/37-付款结算单编辑页-结算合计三口径.png new file mode 100644 index 00000000..dc8a021e Binary files /dev/null and b/doc/docs/业务流程/2026-10-14-金蝶(星瀚)BFI对接操作手册(纯操作版).assets/37-付款结算单编辑页-结算合计三口径.png differ diff --git a/doc/docs/业务流程/2026-10-14-金蝶(星瀚)BFI对接操作手册(纯操作版).assets/38-开票申请表单.png b/doc/docs/业务流程/2026-10-14-金蝶(星瀚)BFI对接操作手册(纯操作版).assets/38-开票申请表单.png new file mode 100644 index 00000000..31226718 Binary files /dev/null and b/doc/docs/业务流程/2026-10-14-金蝶(星瀚)BFI对接操作手册(纯操作版).assets/38-开票申请表单.png differ diff --git a/doc/docs/业务流程/2026-10-14-金蝶(星瀚)BFI对接操作手册(纯操作版).assets/39-选择应收正式结算单弹窗.png b/doc/docs/业务流程/2026-10-14-金蝶(星瀚)BFI对接操作手册(纯操作版).assets/39-选择应收正式结算单弹窗.png new file mode 100644 index 00000000..0c9bb2bf Binary files /dev/null and b/doc/docs/业务流程/2026-10-14-金蝶(星瀚)BFI对接操作手册(纯操作版).assets/39-选择应收正式结算单弹窗.png differ diff --git a/doc/docs/业务流程/2026-10-14-金蝶(星瀚)BFI对接操作手册(纯操作版).assets/40-收款结算单列表-应收.png b/doc/docs/业务流程/2026-10-14-金蝶(星瀚)BFI对接操作手册(纯操作版).assets/40-收款结算单列表-应收.png new file mode 100644 index 00000000..79d6d17f Binary files /dev/null and b/doc/docs/业务流程/2026-10-14-金蝶(星瀚)BFI对接操作手册(纯操作版).assets/40-收款结算单列表-应收.png differ diff --git a/doc/docs/业务流程/2026-10-14-金蝶(星瀚)BFI对接操作手册(纯操作版).assets/bfi-effect-a301-2-公告同步对照.png b/doc/docs/业务流程/2026-10-14-金蝶(星瀚)BFI对接操作手册(纯操作版).assets/bfi-effect-a301-2-公告同步对照.png deleted file mode 100644 index 4b16d08d..00000000 Binary files a/doc/docs/业务流程/2026-10-14-金蝶(星瀚)BFI对接操作手册(纯操作版).assets/bfi-effect-a301-2-公告同步对照.png and /dev/null differ diff --git a/doc/docs/业务流程/2026-10-14-金蝶(星瀚)BFI对接操作手册(纯操作版).assets/bfi-effect-a301-3-认领推送对照.png b/doc/docs/业务流程/2026-10-14-金蝶(星瀚)BFI对接操作手册(纯操作版).assets/bfi-effect-a301-3-认领推送对照.png deleted file mode 100644 index 2ecd50fa..00000000 Binary files a/doc/docs/业务流程/2026-10-14-金蝶(星瀚)BFI对接操作手册(纯操作版).assets/bfi-effect-a301-3-认领推送对照.png and /dev/null differ diff --git a/doc/docs/业务流程/2026-10-14-金蝶(星瀚)BFI对接操作手册(纯操作版).assets/bfi-effect-a301-3-金蝶认领结果.png b/doc/docs/业务流程/2026-10-14-金蝶(星瀚)BFI对接操作手册(纯操作版).assets/bfi-effect-a301-3-金蝶认领结果.png deleted file mode 100644 index 022c9a96..00000000 Binary files a/doc/docs/业务流程/2026-10-14-金蝶(星瀚)BFI对接操作手册(纯操作版).assets/bfi-effect-a301-3-金蝶认领结果.png and /dev/null differ diff --git a/doc/docs/业务流程/2026-10-14-金蝶(星瀚)BFI对接操作手册(纯操作版).md b/doc/docs/业务流程/2026-10-14-金蝶(星瀚)BFI对接操作手册(纯操作版).md index 270be3fa..63ff19da 100644 --- a/doc/docs/业务流程/2026-10-14-金蝶(星瀚)BFI对接操作手册(纯操作版).md +++ b/doc/docs/业务流程/2026-10-14-金蝶(星瀚)BFI对接操作手册(纯操作版).md @@ -1,9 +1,21 @@ -# 金蝶(星瀚)BFI 对接操作手册(纯操作版) +# 金蝶(星瀚)BFI 与合同合规系统(HCS)对接操作手册(纯操作版) -> 整理日期:2026-10-14 -> 适用对象:TMS 业务操作人员(财务/业务)、系统管理员、实施运维。 +> 整理日期:2026-10-14(2026-11-04 金蝶+合同系统合并修订) +> 适用对象:TMS 业务操作人员(财务/业务/合同经办)、系统管理员、实施运维。 > 本文只讲**怎么操作**:在哪个页面、点什么按钮、前提是什么、成功的标志是什么、失败了怎么处理。 -> 接口与代码细节见同目录《2026-10-14-金蝶(星瀚)BFI全链路梳理(前端-后端-定时任务·业务域汇总与操作手册)》。 +> 覆盖两个外部系统的全部日常操作:**金蝶(星瀚)BFI**(客商、结算、付款、开票、收票、收款、结算调整)与**合同合规系统(HCS)**(项目立项、合同、特批、客商风险、履约计划/记录,见 4.2–4.18)。 +> 接口与代码细节见同目录《2026-10-14-金蝶(星瀚)BFI全链路梳理(前端-后端-定时任务·业务域汇总与操作手册)》与 hcs合同 目录各梳理文档; +> 预付与结算调整的深度说明见《2026-10-18-预付转正式结算单与项目预付操作手册(纯操作·带状态)》《2026-10-17-结算调整单操作流程说明(分场景)》(本文已汇总其操作要点)。 + +## 修改记录 + +| 日期 | 修订内容 | +|---|---| +| 2026-10-14 | 初版 | +| 2026-10-19 | 核对并补充 5.2 定时任务运转逻辑(cron 默认值、错峰、终态不轮询、防呆与自愈行为) | +| 2026-10-31 | ① 收票登记改「先票后款」:收票登记只选票推金蝶收票单(不再关联结算单,新增手选结算组织/发票明细),「认领收票单+指定应付」移到付款结算单;② 收款流水与认领改口径:数据源改金蝶收款单(A301-1)、「推送金蝶」「查询金蝶认领结果」按钮下线、认领只登记本地、作废同步合同合规;③ 客商建档新增供应商编码自动获取与收款信息开户行带出联行号操作;④ 准备事项、定时任务清单、FAQ、速查表同步更新 | +| 2026-11-03 | **四文档汇总修订(合并《2026-10-17 结算调整单》《2026-10-18 预付与项目预付》两本手册 + 按 11-01/11-02 代码与界面更新)**:① 全篇菜单更名对齐系统实际(预付结算单/付款结算单/收款结算单,新增 1.4 页面名称对照);② 1.1 状态表补页面显示别名与「已指定应付」列;③ 新增 4.5 进度预付、4.6 项目预付(含客商风险与付款特批)、4.7 项目预付核销、4.8 尾款结算、4.12 结算调整单、4.16 作废与撤销;④ 开票申请按「剩余可开票金额」累计控制(放开必须未开票限制)、分摊发票金额系统自动分摊;⑤ **收票后款认领按 11-02 改造更新**:发票登记移到付款结算单工具栏、弹窗回显已认领收票单、「匹配结算单金额」支持手动修改、查看页新增「是否已指定应付」列与「重新推送」入口、列表指定失败悬浮展示原因;⑥ **A302-6 指定应付定时自动补偿改开关控制(默认关闭)改人工兜底**;⑦ 4/5/6 节同步更新,截图全面更新(付款结算单列表与发票登记弹窗、选择收票单、查看页发票信息、指定失败悬浮、付款管理已指定应付列、收票登记表单去税率) | +| 2026-11-04 | **合并合同系统(HCS)操作内容,形成金蝶+合同系统组合手册(并入《HCS合同管理操作手册(纯操作版)》《履约计划与履约记录四场景业务说明》《客商黑名单与风险等级业务说明》)**:① **新增第 2 节「全链路总览」**(**12 条环节流程链**【系统】→步骤→▼ 格式逐环节梳理、两条通用规则与三条先后依赖;场景分流/拦截提示原文/成功提示在 4.x 对应小节体现),原第 2–6 节顺延为第 3–7 节,日常操作小节相应重排为 4.1–4.18;② 新增 1.5–1.8(HCS 是什么与合规状态、特批状态、三条推送路径、客商风险与黑名单口径),1.4 页面对照补客商/项目/合同顺序;③ 第 3 节新增合同系统准备清单;④ **项目立项推合规、合同管理与推送合规(含合同特批/变更/归档)置于 4.2/4.3(客商章节后)**;付款特批操作细节不单独成节(`spec-pay.enabled` 开关口径保留在 4.6);⑤ 新增 4.17 客商风险查看与筛选、4.18 履约计划与履约记录四场景;⑥ 4.6/4.10/4.14 补合同系统联动与金蝶实付门禁口径;⑦ 第 5 节新增 5.5 HCS 定时任务与开关及**六任务逐项运转逻辑**(判重/更新分流、特批兜底查询、金蝶实付门禁、开票自愈、收票合同回溯、风险分批刷新);⑧ 第 6/7 节补 HCS 常见问题与操作入口;⑨ 合同支持甲乙丙多方客商(2026-11-02)已写入 4.3;⑩ 新增截图 41/42/43(付款结果回写列表效果,FKD2026110300003↔FKSQ-000000282 对照),引用 HCS 手册现有截图 | --- @@ -16,62 +28,386 @@ ​ 13737128880 websoft5386339 +合同合规系统(HCS)测试环境地址:https://contract-sit.gxwuzi.com/ 账号:13737128880 密码:Mk13737128880@ + ### 1.1 金蝶单据状态含义(列表里看到的字段) -| 状态值 | 含义 | 还会变吗 | -|---|---|---| -| unsynced | 未推送金蝶 | 会(推送成功变 synced) | -| synced | 已推送金蝶,金蝶受理 | 会(继续走后续状态) | -| failed | 推送失败(有失败原因) | 会(重推成功变 synced) | -| paying | 金蝶付款中(仅付款申请) | 会(付款完成变 paid) | -| paid / closed | 已付款 / 已关闭(终态) | 不变 | -| invoiced | 已开票(终态,开票申请) | 不变 | -| assigned | 已指定应付(终态,收票) | 不变 | -| exist | 金蝶已有该客商且审核通过(终态,客商) | 不变 | +各列表的「金蝶单据状态」列是同一个字段,但不同页面的**显示文案**不同(下表「页面显示」列),看到哪个词都对得上: + +| 状态值 | 页面显示 | 含义 | 还会变吗 | +|---|---|---|---| +| unsynced | 未同步(结算/收票/开票/客商)·未生成(付款申请) | 未推送金蝶 | 会(推送成功变 synced) | +| synced | 已同步 ·已生成 | 已推送金蝶,金蝶受理 | 会(继续走后续状态) | +| failed | 同步失败 ·生成失败 | 推送失败(悬浮状态标签可看失败原因) | 会(重推成功变 synced) | +| paying | 付款中 | 金蝶付款中(仅付款申请) | 会(付款完成变 paid) | +| paid / closed | 已付款 / 已关闭 | 已付款 / 已关闭(终态) | 不变 | +| invoiced | 开票完成 | 已开票(终态,开票申请) | 不变 | +| pushed | 已推送(收票)/ 已推送(收款流水) | 金蝶收票单已生成(收票推送到此为止);已推送金蝶(收款流水) | 会(收票被付款结算单认领并指定应付后变 assigned) | +| assigned | 已指定应付 | 已指定应付(终态,收票——由付款结算单侧认领并在应付单推送后指定) | 不变 | +| exist | 金蝶已有该客商且审核通过 | 金蝶已审核(终态,客商) | 不变 | + +结算单列表、付款管理列表还有一列「**已指定应付**」(聚合本单认领收票单的指定状态):**已指定应付**(绿)/ **指定失败**(红,悬浮展示聚合的各票失败原因)/ **未指定**(灰)/ `-`(本单未认领收票单)。 ### 1.2 「金蝶锁定」标签是什么 结算单、付款申请、开票申请推送成功后,TMS 会**自动锁定金蝶侧单据**。列表出现红色「金蝶锁定中」标签时:**该单据不可修改、不可作废**。要改动只能先在金蝶侧解锁/删单,金蝶会回调 TMS 自动还原本地状态。 +| 列显示 | 含义 | 影响 | +|---|---|---| +| 金蝶锁定中(红标) | 金蝶已锁单 | 不可编辑、不可作废 | +| 锁定失败·补偿中 | 加锁没成功 | 系统每 5 分钟自动补偿重锁(≤5 次) | +| 锁定失败·已达上限 | 自动补偿 5 次都没成功 | 不再自动重试;重新点一次「同步金蝶」即可重置计数补锁,必要时联系管理员 | +| 金蝶删单 / 金蝶解锁(留痕) | 金蝶侧把单删了或解锁过 | 付款申请→金蝶状态变"生成失败"待重推;结算单/开票申请→系统自动作废(见 4.16) | + ### 1.3 两条推送路径 -- **自动**:审批「通过」时系统自动推送金蝶(需管理员开启对应开关,见第 4 节);开启后审批接口会直接返回推送结果,失败时页面提示「审批通过,但自动同步金蝶失败:xxx」。 -- **手动**:列表勾选单据点工具栏「同步金蝶 / 批量同步 / 推送金蝶」按钮。开关没开时用这条路径。 +- **自动**:审批「通过」时系统自动推送金蝶(需管理员开启对应开关,见第 5 节);开启后审批接口会直接返回推送结果,失败时页面提示「审批通过,但自动同步金蝶失败:xxx」。 +- **手动**:列表勾选单据点工具栏「同步金蝶 / 批量同步」按钮。开关没开时用这条路径。(收款流水认领没有推送动作——认领只登记本地并自动推合同合规,见 4.14。) 两条路径效果完全一样,重复点不会重复推送(系统自动查重)。 +合同系统(HCS)另有自己的三条推送路径(即时/定时/手动),见 1.7;两条系统各自独立推送、互不替代。 + +### 1.4 页面名称对照(先看这个) + +系统菜单已按"应付 / 应收"拆分,本文全部使用系统里的实际名称(旧的「正式结算单/预结算单」叫法已不再使用): + +| 本文场景 | 系统菜单位置 | 页面名称 | +|---|---|---| +| 客商档案 | 客商管理 | 客商档案 | +| 项目立项 | 项目管理 | **项目管理** | +| 合同新增/审批/推送合规 | 合同管理 | **合同管理** | +| 合同变更单 | 合同管理 | **合同变更** | +| 预付结算单(原预结算单) | 结算管理 | **预付结算单** | +| 应付正式结算单 | 结算管理 | **付款结算单** | +| 应收正式结算单 | 结算管理 | **收款结算单** | +| 结算调整 | 结算管理 | 结算调整单 | +| 付款申请列表 | 收付款管理 | **付款管理** | +| 开票申请列表 | 收付款管理 | **开票管理** | +| 收票登记 | 收付款管理 | 收票管理 | +| 收款流水/认领记录 | 收付款管理 | 收款流水、认领记录 | + +付款申请的**付款类型**有三种,列表和表单里都按这个名称显示: + +| 类型值 | 页面显示 | 用途 | +|---|---|---| +| 进度预付 | 进度预付 | 挂预付结算单提前付款 | +| 项目预付 | 项目预付 | 不挂结算单,按项目+承运商合同预付 | +| 尾款付款 | 尾款付款 | 挂付款结算单付尾款 | + +### 1.5 合同系统(HCS)与「合规状态」 + +TMS 合同审批通过后,合同会推送到桂物合同合规系统(HCS)。HCS 完成客商风险校验、合同合规校验、特批和金蝶同步,并把合规合同ID、金蝶合同ID、特批结果回写到 TMS。项目立项、开票登记、收票单、付款履约数据按同一套框架推送(见 4.2–4.18)。 + +各单据列表的「**合规状态**」列是同一个字段,不同页面显示文案一致: + +| 状态值 | 页面显示 | 含义 | 还会变吗 | +|---|---|---|---| +| unsynced | 待推送 | 审批已通过,尚未成功推送 HCS | 会(推送成功变已推送) | +| synced | 已推送 | HCS 已受理,已回填合规ID | 会(特批状态、金蝶ID可能继续变化) | +| failed | 推送失败 | 推送失败,鼠标悬停合规状态可查看原因 | 会(自动重试或手动重推成功后变已推送) | + +> 「已推送」只代表 HCS 已受理,不代表特批和金蝶同步已完成;金蝶合同ID/金蝶项目ID由 HCS 完成金蝶同步后回写,可能延迟。 + +### 1.6 特批状态(合同与付款共用) + +| 状态值 | 页面显示 | 含义 | 后续 | +|---|---|---|---| +| need | 待特批 | 尚未进入特批流程,多见于存量数据或推送未完成 | 重新推送成功后转特批中 | +| processing | 特批中 | HCS 正在审批特批;合同列表当前节点显示「特批审核」 | 等待 HCS 回传、定时查询或人工确认 | +| approved | 特批通过 | 特批完成 | 合同恢复「审批通过」,继续后续流程 | +| rejected | 特批驳回 | 特批未通过 | 合同/付款审批联动「已驳回」,修改后重新提交 | + +合同特批看合同详情「合规信息」区的特批状态;**付款申请页面没有独立特批状态列**,看列表「当前节点」:显示「特批审核」即为特批中,通过后恢复「审批通过」,驳回则审核状态变「已驳回」。 + +![付款管理列表「当前节点」列:特批中的付款申请当前节点显示「特批审核」(审核状态仍为审批通过,不能只看审核状态判断流程是否结束;特批通过恢复「审批通过」后金蝶单据才会生成)](./2026-10-14-金蝶(星瀚)BFI对接操作手册(纯操作版).assets/42-付款管理-特批审核当前节点.png) + +### 1.7 合同系统三条推送路径 + +| 路径 | 触发时机 | 开关 | 说明 | +|---|---|---|---| +| 自动即时推送 | 审批人点「通过」后 | `thirdParty.hcs.push.immediate-enabled` | 项目立项、合同受此开关控制;审批响应等待推送结果,失败提示「审批通过,但合规推送失败:xxx」 | +| 定时扫描推送 | 每 5 分钟(五个业务错峰) | `thirdParty.hcs.push.{bizType}.sync-enabled` | 扫「待推送/推送失败未超限」的单据;只有合同任务额外兜底查询特批结果 | +| 手动推送 | 随时 | 无 | 列表行内「推送合规」或工具栏「批量推送合规」(开票登记、收票单页面无推送按钮,靠定时或运维接口) | + +- **付款申请**:审批通过后即时发起付款特批/推送合规的动作受 `thirdParty.hcs.spec-pay.enabled` 控制(★2026-11-03 起默认关闭,见 4.6);定时扫描只做失败补偿。 +- 三条路径共用同一套推送逻辑;HCS 侧有判重,重复触发不会重复生成。 +- 推送失败默认自动重试 **5 次**,超过后只能人工重推(人工重推会把失败次数清零)。 + +### 1.8 客商风险与黑名单口径 + +客商风险数据来自两个外部系统,每日 02:30 自动刷新到客商档案(范围:合同期限内生效合同的甲乙方客商): + +| 数据 | 来源 | 说明 | +|---|---|---| +| 风险等级 | 合同合规系统(C101-1) | 低风险 / 中风险 / 高风险 | +| 金蝶黑名单 | 金蝶(星瀚)· 国资委黑名单(A801-5) | 命中记录失信类别(**A 类禁止交易类** / B 类风险警示类)与列入事由 | + +- **黑名单仅指金蝶黑名单命中**;合同合规系统返回的黑名单状态字段只用于付款提交强制校验,不在列表展示。 +- 客商档案、合同列表的「客商风险」列为等级+黑名单组合展示(等级在前,如「高风险、黑名单」),红色标签悬浮查看明细(失信类别、列入事由、风险等级);查看与筛选操作见 4.17。 +- 内部组织客商不查询风险,默认「无风险」;数据每天 02:30 刷新,当天新增客商以客商表单实时提示为准,列表次日生效。 +- 风险管控作用点:①客商准入提审黑名单硬校验(4.1 第 3 步);②合同推送自动特批(4.3 第四步);③付款提交特批附件必传(4.6)。 + --- -## 2. 一次性准备(实施/管理员操作) +## 2. 全链路总览(各环节流程) + +> 本章讲清各环节的先后顺序、每一步推给哪个系统、成功标志是什么;逐环节的操作细节见「日常操作流程」章节对应小节(节号在各链标题中)。 + +### 2.1 各环节流程链(【系统】→ 步骤 → ▼,按业务顺序编号) + +**① 客商建档与同步(4.1)** +```text +【TMS】客商档案新增(blade_customer_archive) + │ ① 录统一社会信用代码满 18 位 → 实时查金蝶(A901-1)回填工商信息 + │ ② 收款信息「开户行」失焦 → 按行名从金蝶带出联行号;保存前按行号反查校验 + │ ③ 提交审批 → 按纳税人识别号查金蝶黑名单(A801-5):A 类禁止提交 + ▼ +审批通过 → 自动推金蝶(A902) + │ ④ 成功 kingdee_status=synced;失败 failed(行内「同步金蝶」重推) + │ ⑤ 定时任务(每天 02:00)复查金蝶审核状态:通过 → exist(终态) + ▼ +金蝶已审核客商(exist) +``` + + +**② 项目立项推合规(4.2)** +```text +【TMS】项目立项(blade_project_apply) + │ ① 审批通过 / 变更审批通过 → 待推送 + ▼ +推送合规(审批即时开关 / 定时扫描 / 行内「推送合规」) + │ ② C102-3 按项目编号判重:命中 → 回填已有合规ID(不重复建) + │ ③ 未命中 → C102-1 新建(付款/开票额度万元×10000 转元) + │ ④ 已有合规ID(变更)→ C102-2 更新 + ▼ +【TMS】合规状态=已推送 + 合规项目ID 落库(hcs_fd_id) + │ ⑤ 合规完成金蝶同步后回调 projectLegal/result → 回填金蝶项目ID/编号(不改合规状态) + ▼ +列表「金蝶项目ID / 金蝶项目编号」回显(推送成功≠金蝶项目已建) +``` + + +**③ 合同推合规与合同特批(4.3)** +```text +【TMS】合同(blade_contract_manage) + │ ① 审批通过 / 变更审批通过 → 推合规(即时开关 / 定时扫描 / 行内「推送合规」) + ▼ +推送链(单条依次执行) + │ ② 附件转传 C103-2(失败整条推送失败) + │ ③ 客商风险 C101-1 + 金蝶黑名单 A801-5(甲乙逐个查,命中只标记不阻断) + │ ④ C103-3 按合同编号判重 → C103-1 起草 / C103-11 更新(高风险随报文发起特批) + │ ⑤ 履约规则 C103-9 行级同步(开关开启时;失败整条 failed,下轮凭合规ID走更新补偿) + ▼ +【TMS】合规状态=已推送 + 合规合同ID 落库 + │ ⑥ 风险命中 → 特批状态=特批中、当前节点「特批审核」(特批中禁止重推) + ▼ +特批结果:contractLegal/result 回调 或 定时 C103-4 查询兜底 + │ ⑦ 通过 → 恢复「审批通过」(金蝶合同ID可能同时回填) + │ ⑧ 驳回 → 合同联动「已驳回」,修改后重新提交审批、重新推送 +``` + + +**④ 预付结算单 → 进度预付(4.4/4.5)** +```text +【TMS】预付结算单(blade_pre_settlement,本身不推金蝶) + │ ① 制单/审批通过(外币在此选汇率日期带出结算汇率) + ▼ +三个出口:预付申请(进度预付) / 项目预付核销 / 尾款结算 + │ ② 工具栏「预付申请」(限应付+审批通过+未转结算+同一合同,可多选) + ▼ +【TMS】付款申请新增页(blade_payment_application,类型锁定「进度预付」) + │ ③ 结算金额/可付款金额自动带出 → 填申请付款金额/付款方式/收款方 → 保存 + │ ④ 提交 → 审批通过 → 推金蝶(见⑧) + ▼ +金蝶付讫(已付款)→ 自动回写预付结算单垫资记录与「已付款金额」 +``` + + +**⑤ 项目预付与核销(4.6/4.7)** +```text +【TMS】付款申请新增,付款类型=项目预付(不挂结算单) + │ ① 先选项目再选承运商合同 → 提交校验项目额度/客户额度 → 审批通过 + ▼ +推金蝶(预付款通道,金蝶生成「其他付款申请」) + │ ② 金蝶付款完成 → 金蝶状态「已付款」(未付款不能核销) + ▼ +【TMS】结算单编辑页「项目预付核销」分区(blade_settlement_project_advance_write_off) + │ ③ 「选择」弹窗只列:同合同 + 审批通过 + 金蝶已付款 + 有剩余可核销额 + │ ④ 核销金额计入「已付金额(含预付、核销)」,可核销额度即时占用 +``` + + +**⑥ 尾款结算(4.8)** +```text +【TMS】预付结算单列表勾选(同项目、应付、审批通过、无在途预付申请) + │ ① 工具栏「尾款结算」→ 跳新增付款结算单,明细/费用项合并带入 + │ ② 结算合计三口径:结算合计(全额)/ 已付金额(含预付、核销)/ 剩余未付 + │ ③ 来源预付结算单被锁定(不能再发起预付、不能作废) + ▼ +保存草稿 → 提交 → 审批通过 → 推金蝶(见⑦) +``` + + +**⑦ 结算单推送金蝶(4.9)** +```text +【TMS】付款结算单 / 收款结算单(blade_formal_settlement) + │ ① 审批通过;外币先重选汇率日期带出结算汇率(预付结算单汇率不自动继承) + ▼ +推送(审批自动开关 / 工具栏「同步金蝶」) + │ ② 应付 A102-2 / 应收 A302-2 按结算类型自动分流 + │ ③ 成功:金蝶单据号回填 + 状态已同步 + 金蝶锁定(不可改/不可作废) + │ ④ 应付单生成后,系统自动对本单认领的收票单执行「指定应付」(见⑩) + ▼ +失败:悬浮状态标签看原因;failed 重推任务每 10 分钟自动续推 +``` + + +**⑧ 付款申请推送与实付回写(4.10)** +```text +【TMS】付款申请(blade_payment_application,审批通过) + │ ① 门禁:付款特批审核中拦截推送金蝶(仅特批走合规开关开启时) + │ ② 推金蝶 A103(预付走预付款通道):已生成 + 金蝶单号 + 落锁 + │ ③ 履约计划金额本地累加(blade_contract_exe_stage,按款项性质精确匹配) + ▼ +金蝶付款结果回写(定时 10 分钟 / 工具栏「同步付款结果」) + │ ④ 已生成 → 付款中 → 已付款(回写付款记录/凭证)/ 已关闭 + ▼ +实付回写(paidAmount>0)→ 自动重新置待同步 + │ ⑤ C103-9 重推履约计划 → C108-1 新建 / C108-2 更新付款履约记录(一张申请一条) + │ 未实付时整体照常已推送,结果说明「金蝶未实际付款…」(正常等待,非失败) +``` + + +**⑨ 开票申请(4.11)** +```text +【TMS】开票申请(blade_invoice_application) + │ ① 选应收结算单(审批通过+剩余可开票金额>0)→ 分摊发票金额系统自动分摊 → 审批通过 + ▼ +推金蝶(按发票张次逐张) + │ ② A303-1:一张申请 N 张发票 → N 张金蝶开票申请单,逐张锁定 + ▼ +开票结果回写(定时 10 分钟) + │ ③ 发票逐张落库 → 状态「开票完成」(终态) + │ ④ 合规 C111 开票登记(金蝶已开票自动重置待同步 → C111-3 更新票据日期/状态) + │ ⑤ 每张发票一条开票履约记录(C108-1,合规检索「开票履约-发票号」) +``` + + +**⑩ 收票登记 → 认领 → 指定应付(4.12,先票后款两段式)** +```text +【TMS】收票登记(blade_invoice_receipt) + │ ① 票池选票(带出发票明细)+ 手选结算组织 → 保存前重查金蝶票池可用性 + │ ② 审批通过 → 推金蝶收票单 A303-2(按登记ID幂等防重)→ 终态 pushed + │ ③ 合规 C109 推收票单 → 成功后按认领分摊行推履约收票记录(C108-1) + ▼ +【TMS】付款结算单认领收票单(blade_invoice_receipt_settlement) + │ ④ 三入口等效:编辑页「选择收票单」/ 工具栏「发票登记」/ 查看页「重新推送」 + │ ⑤ 一张收票单只能被一张付款结算单认领;匹配金额默认=发票金额可改小 + ▼ +应付单推送金蝶后 → 系统自动指定应付(A302-6) + │ ⑥ 收票状态 pushed → assigned(终态);指定失败不影响结算单,人工入口重推兜底 +``` + + +**⑪ 收款流水与认领(4.14)** +```text +【金蝶】A301-1 收款单 + │ ① 手动同步(可按窗口重复执行,幂等) + ▼ +【TMS】收款流水列表(blade_kingdee_receipt_flow) + │ ② 勾选/单条 → 选择应收正式结算单 → 填分摊金额 + ▼ +【TMS】认领(blade_receipt_claim + blade_receipt_claim_settlement) + │ ③ 更新流水累计认领金额/状态、刷新结算单收款汇总 + │ ④ 事务提交后推合规:C103-9 → C107-1 → C108-1(失败只落标记,不自动重试) + ▼ +合规侧认领记录(C107-1 返回 fdId 落库 blade_receipt_claim_hcs_record) + │ ⑤ 需要撤销时在「认领记录」页作废 + ▼ +本地回退金额 + 推 C107-3(fdStatus=1 已作废;计划金额与履约流水不回冲) +``` + + +**⑫ 结算调整单(4.13)** +```text +【TMS】结算调整单(blade_settlement_adjustment,只能手工新增,无作废) + │ ① 选可调整结算单(审批通过+未完成收付款+无在途付款申请)→ 带出费用明细 + │ ② 录调整金额(可为负/新增手工费用)→ 选单时做金蝶冲销预检(软提示不拦截) + ▼ +审批「通过」(确认后按原单是否推过金蝶自动分流) + │ ③ 场景A 原单未推金蝶:调整直接回写原单 → 自动推送金蝶(应付/应收+落锁+附件) + │ ④ 场景B 原单已推金蝶:原单零改动只追加变更记录 → 提示财务先在金蝶手工冲销旧单 + ▼ +场景B 续:金蝶侧冲销完成后 → 业务点「重新推送(高危)」 + │ ⑤ 硬闸门查金蝶旧单必须已冲销 → 克隆新结算单(直接审批通过态)→ 推金蝶 + │ ⑥ 项目预付核销行随迁新单;原单标记「已调整」仅存档;失败复用新单续推(幂等) +``` + + +### 2.2 两条通用规则与三条先后依赖(先记这些,大部分问题都是顺序或状态没走到) + +**通用规则**: +- 同一套推送逻辑两条触发路径:审批通过自动推(开关开启时)与页面手动按钮效果完全一致,重复触发自动查重,不会重复生成金蝶/合规单据。 +- 推送成功即落锁:结算单/付款/开票推送成功后自动锁金蝶侧单据——不可编辑、不可作废(见 1.2)。 + +**三条先后依赖**: + +1. **结算单必须先推金蝶**,付款申请、开票申请才能推——付款从金蝶应付单下推、开票前系统检查金蝶应收单。 +2. **合同必须先推合规**,付款特批与履约数据才有前提(C106-3 要求关联合同有合规ID;履约计划/记录以合同为根)。 +3. **履约记录晚于业务发生**:付款=金蝶实付后、收款=认领提交后、开票=金蝶开票完成后、收票=收票单推合规后——审批通过就去合规找记录是找不到的(付款场景连计划金额都还没同步),口径对照见 4.18。 + +--- + +## 3. 一次性准备(实施/管理员操作) | # | 事项 | 操作人 | 操作内容 | 不做的后果 | |---|---|---|---|---| | 1 | 部门金蝶组织编码 | 管理员 | 在部门档案维护「金蝶组织编码」字段 | 该部门单据推送时直接报错拦截 | | 2 | 客商收款账户联行号 | 财务 | 客商档案维护收款账户(账号/开户行,**联行号必填**) | 付款推送、客商推送报错拦截 | | 3 | 主数据初始化 SQL | 运维 | 执行 `doc/sql/transport/` 下金蝶相关脚本(计量单位等主数据、认领表、10-13 迁移脚本 `lqj/`) | 对应链路数据缺失 | -| 4 | 认领人配置 | 运维 | Nacos 配 `thirdParty.bfi.claimAnnounce.claimusers`(值须与金蝶认领公告的认领人一致) | 收款流水同步查不到数据(不报错) | +| 4 | 平台公司金蝶组织编码 | 管理员 | 组织管理维护「是否平台公司=是」并填**金蝶编码** | 收款流水同步直接报错拦截;收票按手选部门上溯平台公司取组织编码失败也会推送拦截 | | 5 | 票池票种白名单 | 运维 | Nacos 配 `thirdParty.bfi.invoiceReceipt.invoiceTypes`(要同步的发票种类编码,逗号分隔) | 票池同步不到对应票种 | | 6 | 物料映射 | 运维 | Nacos 配 `thirdparty.bfi.material.*`(有默认值,建议显式确认) | 推送报「运输方式未配置」类拦截 | -| 7 | 定时任务开关 | 运维 | 按第 4 节清单逐项开启 | 只能全靠手动按钮 | -| 8 | 审批自动同步开关 | 运维 | 按第 4 节 `autoSync.*` 按域开启 | 审批通过后不自动推送,走手动 | +| 7 | 定时任务开关 | 运维 | 按第 5 节清单逐项开启 | 只能全靠手动按钮 | +| 8 | 审批自动同步开关 | 运维 | 按第 5 节 `autoSync.*` 按域开启 | 审批通过后不自动推送,走手动 | + +**合同系统(HCS)准备**(Nacos 前缀 `thirdParty.hcs.*`,与上表金蝶准备相互独立): + +| # | 事项 | 操作人 | 操作内容 | 不做的后果 | +|---|---|---|---|---| +| 1 | HCS 连接配置 | 运维 | `baseUrl`、`authorization`、`key-auth` | 所有推送失败或认证被拒 | +| 2 | 合同类别模板映射 | 运维 | `contract.template-mapping` 为**每个合同类别**配置 HCS 模板ID(YAML 中文键必须写 `"[承运商合同]"` 括号写法) | 未映射的类别推送必失败 | +| 3 | 项目立项模板编码 | 运维 | `project.template-code` | 项目立项推送必失败 | +| 4 | 付款特批模板与开关 | 运维 | `spec-pay.template-id`、`payment-purpose/nature/type` 字典;★`spec-pay.enabled` 特批走合规开关,**默认 false**(见 4.6) | 开启但未配模板时特批发起失败;默认关闭时付款不走合规特批 | +| 5 | 履约计划/记录开关 | 运维 | `exe-stage.sync-enabled`(履约计划)、`perf-record.sync-enabled`(履约记录,默认 false,开启需配 `perf-record.xform-id`) | 不推送履约数据(不影响合同主流程) | +| 6 | 附件下载基址 | 运维 | `attachment.file-download-base`;`transfer-enabled=true` | 合同有附件时转传失败 | +| 7 | 币种映射 | 运维 | 合同 `contract.currency-name-mapping`;开票 `invoice.currency-id-mapping`(**RMB 必配**) | 币种传错或为空;RMB 未配时开票登记每轮失败 | +| 8 | 风险/额度校验开关 | 运维 | `contract.require-risk-check=true`、`risk.check-on-payment=true`、`credit.check-enabled`(超额自动转特批) | 关闭后高风险合同/付款不触发特批 | +| 9 | 推送开关 | 运维 | `push.immediate-enabled`(项目/合同即时)、`push.{project|contract|payment|invoice-application|invoice-receipt}.sync-enabled`(定时扫描,默认全关,见 5.5) | 只能靠手动按钮推送 | +| 10 | 客商风险每日刷新 | 运维 | `risk.refresh-enabled` 上线置 `true`(默认 false,每天 02:30) | 客商档案/合同列表「客商风险」列无数据 | +| 11 | 客商名称一致性 | 业务 | 合同甲乙方名称与客商档案全称或简称一致 | 风险校验取不到客商被跳过,可能漏特批 | --- -## 3. 日常操作流程 +## 4. 日常操作流程 -### 3.1 客商建档与同步 +### 4.1 客商建档与同步 **页面:客商管理 → 客商档案** ![客商档案列表:行内「同步金蝶」链接仅在审批通过且推送失败(failed)的单据上显示;审批通过的客商由系统自动推送](./2026-10-14-金蝶(星瀚)BFI对接操作手册(纯操作版).assets/01-客商档案列表.png) 1. 点「新增」,录入**统一社会信用代码**(满 18 位自动查金蝶回填工商信息并锁定相关字段)。 + - 表单「供应商编码」字段自动从金蝶获取(金蝶侧建档/同步后回显),无需手填。 - 若提示金蝶侧该客商「审核中」——等金蝶审核完再保存/推送。 -2. 维护**收款账户(联行号必填)**和发票信息(纳税人识别号用于黑名单校验)。 +2. 维护**收款账户**和发票信息(纳税人识别号用于黑名单校验)。 + - 收款信息「开户行」输入后失焦,系统自动按行名从金蝶**带出联行号**;带不出时可手动填写,保存时系统会按联行号反查金蝶校验,查不到会拦截保存。 3. 提交审批:系统自动做**黑名单校验**。 - - 提交被拦「命中黑名单」→ 该客商为国资委黑名单 A 类,**不能提交**,走线下处置。 + - 提交被拦「命中黑名单」→ 该客商为国资委黑名单 A 类(禁止交易类),**不能提交**,走线下处置;B 类(风险警示类)仅告警默认放行并记录;黑名单查询异常时暂缓提交(防漏拦),稍后重试。 4. 审批「通过」→ 系统自动推送金蝶 → 列表 `kingdee_status` 变 **synced**。 - 变 **failed** → 看失败原因(悬停/详情),修复后点行内「同步金蝶」重推。 + - failed 原因是金蝶侧该客商「审核中」→ 等金蝶审核完由定时任务自动升级(**无需反复重推**)。 5. synced 是「金蝶已受理」;金蝶审核通过后由定时任务升级为 **exist**(终态,无需操作)。 推送成功后金蝶侧效果(供确认推送结果时对照): @@ -80,43 +416,242 @@ ![TMS 客商档案与金蝶客户档案对照:编码一致、以统一社会信用代码为关联键](./2026-10-14-金蝶(星瀚)BFI对接操作手册(纯操作版).assets/bfi-effect-a902-客商编码对照.png) -![新增客商档案:统一社会信用代码录入满 18 位自动到金蝶查询并回填工商信息](./2026-10-14-金蝶(星瀚)BFI对接操作手册(纯操作版).assets/02-客商档案新增-信用代码自动查询.png) +![新增客商档案:统一社会信用代码录入满 18 位自动到金蝶查询并回填工商信息;「供应商编码」字段由系统自动获取(金蝶 A901-1)](./2026-10-14-金蝶(星瀚)BFI对接操作手册(纯操作版).assets/02-客商档案新增-信用代码自动查询.png) -> 注意:审批通过后的**档案变更不会重新推送金蝶**。改了信用代码等关键信息需人工确认金蝶侧一致性。 +![客商档案表单:基础信息含「供应商编码」(金蝶供应商编码,系统自动获取);收款信息 tab 维护开户行/联行号(开户行失焦按行名从金蝶带出联行号,保存前按行号校验)](./2026-10-14-金蝶(星瀚)BFI对接操作手册(纯操作版).assets/15-客商档案-供应商编码.png) -### 3.2 预结算单(正式结算与预付款的前置) +![新增客商表单风险实时提示:客商名称下方显示「被列入外部黑名单(失信类别:风险警示类)…」红色提示,悬浮见明细;提交审批时按纳税人识别号做金蝶黑名单硬校验(A 类禁止提交)](./2026-10-14-金蝶(星瀚)BFI对接操作手册(纯操作版).assets/image-20261030195438695.png) -**页面:结算管理 → 预结算单** -预结算单**本身不推送金蝶**(列表无金蝶状态字段),它是正式结算和进度预付的前置单据,与金蝶的关联有两处:①非人民币项目在此维护汇率(从金蝶自动带出);②进度预付款在金蝶付讫后自动回写本单的垫资记录。 -![预结算单列表:工具栏「新增 / 预付申请 / 尾款结算」;草稿行可 编辑/删除/提交,审批中可 通过/驳回](./2026-10-14-金蝶(星瀚)BFI对接操作手册(纯操作版).assets/11-预结算单列表.png) + + +> ·注意:审批通过后的**档案变更不会重新推送金蝶**。改了信用代码等关键信息需人工确认金蝶侧一致性。 + +### 4.2 项目立项推送合同合规 + +**页面:项目管理 → 项目管理** + +项目立项审批通过后,TMS 把项目信息推送到合同合规系统(C102):首次推送先按**项目编号**判重,命中直接回填已有合规项目ID(不重复建);变更审批通过后按已有合规项目ID更新。项目没有客商风险特批环节。 + +![项目管理列表:审批通过且未推送的项目行内显示「推送合规」;合规状态/金蝶项目ID列在表格右侧横向滚动区](./2026-10-14-金蝶(星瀚)BFI对接操作手册(纯操作版).assets/41-项目管理列表-推送合规.png) + +**前提**:项目审核状态为「审批通过」或「变更审批通过」、项目编号不为空;运维已配置 `thirdParty.hcs.project.template-code`(第 3 节 HCS 准备 #3)。 + +1. **自动路径**:审批「通过」时即时推送(开关 `push.immediate-enabled` 开启时页面显示「正在推送合规,请稍候…」遮罩,等待结束勿重复点击);未开启时等定时扫描(`push.project.sync-enabled`,每 5 分钟)。 +2. **手动路径**:行内「**推送合规**」→ 确认弹窗「确定」→ 等遮罩结束列表自动刷新;勾选多条可点工具栏「**批量推送合规**」(选中项必须全部满足推送条件,否则整批提示「选中数据中有 N 条不符合推送条件」)。 +3. **成功标志**:合规状态变**已推送**;提示可能为「项目立项新建成功:项目编号」(新建)、「项目立项更新成功:项目编号」(变更更新)、「判重命中,回填已有合规项目ID」。 +4. **结果查看**:列表「金蝶项目ID」「金蝶项目编号」由 HCS 完成金蝶同步后回调回填,可能延迟——**推送成功 ≠ 金蝶项目已建**,稍后刷新查看。 +5. **失败处理**:悬停合规状态看原因(常见:「立项表单模板编码未配置」→运维补配置;C102 接口异常/超时→稍后重推)。失败每 5 分钟自动重试,超 5 次停止,人工重推清零。 +6. **项目变更**:审批通过/变更审批通过的项目行内「变更」→ 修改提交审批 → 通过后按合规项目ID更新 HCS;从未推送过的按首推处理(先判重再新建)。 + +### 4.3 合同管理:新增、审批与推送合规 + +**页面:合同管理 → 合同管理 / 合同变更** + +合同是付款特批与履约数据的前提:付款申请的特批、履约计划/记录都挂在**已推送合规的合同**下。合同列表的「合规状态」「金蝶合同ID」「当前节点」「归档状态」列是本节的主看板。 + +![合同列表状态与操作列:合同阶段/审核状态/归档状态/合规状态/金蝶合同ID/当前节点(特批审核=特批中)/操作列(查看/复制/变更/附件上传/推送合规),工具栏含「批量推送合规」](../hcs合同/2026-10-23-HCS合同管理操作手册(纯操作版).assets/02-合同列表状态与操作列.png) + +**第一步:新增与提交审批** + +![新增合同:签约类型、合同名称、合同类型、项目、甲乙方、合同期限、金额、结算方式等基本信息(甲乙方可选多个客商,支持甲乙丙多方;推送合规按多方展开签署方)](../hcs合同/2026-10-23-HCS合同管理操作手册(纯操作版).assets/04-新增合同-基本信息.png) + +1. 「新增」填写基本信息(合同编号可不填,按合同负责部门规则自动生成)、计费信息、结算单规则、付款比例设置——**正式合同**这三块必须完整并通过校验,**临时合同**只校验基础必填项。 +2. 上传附件:用印材料区(用印材料/双章归档文件/其他文件)、佐证材料区(**特批附件**/其他文件)。**高风险/黑名单客商合同必须上传「特批附件」**,否则提交被拦截(提示见 4.6 同款)。 +3. 三种提交:「保存」(草稿,不推送)/「提交临时合同」(有效期 30 天,及时「转正式合同」)/「提交正式合同」。提交后审核状态「审批中」。 + +**第二步:审批与推送合规** + +![手动推送合规确认:列表行内「推送合规」,确认后显示「正在推送合规,请稍候…」](../hcs合同/2026-10-23-HCS合同管理操作手册(纯操作版).assets/15-手动推送合规确认.png) + +1. 审批人点「通过」→ 即时推送(开关开启时)或等定时扫描;也可对「待推送/推送失败」的合同行内「推送合规」/勾选后工具栏「批量推送合规」手工推。 +2. **成功标志**:合规状态**已推送** + 详情回填**合规合同ID**。 +3. **推送时系统自动做的事**(供理解,无需操作):附件转传合规(C103-2)→ 客商风险校验(C101-1 + 金蝶黑名单 A801-5,甲乙双方逐个查,**命中只标记不阻断推送**)→ 判重/起草(C103-3/C103-1,报文含业务合同号与归档状态)→ 履约规则同步(C103-9,开关开启时同步合同表单「履约规则」页签的付款/收款计划行)→ 高风险/黑名单命中则挂起「特批审核」(第四步)。 +4. **失败处理**:悬停合规状态看原因。常见:「合同类别 [xxx] 未配置合规模板ID」→运维补映射;附件转传失败→查下载基址与附件;C103-1/C103-11 业务校验错误→按返回 msg 修数据。超 5 次停止自动重试,人工重推清零。 + - 怀疑合规侧其实已生成:**直接重推即可**——本地无合规ID先走判重回填,已有合规ID走更新,两种路径都不会重复生成合同。 + +![待推送与推送失败列表:合规状态列「待推送/推送失败」,行内「推送合规」按钮,失败原因可悬停查看](../hcs合同/2026-10-23-HCS合同管理操作手册(纯操作版).assets/14-待推送与推送失败列表.png) + +**第三步:查看推送结果(合同详情「合规信息」区)** + +![合同详情合规信息:合规同步状态、合规合同ID、客商风险、特批状态、特批说明](../hcs合同/2026-10-23-HCS合同管理操作手册(纯操作版).assets/06-合同详情-基本信息与合规信息.png) + +- 合规状态=已推送且合规合同ID不为空即推送成功;**金蝶合同ID**由 HCS 金蝶同步后回写,可能延迟,空着先刷新。 +- **已推送的合同不能直接编辑**——要改内容必须走「变更」(第五步),否则会在合规侧产生重复合同。 + +**第四步:高风险/黑名单合同特批** + +![高风险客商合同:合规信息显示客商风险红色标签与特批状态](../hcs合同/2026-10-23-HCS合同管理操作手册(纯操作版).assets/12-特批待发起.png) + +1. 触发条件:甲方或乙方为外部客商,且客商为高风险或金蝶黑名单命中(口径见 1.8)。 +2. 流程:推送成功后合同自动进入特批 → 特批状态「特批中」、当前节点「**特批审核**」→ HCS 完成后经回调或定时查询(随合同扫描任务,每 5 分钟兜底)回传 → **特批通过**:恢复「审批通过」(金蝶合同ID可能同时回填);**特批驳回**:合同审批联动「已驳回」,修改后重新提交审批、重新推送。 +3. **人工确认特批结果**(结果长时间未回传时):详情「合规信息」右上角「**确认特批结果**」→ 输入 `approved`(通过)或 `rejected`(驳回)→ 确定。仅「特批中」可操作,确认前务必与合规侧核对。 + +![确认特批结果:输入 approved 或 rejected](../hcs合同/2026-10-23-HCS合同管理操作手册(纯操作版).assets/13-确认特批结果.png) + +4. 特批中的合同**不允许重推**(提示「合同特批审核中,禁止重推」)。详情页右上角「发起特批」是旧版独立入口,点击提示接口不存在可忽略——特批随推送自动发起。 + +**第五步:合同变更(已推送合同改内容)** + +![合同变更页面:在正式合同基础上修改基本信息、附件、计费、结算和付款比例](../hcs合同/2026-10-23-HCS合同管理操作手册(纯操作版).assets/09-合同变更-基本信息.png) + +1. 合同管理列表行内「**变更**」(变更驳回后点「重新提交变更」)→ 系统创建变更单草稿并跳转「**合同变更**」页面;同一合同已有审批中的变更单时拦截,有草稿/已撤回/变更驳回的变更单时打开继续编辑。 +2. 选**变更类型**(合同信息变更/结算规则变更/终止合同——「终止合同」当前版本未开放)→ 修改内容 → 填**变更原因**(最多 500 字)→ 上传变更材料 → 「提交」。 +3. 审批人在「合同变更」列表审批:**通过** → 回写原合同(状态变「变更审批通过」)并按合规合同ID更新 HCS(C103-11),履约计划行随变更重推,变更记录保留在合同详情底部;**驳回** → 变更单「变更驳回」可改后重提;审批中可**撤回**(不影响原合同状态)。 + +![合同变更下部:填写变更原因(最多 500 字)并上传变更材料](../hcs合同/2026-10-23-HCS合同管理操作手册(纯操作版).assets/10-合同变更-原因与材料.png) + +**第六步:附件上传与归档** + +![附件上传弹窗:先选附件位置(用印材料/佐证材料),再选附件类型并上传](../hcs合同/2026-10-23-HCS合同管理操作手册(纯操作版).assets/11-附件上传弹窗.png) + +1. 列表行内「**附件上传**」(正式合同审批通过后):选附件位置与类型上传(支持 pdf/图片/office/zip 等,单个 ≤50MB)。 +2. **归档**:附件类型选「**双章归档文件**」(位置=用印材料)→ 合同归档状态变「已归档」,且已推送合规的合同**即时**更新合规侧归档状态为「待归档」并挂载归档附件;未推送的合同在首次推送报文中自带归档状态。合规侧后续归档流转由合规系统维护。 + +### 4.4 预付结算单(进度预付与尾款结算的前置) + +**页面:结算管理 → 预付结算单**(原「预结算单」,本身不推送金蝶) + +预付结算单**本身不推送金蝶**(列表无金蝶状态字段),它是进度预付和尾款结算(付款结算单)的前置单据,与金蝶的关联有两处:①非人民币项目在此维护汇率(从金蝶自动带出);②进度预付款在金蝶付讫后自动回写本单的垫资记录。 + +![预付结算单列表:工具栏「新增 / 预付申请 / 尾款结算 / 打印结算单 / 导出」,需先勾选才能点预付申请或尾款结算;行内操作按状态显示 查看/编辑/删除/提交/通过/驳回/作废](./2026-10-14-金蝶(星瀚)BFI对接操作手册(纯操作版).assets/31-预付结算单列表.png) 1. 制单:「新增」录入结算金额与明细;**非人民币项目**:选择币种和汇率日期 → 系统自动从金蝶带出结算汇率并重算本位币。 - ![预结算单编辑页:汇率日期/结算汇率字段(人民币时禁用为 1.00,外币选择日期后自动带出)](./2026-10-14-金蝶(星瀚)BFI对接操作手册(纯操作版).assets/12-预结算单-汇率字段.png) + ![预付结算单编辑页:汇率日期/结算汇率字段(人民币时禁用为 1.00,外币选择日期后自动带出)](./2026-10-14-金蝶(星瀚)BFI对接操作手册(纯操作版).assets/12-预结算单-汇率字段.png) 2. 提交审批 → 审批「通过」(或驳回后修改重新提交)。 3. 审批通过后的三个出口: - - **发起预付**:工具栏「预付申请」(限应付且已审批、未转正式)→ 生成进度预付付款申请,走 3.4 预付款推送金蝶; - - **转正式结算**:在正式结算单页面新建时选择本预结算单作为来源(可多张合并)→ 转入 3.3 正式结算推送;转正式后本单被锁定(节点显示「已锁定(正式结算)」并回写正式结算单号); - - **尾款结算**:应付单生成尾款结算单号(同样需先转入正式结算推金蝶)。 -4. 注意:**正式结算单的汇率不自动继承预结算单**,转正式后需在正式结算单编辑时重新选汇率日期带出(见 3.8),否则推送金蝶不带汇率。 -5. 金蝶付款回写联动:进度预付在金蝶付讫(paid)后,系统自动在本单垫资记录回写付款金额与金蝶付款单号,并刷新已付/未付汇总——无需手工登记(异常时管理员可用回写接口手工补)。 + - **发起进度预付**:工具栏「预付申请」(限应付且已审批、未转付款结算、同一合同可多选)→ 跳转付款申请新增页,见 4.5; + - **项目预付核销**:付款结算单/预付结算单编辑页「项目预付核销」分区,见 4.7; + - **尾款结算(转付款结算单)**:工具栏「尾款结算」→ 见 4.8;转后本单被锁定(不能再发起预付、不能作废)。 +4. 注意:**付款结算单的汇率不自动继承预付结算单**,转正式后需在编辑时重新选汇率日期带出(见 4.15),否则推送金蝶不带汇率。 +5. 金蝶付款回写联动:进度预付在金蝶付讫(已付款)后,系统自动在本单垫资记录回写付款金额与金蝶付款单号,并刷新「申请预付金额 / 已付款金额」两列——无需手工登记(异常时管理员可用回写接口手工补)。 -### 3.3 结算单推送(应付 / 应收) +### 4.5 进度预付(预付结算单 → 付款申请) -**页面:结算管理 → 正式结算单(应付 / 应收 Tab)** +**页面:结算管理 → 预付结算单 →(预付申请)→ 收付款管理 → 付款管理** -![正式结算单:应付/应收同页两个 Tab;工具栏「同步金蝶」需先勾选一条审批通过的单据;列表含结算汇率、金蝶单据状态、金蝶锁状态等列](./2026-10-14-金蝶(星瀚)BFI对接操作手册(纯操作版).assets/03-正式结算单-同步金蝶.png) +**前提**:预付结算单是**应付**、状态**审批通过**、还没转付款结算。 -前提:结算单已审批通过;非人民币结算单已维护汇率(见 3.8);客商已 synced/exist。 +1. 进「预付结算单」列表,勾选单据(**同一合同**可多选;不满足"应付+审批通过+未转付款结算+同合同"会按钮拦截提示 → 逐项核对:类型改应付/先完成审批/已转单的换其他预付结算单/不同合同的分开勾选)。 +2. 点工具栏「**预付申请**」→ 自动跳到「付款申请新增页」:**付款类型固定为"进度预付"不可改**,所选预付结算单已带入、结算单号只读,"结算金额 / 可付款金额"自动带出。 + +![付款申请表单(进度预付):付款类型=进度预付且锁定,结算单号只读带出,结算金额/可付款金额自动显示,需填申请付款金额、付款方式、收款方](./2026-10-14-金蝶(星瀚)BFI对接操作手册(纯操作版).assets/32-付款申请表单-进度预付.png) + +3. 如果是空表单进来,可点「结算单号」右侧「**选择**」,在弹窗「选择预付结算单」里按结算单号 / 项目 / 合同名称搜索后勾选。 + +![选择预付结算单弹窗:按结算单号/项目/合同名称搜索,列示结算金额与可付款金额,可付款金额为 0 的单选了也付不了](./2026-10-14-金蝶(星瀚)BFI对接操作手册(纯操作版).assets/33-付款申请-选择预付结算单弹窗.png) + +4. 填「申请付款金额」(不超过可付款金额)、「付款方式」(选承兑汇票时要再选汇票台账)、「收款方」→「**保存**」。 +5. 回到「付款管理」列表 → 找到该单 →「提交」→ 审批人「通过」。 + +**状态变化**:付款申请 `草稿 → 审批中 → 审批通过`;随后系统自动推金蝶,金蝶状态 `未生成 → 已生成 → 付款中 → 已付款`(推送与回写见 4.10)。 +**同时发生**:预付结算单列表出现「申请预付金额 / 已付款金额」两列并随付款进度更新;预付结算单仍可继续「尾款结算」。 + +> 若提交/通过后看到黄色警告写着金蝶推送失败:审批结果**不受影响**,稍后自动重推,或到付款管理列表勾选后点「批量同步」。 + +### 4.6 项目预付(不挂结算单) + +**页面:收付款管理 → 付款管理 →(新增付款申请,付款类型=项目预付)** + +**前提**:项目已建、**承运商合同**已建;金额不超过项目额度与客户额度。 + +**入口(注意)**:进「付款管理」列表 → 点「**新增付款申请**」→ 把「**付款类型**」选成「**项目预付**」。 +付款管理工具栏还有一个「预付申请」按钮——那个是**进度预付**(配合预付结算单用的),项目预付**必须**走新增+手选类型。 + +![付款申请表单(项目预付):付款类型=项目预付,需先选所属项目再选合同名称,不显示结算单/结算信息区](./2026-10-14-金蝶(星瀚)BFI对接操作手册(纯操作版).assets/34-付款申请表单-项目预付.png) + +1. 填写:**所属项目**(必选)→ **合同名称**(必选,**选了项目后才能选**,只能选承运商合同)→ 申请付款金额、付款方式、付款方/收款方等。本类型不挂结算单,不显示结算信息与发票相关内容。 +2. 「保存」→「付款管理」列表「提交」→ 审批人「通过」。 + +**会被拦截的情况(提示原文 → 解决)**: +- "申请付款金额不能超过项目额度"(项目额度 = 项目资金需求额度 + **审批通过且在有效期内**的临时额度;按单笔申请金额比较)→ 调小申请金额,或让管理员先审批通过临时额度管理里的临时额度。 +- "申请付款金额不能超过客户额度"(按收款方匹配客商档案的信用额度)→ 调小申请金额,或联系管理员上调该客商档案的信用额度。 +- "项目预付必须选择有效合同"、"付款申请只能选择承运商合同" → 先选所属项目再选合同;只选承运商合同,选了其他类别合同换掉。 +- "申请付款金额不能超过可付款金额"(项目预付的可付款金额=项目资金需求×10000)→ 调小申请金额;确需更大额度先在项目立项/变更里调增资金需求额度。 + +**状态变化**:同 4.5。**务必等到金蝶状态「已付款」**,否则下一步核销(4.7)做不了。 + +**客商风险与付款特批(所有付款申请提交时通用)**: +- 提交时如果**收款方是高风险/黑名单客商**:会被拦截——"收款方[x]为高风险/黑名单客商,必须上传「特批附件」类型附件后再提交审批"。先在附件区上传类型为「特批附件」的文件再提交。 +- **付款特批是否走合同合规系统由开关 `thirdParty.hcs.spec-pay.enabled` 控制(2026-11-03 起默认关闭)**: + - **关闭(当前默认)**:审批通过后**不发起**合规特批、不挂起「特批审核」,直接同步金蝶;触发合规额度超额也不转特批,照常走金蝶。特批附件必传校验保留(高风险/黑名单收款方仍必须上传)。 + - **开启**:触发合规额度超额(或高风险/黑名单)时**不拦截提交**,审批通过后自动发起付款特批,特批完成后才推金蝶(特批流程由实施/管理员配合处理,本文不展开)。 +- 审批人点「通过」后,弹窗提示可能带一句话: + - "审批通过,已同步金蝶:XXX" → 已经推成功(开关关闭时的常见提示); + - "审批通过,已发起付款特批,特批通过后将自动推送履约数据并同步金蝶" → 开关开启且走了特批,等特批完成; + - "审批通过,但自动同步金蝶失败:…" → 审批不受影响,稍后自动重推或手动「批量同步」。 + +### 4.7 项目预付核销(在结算单上抵扣) + +**页面:结算管理 → 预付结算单 / 付款结算单(编辑页「项目预付核销」分区)** + +**前提**:有一笔**已付款**的项目预付(金蝶状态=已付款),且与本次结算单**同一合同**,还有剩余可核销金额。 + +![付款结算单编辑页的「项目预付核销」分区:右上角「选择」按钮带出核销行,列为 预付单号/项目预付金额/可核销金额/本次核销金额/金蝶预付单号/创建人/创建时间;下方发票信息区的「选择收票单」是收票功能,不要点错](./2026-10-14-金蝶(星瀚)BFI对接操作手册(纯操作版).assets/35-项目预付核销分区.png) + +1. 打开结算单(**预付结算单**或**付款结算单**都行)的编辑页,滚动到「项目预付核销」分区。 +2. 点分区右上角「**选择**」(没选合同会提示"请先选择合同")。 +3. 弹窗「**选择项目预付单**」里勾选要抵扣的预付单(已选的行会灰掉,不能重复选)→「确定」。 + +![选择项目预付单弹窗:只列示 同合同+审批通过+金蝶已付款+仍有可核销余额 的项目预付单,列含 预付单号/项目预付金额/可核销金额/金蝶预付单号](./2026-10-14-金蝶(星瀚)BFI对接操作手册(纯操作版).assets/36-选择项目预付单弹窗.png) + +4. 在「**本次核销金额**」列:默认已按"可核销金额"填满,可以改小。 +5. 「保存」。 + +**会被拦截的情况(提示原文 → 解决)**: +- "预付单 XXX 本次核销金额必须大于 0" → 核销行本次核销金额填大于 0 的数(不需要核销就删掉该行)。 +- "预付单 XXX 本次核销金额不能超过可核销金额" → 把本次核销金额改小到不超过该行「可核销金额」。 +- "项目预付核销合计不能超过结算单金额" → 减小各行核销金额,使合计不超过结算单金额。 +- "仅可选择审核通过的项目预付单" → 只能从弹窗候选里选(弹窗已过滤),手工改动数据导致的不合法行删掉重选。 +- "预付单 XXX 尚未付款,项目预付须金蝶付款完成后方可核销" → 等该预付单金蝶状态变「已付款」(4.10 回写)后再核销。 +- "同一项目预付单不可重复核销" → 该预付单已在本单或其他结算单核销过,弹窗里已灰掉的行不能重复选。 +- "项目预付单与结算单合同不一致" → 只核销与结算单同一合同的项目预付单。 + +**状态变化**:结算单「**已付金额(含预付、核销)**」立刻增大、「剩余未付金额」减小;核销金额会同时计入"已申请付款金额"和"已付金额"两个口径;列表上"申请付款金额(含预付)/已收已付合计(含预付)/剩余可付/收付款状态"同步变化。 + +### 4.8 尾款结算(预付结算单转付款结算单,容纳预付) + +**页面:结算管理 → 预付结算单 →(尾款结算)→ 新增付款结算单** + +**前提**:预付结算单**审批通过**,且所选单据**同一项目**、**应付**。 + +1. 进「预付结算单」列表,勾选(可多选,须同项目)→ 点工具栏「**尾款结算**」。 +2. 系统跳到「**新增付款结算单**」页,把所选预付结算单的明细、费用项合并进来。 +3. 检查结算合计区底部的三个金额(**付款结算单才有**): + - **结算合计(原金额)**:费用合计,**始终全额**,不会因为预付/核销而变小; + - **已付金额(含预付、核销)**:把来源预付结算的已付预付 + 本次核销都算进去; + - **剩余未付金额** = 结算合计 − 已付。 + +![编辑付款结算单:结算合计区底部一行显示 结算合计(原金额)/已付金额(含预付、核销)/剩余未付金额,下方依次为 结算明细/附件/项目预付核销/发票信息 分区](./2026-10-14-金蝶(星瀚)BFI对接操作手册(纯操作版).assets/37-付款结算单编辑页-结算合计三口径.png) + +4. 「项目预付核销」区会**自动带出来源预付结算上已核销的行**,可增/删/改金额(口径见 4.7)。 +5. 「保存」(草稿)→「提交」→ 审批人「通过」。 + +**会被拦截的情况(提示原文 → 解决)**: +- "仅审核通过的预付结算单可转付款结算" → 先把预付结算单审批通过再勾选。 +- "尾款结算只能选择同一项目下的预付结算单" → 取消勾选不同项目的单据,只留同一项目。 +- "该预结算单存在在途预付申请,无法尾款结算"(**审批中或已审批但还没付完款的预付申请都算在途**)→ 先把在途预付申请处理完/付完款再来转。 + +**状态变化**:付款结算单 `草稿 → 审批中 → 审批通过`;预付结算单被标记为已转付款结算(来源被锁定:不能再发起预付、不能作废)。随后走 4.9 推送金蝶。 + +### 4.9 结算单推送(付款结算单 / 收款结算单) + +**页面:结算管理 → 付款结算单(应付)/ 收款结算单(应收)** + +![付款结算单列表(应付):工具栏 新增/付款申请/发票登记/同步金蝶/打印结算单/导出;列表含 金蝶单据号/金蝶单据状态/金蝶锁状态/已指定应付/附件同步 列;图中 FJS2026110200002 已同步且指定失败、FJS2026110200001 已同步且已指定应付](./2026-10-14-金蝶(星瀚)BFI对接操作手册(纯操作版).assets/16-付款结算单列表-发票登记与已指定应付列.png) + +![收款结算单列表(应收):与付款结算单同构,工具栏含 同步金蝶;金蝶单据状态需为「已同步」才能开票](./2026-10-14-金蝶(星瀚)BFI对接操作手册(纯操作版).assets/40-收款结算单列表-应收.png) + +前提:结算单已审批通过;非人民币结算单已维护汇率(见 4.15);客商已 synced/exist。 1. **自动路径**:审批「通过」时自动推送(开关开启时)。 -2. **手动路径**:勾选一条单据 → 工具栏「同步金蝶」。 - - 按钮是单选操作,一次推一张。 -3. 成功标志:列表出现**金蝶单号**,状态变 **synced**,并出现「金蝶锁定中」标签。 -4. 失败处理:状态变 failed → 看失败原因 → 常见原因: - - 「请先同步金蝶应付单」类 → 关联结算单还没推(应付/应收/收票有先后依赖,见 3.6); - - 客商编码取不到 → 先完成 3.1; +2. **手动路径**:勾选一条或多条单据 → 工具栏「同步金蝶」。 + - 应付单(A102-2)/ 应收单(A302-2)按结算类型自动分流,无需选择。 +3. 成功标志:列表出现**金蝶单据号**,状态变 **已同步**,并出现「金蝶锁定中」标签。 +4. 失败处理:状态变 **同步失败** → 鼠标悬浮状态标签看失败原因 → 常见原因: + - 「请先同步金蝶应付单」类 → 关联结算单还没推(付款/开票依赖结算单先推,见 4.10/4.11); + - 客商编码取不到 → 先完成 4.1; - 部门金蝶组织编码缺失 → 找管理员维护。 5. 纠偏:如果怀疑推送实际成功但本地状态没更新,等定时勾稽(或联系运维手动触发),系统会自动核对金蝶侧并纠正状态/补单号。 @@ -126,115 +661,252 @@ ![金蝶财务应收单与 TMS 结算单对照:应收金额、费用合计对应,明细行物料/车号/运输货物逐行对应](./2026-10-14-金蝶(星瀚)BFI对接操作手册(纯操作版).assets/bfi-effect-a302-应收单对照.png) -### 3.4 付款申请推送与结果回写 +### 4.10 付款申请推送与结果回写 **页面:收付款管理 → 付款管理** -![付款管理:工具栏「批量同步」推送付款申请;列表自带「金蝶单据号 / 金蝶单据状态 / 金蝶锁状态 / 附件同步」列——图中首行即「已生成 + 金蝶锁定中 + 附件已上传」的已推送单据](./2026-10-14-金蝶(星瀚)BFI对接操作手册(纯操作版).assets/04-付款管理-批量同步.png) +![付款管理:工具栏 新增付款申请/批量同步/申请付尾款/预付申请/导出;付款类型列按 进度预付/项目预付/尾款付款 展示;金蝶单据号/金蝶单据状态/已指定应付/金蝶锁状态/附件同步 列在表格右侧(见下图)](./2026-10-14-金蝶(星瀚)BFI对接操作手册(纯操作版).assets/04-付款管理-批量同步.png) -前提:付款申请已审批通过,且**关联的结算单已推送金蝶(synced)**。 +前提:付款申请已审批通过,且**关联的结算单已推送金蝶(已同步)**(尾款从金蝶应付单下推,必须先推结算单)。 1. 推送: - 自动路径:审批「通过」时自动下推。 - 手动路径:勾选**审批通过**的单据 → 工具栏「批量同步」(一次可多张,单张失败不影响其他)。 - - 预付款(项目垫资/进度垫资)走同样的按钮,系统自动走预付款通道。 -2. 成功标志:状态 **synced** + 金蝶单号 + 锁定标签。 + - 预付款(进度预付/项目预付)走同样的按钮,系统自动走预付款通道。 +2. 成功标志:状态 **已生成** + 金蝶单号 + 锁定标签。 3. 查付款结果: - 正常等定时任务回写(默认每 10 分钟,开关开启时); - 急用点工具栏「**同步付款结果**」(勾选单据后显示;若未见到该按钮,说明当前环境以定时回写为主,等下一轮即可)。 -4. 结果状态流转:synced → **paying**(金蝶付款中)→ **paid**(已付款,含付款记录/凭证信息)/ **closed**(关闭)。 +4. 结果状态流转:已生成 → **付款中** → **已付款**(含付款记录/凭证信息)/ **已关闭**(关闭)。 5. 失败与特殊场景: - - failed → 修复后重推(批量同步按钮即可,系统会自动跳过金蝶已有的); - - 单据在金蝶被删:回调后自动置 failed 并清单号,直接重推; - - 金蝶删了但回调丢了:约 20 分钟后系统自动检测到「查不到」并解锁置 failed,再重推。 + - **生成失败** → 鼠标悬浮状态标签看失败原因,修复后重推(批量同步按钮即可,系统会自动跳过金蝶已有的);系统每 10 分钟也会自动重推。 + - 单据在金蝶被删:回调后自动置生成失败并清单号,直接重推; + - 金蝶删了但回调丢了:约 20 分钟后系统自动检测到「查不到」并解锁置生成失败,再重推。 + - 推送被拦"收款方客商为高风险/黑名单,付款特批审核中(特批审核),特批完成后方可推送金蝶" → 仅付款特批走合规开关(`spec-pay.enabled`,默认关闭)开启时出现:特批通过(当前节点恢复「审批通过」)后自动续推,无需人工操作(特批流程由实施配合处理,口径见 4.6)。 +6. **合同系统联动(履约数据自动推送)**:付款申请审批通过只做**本地履约计划金额累加**(不推合规);**金蝶实际付款(状态「已付款」)回写后**,系统才自动推送履约计划(C103-9)与履约记录(C108)到合同合规系统——受 `exe-stage.sync-enabled`/`perf-record.sync-enabled` 开关控制,效果与核对方法见 4.18。 -推送成功后金蝶侧效果(供确认推送结果时对照): +推送成功后的效果对照(TMS 回填与金蝶侧,供确认推送结果时对照): ![TMS 付款申请与金蝶付款申请单对照:申请付款金额、收款账号、结算方式逐项对应;金蝶单号 FKSQ-… 与 TMS 列表回写的金蝶单号一致](./2026-10-14-金蝶(星瀚)BFI对接操作手册(纯操作版).assets/bfi-effect-a103-付款申请对照.png) ![预付款推送效果:TMS 进度预付在金蝶生成「其他付款申请」,预付款金额、收款账户对应,附件随单上传](./2026-10-14-金蝶(星瀚)BFI对接操作手册(纯操作版).assets/bfi-effect-a103-3-预付款对照.png) +![付款结果回写后付款管理列表效果:金蝶单据状态变「已付款」(FKSQ-000000282),已付款金额 66364.33 自动回填;右侧「已指定应付」列同步展示(指定失败红色可悬浮看原因)](./2026-10-14-金蝶(星瀚)BFI对接操作手册(纯操作版).assets/43-付款管理列表-已付款回写.png) + +![付款结果回写效果:TMS 付款申请(FKD2026110300003)查看页「付款记录」回写实付金额 66364.33/付款单号/付款凭证(出纳付款-桂糖物流-202611-0007),与金蝶付款处理单(上游单据编码 FKSQ-000000282、付款金额 ¥66,364.33、票据状态已付款)逐项对应](./2026-10-14-金蝶(星瀚)BFI对接操作手册(纯操作版).assets/image-20261103185134875.png) + > 注意:推送后单据被金蝶锁定,**不能改金额、不能作废**。发现金额错误必须先金蝶侧处理。 -### 3.5 开票申请推送与开票结果 +### 4.11 开票申请推送与开票结果 **页面:收付款管理 → 开票管理** ![开票管理:工具栏「批量同步」逐张推送(按发票张次拆单);搜索区可按「金蝶单据状态」筛选](./2026-10-14-金蝶(星瀚)BFI对接操作手册(纯操作版).assets/05-开票管理-批量同步.png) -前提:开票申请已审批通过,且**关联结算单的应收单已推送金蝶**。 +前提:有一张**收款结算单**(应收),**审批通过**、未作废、**剩余可开票金额 > 0**,且已完成 4.9 应收推送(开票前系统会先检查金蝶里有没有这张单的应收单)。 +> 已放开"必须未开票"的限制:**部分开票过的结算单只要还有剩余可开票金额,可以再次选择继续开**,累计额度自动控制(可开票金额=结算金额 − 已被其他未作废开票申请占用的金额)。 -1. 推送:审批「通过」自动推送,或勾选后工具栏「批量同步」(逐张处理)。 +1. 新增:进「开票管理」列表 → 点「新增」→ 「结算单」右侧「**选择**」→ 弹窗「**选择应收正式结算单**」→ 按结算单号/项目/合同搜索并勾选 →「确定」(**合并开票必须同一合同**)。 + +![选择应收正式结算单弹窗:列示 结算金额/已申请开票金额/可开票金额,已申请开票金额是所有未作废开票申请的累计占用](./2026-10-14-金蝶(星瀚)BFI对接操作手册(纯操作版).assets/39-选择应收正式结算单弹窗.png) + + - 「项目」「开票单位」自动带出;受票方开票信息按客商档案自动带出。 +2. 填「发票类型」等;选**电子专票**时,纳税人识别号、开户行、开户账号、注册地址**必填**。 +3. 填「开票内容」(发票张次 + 商品行),系统自动算「**本次开票金额**」;注意"运费不能与其他费用合并开在同一张发票中"。 + +![开票申请新增页:开票基本信息 + 开票内容(按发票张次分 sheet)+ 结算信息](./2026-10-14-金蝶(星瀚)BFI对接操作手册(纯操作版).assets/38-开票申请表单.png) + +4. 「结算信息」表里的「**分摊发票金额**」由**系统按选择顺序自动分摊**(先选的先摊、摊满为止),该列**只读**;点表单底部「**同步**」可刷新分摊结果。 +5. 「保存」→「提交」→ 审批人「通过」→ 系统按发票张次逐张推金蝶(自动路径开关开启时;或勾选后工具栏「**批量同步**」逐张处理)。 - 系统按**发票张次**拆分:一张申请 3 张发票会生成 3 张金蝶开票申请单。 -2. 成功标志:状态 **synced**,金蝶单号多张时逗号拼接显示;逐张锁定。 -3. 查开票结果:等定时回写(默认每 10 分钟),状态变 **invoiced**(终态)。 -4. 查看发票文件:正式结算单详情 →「开票发票」区 → 查看/下载(PDF 优先)。 +6. 成功标志:状态 **已同步**,金蝶单号多张时逗号拼接显示;逐张锁定。 +7. 查开票结果:等定时回写(默认每 10 分钟),状态变 **开票完成**(终态,全部发票张次都有回写结果才到这一步)。 +8. 查看发票文件:正式结算单详情 →「开票发票」区 → 查看/下载(PDF 优先)。 + +**会被拦截的情况(提示原文 → 解决)**: +- "只能选择审批通过、未作废的应收正式结算单" → 先把收款结算单审批通过/不作废,再重新选单。 +- "合并开票的结算单必须属于同一合同" → 取消不同合同的结算单,分两张开票申请(每张限同一合同)。 +- "本次开票金额不能超过可开票金额" / "本次开票金额必须大于 0" → 调小开票内容金额到剩余可开票金额以内;负数/0 修正为有效金额。 +- "电子专票的纳税人识别号/开户行/开户账号/注册地址…"(必填校验)→ 到客商档案补全发票信息(受票方开票信息),或改选其他发票类型。 +- "运费不能与其他费用合并开在同一张发票中" → 运费单独放一张发票(增加发票张次分开放)。 +- 推金蝶时提示"正式结算单[x]财务应收单未在金蝶生成,请先在正式付款单页面同步金蝶" → 先做 4.9 应收推送,成功后再推开票。 + +**限制**:开票申请一旦推到金蝶(已同步/开票完成)就**锁定不能改**;数电票已经开出来的只能走红冲,不能在本系统作废。**预付和核销不影响开票金额**,开票始终按结算单的**全额**来匹配。 推送成功后金蝶侧效果(供确认推送结果时对照): ![开票申请按发票张次拆分:TMS 开票内容第1张/第2张分别生成金蝶开票申请单,购销方、不含税金额逐项对应](./2026-10-14-金蝶(星瀚)BFI对接操作手册(纯操作版).assets/bfi-effect-a303-1-开票张次对照.png) -### 3.6 收票登记(进项发票) +### 4.12 收票登记(进项发票·先票后款) -**页面:收付款管理 → 收票管理** +**页面:收付款管理 → 收票管理(先票)+ 结算管理 → 付款结算单(后款)** -![收票管理:工具栏「批量同步」推送收票单并指定应付](./2026-10-14-金蝶(星瀚)BFI对接操作手册(纯操作版).assets/06-收票管理-批量同步.png) +收票是**两段式**: -**硬性前提**:一张票只能登记一次;推送前必须已分摊到**恰好一张审批通过的应付结算单**,且该结算单已推送金蝶。 +- **先票(收票登记)**:只做「收票」——从金蝶票池选发票推金蝶收票单,**不关联结算单/合同**; +- **后款(付款结算单)**:在付款结算单上认领收票单,应付单推送金蝶后系统自动完成「指定应付」。 -1. 选票:表单里点发票号放大镜 → 弹出「查询金蝶票据池」→ 勾选 → 确认(带出票面信息)。 +![收票管理:工具栏「批量同步」推送收票单(终态已推送);不再需要先关联结算单](./2026-10-14-金蝶(星瀚)BFI对接操作手册(纯操作版).assets/06-收票管理-批量同步.png) + +**第一步:先票——收票登记** + +**硬性前提**:一张票只能登记一次;表单「结算组织」必选(一般选合同的承办部门);发票销方需已在客商建档(系统按销方名称取金蝶供应商代码,取不到推送会被拦)。 + +1. 选票:点「新增」→ 表单里点发票号放大镜 → 弹出「查询金蝶票据池」→ 勾选 → 确认(带出票面信息,并自动带出**发票明细**区块)。 ![查询金蝶票据池弹窗:按发票号/日期/税号/票种等筛选;每行可「查看/下载」发票原件(PDF 优先);点「选择」带出到表单](./2026-10-14-金蝶(星瀚)BFI对接操作手册(纯操作版).assets/07-收票登记-金蝶票池选票.png) - - ![img.png](./2026-10-14-金蝶(星瀚)BFI对接操作手册(纯操作版).assets/img.png) + + ![金蝶侧收票管理·全票池查询:TMS 票池选票的数据源头](./2026-10-14-金蝶(星瀚)BFI对接操作手册(纯操作版).assets/img.png) - 票池数据来自定时同步(默认每 30 分钟);票号精确搜索时若本地没有会**自动去金蝶现查**。 - - 查不到票 → 等同步或让运维检查票种白名单配置/手动触发同步(见 4.2)。 -2. 保存:系统自动**重查金蝶票池校验可用性**——票已被别人登记/认领/红冲/作废会保存失败并提示原因。 -3. 分摊应付结算单后提交审批。 -4. 推送:审批「通过」自动推送,或勾选 → 工具栏「批量同步」。 -5. 状态流转:synced→(系统内)→ **pushed**(金蝶收票单已生成)→ **assigned**(已指定应付,终态)。 - - 卡在 pushed 不用管:定时任务会自动补发「指定应付」。 + - 查不到票 → 等同步或让运维检查票种白名单配置/手动触发同步(见 5.2)。 +2. 选**结算组织**(合同承办部门)——推送金蝶时按它确定结算组织编码,必填。 +3. 保存:系统自动**重查金蝶票池校验可用性**——票已被别人登记/认领/红冲/作废会保存失败并提示原因。 +4. 提交审批 → 审批「通过」自动推送,或勾选 → 工具栏「批量同步」。 +5. 成功标志:状态 **已推送(pushed)**(金蝶收票单已生成)。**到这里收票就完成了**——指定应付由付款结算单负责(见第二步)。 - failed 且提示发票状态异常(红冲/作废)→ 该票在金蝶已不可用,需人工处理,不要重推。 + - failed 且提示「供应商代码/往来户」→ 发票销方未建档或未同步金蝶,先完成 4.1 客商建档再重推。 + - failed 且提示免税/零税率行 → 该票存在系统无法映射的税率行,人工核实后联系金蝶方约定编码。 + - failed 且提示结算组织/平台公司类 → 手选结算组织部门上溯不到带金蝶编码的平台公司,管理员补维护组织档案后重推。 6. 附件:表单附件区可查看/下载发票原件(系统自动从金蝶取,PDF 优先)。 +![收票登记表单(先票后款改造后):发票号码选金蝶票据池发票带出票面与「发票明细」;「结算组织(合同承办部门)」必选;「结算信息」区只读——认领关系由付款结算单侧维护](./2026-10-14-金蝶(星瀚)BFI对接操作手册(纯操作版).assets/13-收票登记-先票后款表单.png) + +**第二步:后款——付款结算单认领收票单并指定应付** + +前提:收票登记已审批通过且已推送(pushed);付款结算单为审批通过状态。认领有三条入口,效果一致: + +**入口 A:编辑/新增时认领(编辑器)** +1. 付款结算单编辑页发票信息区点「**选择收票单**」→ 候选弹窗(**分页**,按发票号/销方/购方搜索)只列出**审批通过且未被其他付款结算单认领**的收票单;行内「推送状态」悬浮可看该票上次推送失败的原因。 + +![选择收票单弹窗(新增/编辑付款结算单入口):分页列示候选收票单(发票号码/开票日期/发票类型/开票单位(销方)/受票单位(购方)/发票金额(含税)/推送状态),底部提示"仅列出审批通过且未被其他付款结算单认领的收票单",勾选后「确定」带回](./2026-10-14-金蝶(星瀚)BFI对接操作手册(纯操作版).assets/18-选择收票单弹窗-分页候选.png) + +2. 勾选后确认 → 发票信息区出现该票行,「**匹配结算单金额**」**默认=发票金额全额,可手动改小/改回**(0 ≤ 金额 ≤ 发票金额)。 +3. 保存:系统按发票号同步认领关系——一张收票单只能被一张付款结算单认领(重复认领会拦截);**不再校验「匹配合计 ≤ 结算金额」**,匹配合计可以超过结算金额(覆盖即视为完成收票)。 + +**入口 B:审批后补认领(工具栏「发票登记」)** +1. 付款结算单列表勾选一条**审批通过**的付款结算单 → 工具栏「**发票登记**」。 +2. 弹窗上半部是**结算基本信息**(结算单号/结算总金额/已匹配发票金额/金蝶同步状态等);下半部「认领收票单」表**自动回显本单已认领的收票单**(带「已认领」标记、匹配金额只读、不可移除)。 +3. 点「**选择收票单**」追加新票(同入口 A 的候选弹窗)→ 新增行「匹配结算单金额」默认=发票金额,可修改 → 点「**保存认领**」。 +4. 弹窗「**是否将发票匹配数据推送金蝶?**」→ 确认后系统调金蝶「指定应付」;成功提示「**结算单[FJSxxx]发票登记推送金蝶成功:认领收票单已指定应付**」(提示带结算单号)。 + +![付款结算单「发票登记」弹窗(工具栏入口):结算基本信息 + 认领收票单表回显本单已认领的收票单(「已认领」标记、匹配金额只读),点「选择收票单」追加新票,「匹配结算单金额」新增行可手动修改(默认=发票金额),底部「保存认领」](./2026-10-14-金蝶(星瀚)BFI对接操作手册(纯操作版).assets/17-付款结算单-发票登记弹窗.png) + +**入口 C:查看页「重新推送」(指定失败/pushed 票的一步式重推)** +1. 打开付款结算单**查看页**,发票信息区对 **failed(指定失败)/ pushed(已推送)** 的票显示「**重新推送**」链接(前提:本单审批通过且已同步金蝶)。 +2. 指定失败行的「匹配结算单金额」在查看页也可编辑(改完随重推生效)。 +3. 点「重新推送」→ 确认「确认重新推送发票xxx到金蝶并指定应付?」→ 系统一步式:重推金蝶收票单(A303-2,幂等防重)+ 指定应付(A302-6)→ 完成后自动刷新状态。 + +![付款结算单查看页「发票信息」区:列为 发票号/开票日期/发票类型/发票金额/可匹配发票金额/匹配结算单金额(指定失败行可编辑)/「是否已指定应付」(未指定·指定失败·已指定应付)+操作列 查看/下载/重新推送;下方付款信息区回显付款申请金蝶单号](./2026-10-14-金蝶(星瀚)BFI对接操作手册(纯操作版).assets/19-查看页发票信息-指定应付状态与重新推送.png) + +**系统自动指定应付**:付款结算单推送金蝶(应付单生成,见 4.9)后,系统对**本单认领且已推送**的收票单逐张执行「指定应付」,收票状态推进为 **assigned**(已指定应付,终态)。指定失败不影响结算单推送——失败票用入口 B/C 人工重推兜底。 + +![付款结算单列表「指定失败」悬浮:聚合展示本单各票的失败原因(如"发票xxx:推送金蝶收票单失败:…"),无需翻日志](./2026-10-14-金蝶(星瀚)BFI对接操作手册(纯操作版).assets/16b-付款结算单列表-指定失败原因悬浮.png) + +![付款管理列表「已指定应付」列:按付款申请关联的结算单聚合展示 已指定应付(绿)/指定失败(红)/未指定,失败原因悬浮可见](./2026-10-14-金蝶(星瀚)BFI对接操作手册(纯操作版).assets/20-付款管理-指定失败原因悬浮.png) + +**口径与注意**: +- 一张收票单只能被一张付款结算单认领;匹配金额合计可超结算金额(覆盖即完成收票)。 +- 收票单一直卡在 pushed:说明还没被任何付款结算单认领(或认领它的结算单应付单还没推送成功),认领并推送后自动接上;已认领但指定失败的,用入口 B/C 重推。 +- ⚠️ **定时自动补发指定默认关闭**(2026-11-02 起):收票结果回写任务不再自动对「pushed+已认领」的票补发指定应付(开关 `thirdParty.bfi.receivedInvoiceResult.assignCompensationEnabled`,默认 false),**由人工入口兜底**(发票登记推送 / 查看页重新推送);需要恢复自动补发时由运维开启该开关(见 5.1)。 +- 结算单作废/移除认领**不会**自动通知金蝶解除指定,需金蝶侧人工处理后重推。 + 推送成功后金蝶侧状态与效果(供确认推送结果时对照): ![收票管理列表「金蝶状态」列:已指定应付(终态)/推送失败(含失败原因)/未推送](./2026-10-14-金蝶(星瀚)BFI对接操作手册(纯操作版).assets/bfi-effect-收票金蝶状态列表.png) ![推送的单据附件已上传到金蝶侧单据的「附件」区](./2026-10-14-金蝶(星瀚)BFI对接操作手册(纯操作版).assets/bfi-effect-a902-附件上传金蝶侧.png) -![正式结算单详情「发票信息」区:认领的发票可 查看/下载(系统从金蝶取原件,PDF 优先);付款信息区回显付款申请金蝶单号](./2026-10-14-金蝶(星瀚)BFI对接操作手册(纯操作版).assets/bfi-effect-a801-2-发票查看下载.png) +![结算单详情「发票信息」区:认领的发票可 查看/下载(系统从金蝶取原件,PDF 优先);付款信息区回显付款申请金蝶单号](./2026-10-14-金蝶(星瀚)BFI对接操作手册(纯操作版).assets/bfi-effect-a801-2-发票查看下载.png) -### 3.7 收款认领 +### 4.13 结算调整单(结算单金额调整) + +**页面:结算管理 → 结算调整单** + +正式结算单(付款/收款结算单)**审批通过后不允许直接改金额**,一切费用调整都必须通过「结算调整单」完成: + +- **作用**:对一张已审批通过的正式结算单,调整其费用项目和金额(调增、调减可为负数、新增手工费用)。 +- **单号规则**:`TZ + 日期 + 4 位流水`,保存草稿时自动生成。 +- **只能手工新增**;**没有作废功能**:一旦提交走审批,错了只能"驳回 → 修改"或重新建单。 +- **支持附件材料**:新建/编辑时可上传附件(单个 ≤500M,最多 20 个)。 +- 按钮只有一枚、按金蝶状态自动分流:**未同步/同步失败 → 「推送金蝶」(直接执行);已同步 → 「重新推送(高危)」(红色,有二次确认)**。 + +![结算调整单列表:草稿行显示「查看 / 编辑 / 删除 / 提交」,已驳回行为「查看 / 编辑 / 提交」,审批中行为「查看 / 通过 / 驳回」,审批通过行只剩推送类操作;金蝶状态为「已同步」时按钮变为红色的「重新推送(高危)」,未同步/同步失败时为「推送金蝶」](./2026-10-14-金蝶(星瀚)BFI对接操作手册(纯操作版).assets/21-结算调整单列表-各状态操作入口.png) + +**第一步:新增调整单** + +前置:必须先选定一张**可调整的正式结算单**——① 审批通过;② 未完成收款/付款;③ 未发起尾款结算/尾款付款;④ 无生效中的结算类付款申请关联。不满足的单在选单列表里搜不到。 + +1. 「新增」→ 选定结算单后自动回带客商、结算类型、合同、项目、原结算金额,并带出原单费用明细;在"调整金额(含税)"列填数(**可为负数**),或点「添加费用」新增手工费用行。 +2. 选单后系统做一次**金蝶冲销预检(软提示,不拦人)**:① 原单未推送金蝶 → 提示"审批通过后将自动推送原单";② 已推送且旧单已冲销 → 提示"审批后可直接重新推送新单";③ 已推送且旧单未冲销 → 黄色警告"审批通过后需先由财务在金蝶完成冲销,才能重新推送";④ 金蝶查不到/超时 → 提示"未校验"。 + +![新增调整单:选定关联正式结算单后自动回带客商、结算单类型、合同、项目、原结算金额,带出原单费用明细并录入调整金额;顶部同时弹出金蝶冲销预检提示](./2026-10-14-金蝶(星瀚)BFI对接操作手册(纯操作版).assets/22-结算调整单新增-选单回带与费用录入.png) + +![选单后的金蝶冲销预检:该结算单已推送金蝶且旧单未冲销时,顶部弹出黄色警告"该结算单已推送金蝶且旧单尚未冲销,审批通过后需先由财务在金蝶完成冲销,才能重新推送",但建单不受影响](./2026-10-14-金蝶(星瀚)BFI对接操作手册(纯操作版).assets/23-结算调整单新增-金蝶冲销预检提示.png) + +3. 编辑弹窗底部「取消 / 同步 / 保存」:**同步**=重新拉取关联结算单最新费用明细(已录入的调整金额保留);**保存**=存草稿。 +4. 提交:草稿/已驳回 → 审批中(提交前校验至少一条调整费用、费项必填等)。 + +**第二步:审批(★核心分流点)** + +审批人点「通过」先弹确认"**审批通过后将更新关联正式结算单,确认继续?**"。确认后系统按**原结算单是否推送过金蝶**自动走两条路: + +| 场景 | 系统行为 | 结果 | +|---|---|---| +| **A:原单未推送金蝶**(最常见,全自动) | ① 调整直接回写原结算单(费用、明细、合计、结算金额全部更新);② 原单追加变更记录;③ 调整单审批通过;④ 重算收付汇总(含核销);⑤ **自动推送金蝶**(应付/应收单 + 落锁 + 附件) | 原单即调整后金额,调整单回填"已同步 + 金蝶单据号";推送失败则置"同步失败",审批结果不受影响,等定时重推或点「推送金蝶」 | +| **B:原单已推送金蝶**(原单冻结) | ① **原单业务数据零改动**,只追加 1 条变更记录;② 调整单审批通过(金蝶状态保持"已同步");③ 前端提示:**"原结算单已推送金蝶,请财务人员在金蝶手工完成旧应付/应收单冲销处理后再重新推送"** | 财务在**金蝶侧手工冲销旧单**后,业务回到 TMS 点「重新推送(高危)」 | + +![审批通过二次确认:明确提示"审批通过后将更新关联正式结算单",确认后系统按原单金蝶状态自动分流](./2026-10-14-金蝶(星瀚)BFI对接操作手册(纯操作版).assets/25-结算调整单审批通过二次确认.png) + +> 为什么已推送场景不自动推:金蝶侧已有一张旧单,直接再推会重复生成应付/应收单;必须先冲销旧单,且冲销只能由财务在金蝶完成,TMS 无法代办。 + +**第三步(场景 B):重新推送(高危)** + +财务在金蝶冲销旧单后,业务点「**重新推送(高危)**」并二次确认,系统依次: +1. **硬闸门**:按原结算单号查金蝶旧单,**必须已冲销**(部分冲销也算已冲销),未冲销/被删除直接拒绝; +2. **克隆一张新的正式结算单**(全新单号)应用调整,直接置为审批通过(新单不再走审批流); +3. **项目预付核销行随迁新单**并从原单删除(避免预付额度被新旧两单双占); +4. 用新单号推送金蝶 → 全新应付/应收单 + 落锁 + 附件; +5. 调整单改挂新单并回填金蝶单据号;原单标记「**已调整**」并解锁旧锁。 +6. 失败处理:已克隆的新单不回滚、置"同步失败";再点「重新推送」会**复用已克隆的新单**从推送步骤续推,不会重复建单(幂等)。 + +![重新推送(高危):确认金蝶旧应付/应收单已由财务手工冲销后才继续,系统将克隆新单并生成全新应付/应收单](./2026-10-14-金蝶(星瀚)BFI对接操作手册(纯操作版).assets/24-结算调整单重新推送-高危二次确认.png) + +完成后列表里**新旧两张正式结算单并存**,带标识区分: + +![正式结算单列表(新单):重推克隆出的单据调整标识显示「调整后单」,来源原单号列显示原单号](./2026-10-14-金蝶(星瀚)BFI对接操作手册(纯操作版).assets/27-正式结算单列表-调整后单标识.png) + +![正式结算单列表(原单):被重推过的原单调整标识显示「已调整」,金蝶单据号保留,此单仅作存档不可再用于付款](./2026-10-14-金蝶(星瀚)BFI对接操作手册(纯操作版).assets/28-正式结算单列表-原单已调整标识.png) + +**失败原因与边界**: +- 调整单「同步失败」:**鼠标悬浮状态标签**直接看失败原因;用「推送金蝶」重试或等定时重推。 + +![金蝶同步失败原因悬浮:状态列「同步失败」标签上悬浮即可看到后端落库的失败原因,无需翻日志](./2026-10-14-金蝶(星瀚)BFI对接操作手册(纯操作版).assets/26-结算调整单金蝶生成失败原因悬浮.png) + +- 调整后结算金额**不得低于该项目预付核销合计**,否则审批(场景 A)或重推克隆(场景 B)直接拒绝,提示"请先调减项目预付核销"。 +- **「已调整」原单只能看不能用**:直接付款和付款申请选单都会被拦截,付款请一律使用调整后的新单。 +- 原单的下游关联(开票申请、收款认领、对账)**不迁移**,留在原单;新单发票状态、收付款状态从零开始(应收新单显示"未开票/未收款"属正常)。 +- 已审批通过的调整单**无法驳回、无法作废、无法撤回**;金额再错就新建调整单二次调整。 + +### 4.14 收款流水与认领 **页面:收付款管理 → 收款流水、认领记录** -这是「金蝶收到钱 → TMS 认领到结算单 → 回推金蝶」的流程: +> 2026-10-29 口径变更:数据源改为**金蝶收款单(A301-1 收款单查询)**;认领**只登记本地,不再推送金蝶**(金蝶收款单在认领前就已存在);认领后自动推送**合同合规系统**(履约计划/认领记录/履约流水),作废自动同步合规作废。原「推送金蝶」「查询金蝶认领结果」按钮已下线。 -1. **同步流水**:收款流水页 → 工具栏「**手动同步流水**」→ 拉取金蝶认领公告。 - ![收款流水:工具栏「手动同步流水」拉取金蝶认领公告、「批量认领」进入认领;列表展示认领通知单/付款人/对方户名等](./2026-10-14-金蝶(星瀚)BFI对接操作手册(纯操作版).assets/08-收款流水-同步与推送.png) - - 查不到数据先检查认领人配置(第 2 节 #4)。 -2. **认领**:点流水进入详情(或工具栏「批量认领」)→ 选择**审批通过且已推送金蝶的应收结算单**、填分摊金额 → 确认。 - - 认领此时只落本地,不推金蝶。 - - 分摊总额不能超过该流水未认领金额。 -3. **推送金蝶**: - - 手动:流水详情/符合条件的行内「推送金蝶」; - - 自动:定时任务(默认每 10 分钟,开关开启时)自动推送认领完成的流水。 - - 成功标志:流水显示金蝶收款单号、推送状态 pushed。 -4. **回写结果**:认领记录页 → 工具栏「**查询金蝶认领结果**」→ 状态推进 approved(金蝶已审核)/ **received**(已收款,终态)。 - ![认领记录:工具栏「查询金蝶认领结果」回写金蝶收款单状态](./2026-10-14-金蝶(星瀚)BFI对接操作手册(纯操作版).assets/09-认领记录-查询金蝶认领结果.png) -5. **作废限制**:已推送金蝶的认领**不能作废**(提示去金蝶侧人工处置)。未推送的可以作废。 +1. **同步流水**:收款流水页 → 工具栏「**手动同步流水**」→ 拉取金蝶收款单(按业务日期窗口,默认今天倒推 60 天,全公司口径)。 + ![收款流水:工具栏「手动同步流水」拉取金蝶收款单、「批量认领」进入认领;列表展示收款单号(即金蝶收款单号)/收款金额(认领上限)/未匹配金额(仅参考)等,无推送相关列](./2026-10-14-金蝶(星瀚)BFI对接操作手册(纯操作版).assets/08-收款流水-手动同步流水.png) + - 报错「组织管理中未维护平台公司金蝶编码」→ 找管理员维护组织档案(第 3 节 #4)。 + - 需要更早的历史流水 → 让运维调大同步窗口天数配置后重新同步。 +2. **认领**:点流水「认领」进入详情(或勾选多条「批量认领」,要求同付款人+对方账号)→ 选择**审批通过的应收结算单**、填分摊金额 → 确认。 + - 认领只落本地,不推金蝶;认领提交后系统自动把本次认领推送到**合同合规系统**(履约计划金额累加 + 认领记录 C107-1 + 履约流水 C108-1,效果见 4.18;推送失败只记标记**不自动重试**,不影响认领本身)。 + - 分摊总额不能超过「剩余可认领金额」(=收款金额−已认领金额),超出会拦截 → 调小本次分摊金额,或先作废之前的认领腾出额度;「未匹配金额」是金蝶参考值,**为 0 不影响认领**。 +3. **查看进度**:金蝶收款单状态(已推送/审核通过/已收款)随每次「手动同步流水」自动刷新,无需回写操作。 +4. **作废**:认领记录页 → 本人记录行内「作废」→ 确认(提示含"同步作废合同合规系统认领记录")→ 本地回退金额 + 自动同步合规作废(合规侧认领记录置为作废 C107-3;**履约计划金额与履约流水不回冲**,见 4.18)。 + ![认领记录:仅显示本人认领记录;行内「作废」回退金额并同步作废合同合规系统认领记录;原「查询金蝶认领结果」按钮已删除](./2026-10-14-金蝶(星瀚)BFI对接操作手册(纯操作版).assets/09-认领记录-列表.png) + - 作废不会影响金蝶收款单本身(金蝶侧红冲由财务人工处置)。 -推送成功后金蝶侧效果(供确认推送结果时对照): +### 4.15 汇率维护(非人民币结算) -![金蝶认领公告与 TMS 收款流水对照:「手动同步流水」拉取的就是金蝶认领公告列表里的 RLTZ 通知单](./2026-10-14-金蝶(星瀚)BFI对接操作手册(纯操作版).assets/bfi-effect-a301-2-公告同步对照.png) - -![TMS 认领记录推送后,金蝶「认领处理列表」生成对应认领单(认领通知单单号一致)](./2026-10-14-金蝶(星瀚)BFI对接操作手册(纯操作版).assets/bfi-effect-a301-3-认领推送对照.png) - -![金蝶认领单详情:认领结果行的核心单据编号=TMS 分摊结算单对应的金蝶单据](./2026-10-14-金蝶(星瀚)BFI对接操作手册(纯操作版).assets/bfi-effect-a301-3-金蝶认领结果.png) - -### 3.8 汇率维护(非人民币结算) - -结算单(预结算/正式结算)编辑时:选择非 RMB 币种 → 选择**汇率日期** → 系统自动从金蝶带出汇率并重算本位币金额。带不出(金蝶该日无汇率)→ 换日期或联系金蝶侧确认。 +结算单(预付结算单/付款结算单/收款结算单)编辑时:选择非 RMB 币种 → 选择**汇率日期** → 系统自动从金蝶带出汇率并重算本位币金额。带不出(金蝶该日无汇率)→ 换日期或联系金蝶侧确认。**预付结算单的汇率不自动继承到付款结算单**,转正式后需重新维护。 ![结算单详情:汇率日期/结算汇率字段由金蝶汇率接口自动带出,本位币金额随之重算](./2026-10-14-金蝶(星瀚)BFI对接操作手册(纯操作版).assets/10-结算单-汇率自动带出.png) @@ -242,82 +914,233 @@ ![金蝶侧效果:汇率 6.7367、金额(本位币)自动换算,与 TMS 结算单的本位币合计一致](./2026-10-14-金蝶(星瀚)BFI对接操作手册(纯操作版).assets/bfi-effect-a801-4-外币汇率推送.png) +### 4.16 作废与撤销:能做什么、状态怎么变 + +| 我想…… | 前提 | 操作位置 | 状态变化 | +|---|---|---|---| +| 作废付款申请 | 状态=审批通过,且**未被金蝶锁定** | 付款管理列表行内「作废」(需填作废原因) | 单据→已作废;**未付的预付记录联动作废,相关项目预付核销行被删除**,受影响结算单的已付金额回落(已付出去的钱不会回退) | +| 作废付款/收款结算单 | **审批通过且未同步金蝶**(已同步金蝶的不能直接作废) | 结算单列表行内「作废」(需填作废原因) | 单据→已作废;本单的项目预付核销行被删除,可核销额度释放;未审批通过的单据不走作废,走驳回后删除 | +| 作废预付结算单 | 审批通过、未转付款结算,且**无关联预付申请**(有则提示"请先作废预付申请") | 预付结算单列表行内「作废」(需填作废原因) | 单据→已作废;预付结算上的核销行被删除 | +| 作废开票申请 | 状态=审批通过,且**未被金蝶锁定**(即还没推金蝶/还没开票) | 开票管理列表行内「作废」 | 单据→已作废;来源结算单的开票状态自动回落 | +| 删除 | 状态=草稿 | 各列表行内「删除」 | 草稿被删除 | +| 金蝶侧删付款申请 | — | 金蝶操作后系统自动处理 | 付款申请金蝶状态→生成失败,金蝶单号被清空;**未付的预付记录联动作废**;人工核实后重推,重推成功会重新建档;**已核销的行不会自动删**,需要人工作废该预付款才回落 | +| 金蝶侧删结算单 | — | 金蝶操作后系统自动处理 | 结算单被**系统自动作废**,同步状态还原,核销行删除 | +| 金蝶侧删开票申请 | — | 金蝶操作后系统自动处理 | 开票申请被**系统自动作废**,来源结算单开票状态回落 | +| 结算调整 | 结算单已审批通过(含已同步金蝶) | 结算调整单(4.13) | 金额调整走调整单:原单标记"已调整"并禁止再付款,调整后的新单自动迁移核销行与金蝶推送 | +| 作废收票认领 | — | 结算单移除认领后金蝶侧人工解除 | 移除认领/结算单作废**不会**自动通知金蝶解除指定,需金蝶侧人工处理(4.12) | + +> 一句话记住:**预付一旦真付出去(已付款),作废不会把钱退回**,只会把台账和核销额度清掉。 + +### 4.17 客商风险与黑名单(查看与筛选) + +**页面:客商管理 → 客商档案(列表/表单)、合同管理列表/详情** + +数据每日 02:30 自动刷新(合同期限内生效合同的甲乙方客商),口径见 1.8。 + +![客商档案列表:展开搜索后可见「客商风险」下拉筛选,列表客商风险列为等级+黑名单组合标签](../hcs合同/2026-10-23-HCS合同管理操作手册(纯操作版).assets/16-客商档案列表-客商风险.png) + +1. **筛选**:搜索栏「展开」→「客商风险」下拉选 黑名单/高风险/中风险/低风险 → 查询。 +2. **看明细**:红色标签**鼠标悬浮**,展示失信类别、列入事由、风险等级。 + +![客商风险悬浮明细:被列入外部黑名单(失信类别、列入事由)与风险等级](../hcs合同/2026-10-23-HCS合同管理操作手册(纯操作版).assets/17-客商风险悬浮明细.png) + +3. **当天新增客商**:客商表单填客商名称失焦即实时查询,名称下方显示红色风险提示(悬浮见明细);列表落库数据次日刷新后生效。 +4. **显示规则**:组合展示等级在前(如「高风险、黑名单」红);仅黑名单/仅高风险红;中风险黄;低风险/无风险绿;内部组织默认「无风险」;未查询到显示「-」。 +5. **合同列表/详情同口径**:实时关联客商档案组装甲乙双方风险,红色标签悬浮看双方明细。 + +![合同列表客商风险列:红色标签悬浮展示甲乙双方风险明细](../hcs合同/2026-10-23-HCS合同管理操作手册(纯操作版).assets/18-合同列表-客商风险悬浮.png) + +> 「客商风险」显示「-」:客商不在合同期限内生效合同的关联范围,或刚创建未被刷新过——等次日 02:30 或联系运维补刷。黑名单命中但标签没显示「黑名单」字样:金蝶黑名单类别/事由数据缺失时只显示等级段,同样等刷新。 + +### 4.18 履约计划与履约记录(付款/收款/开票/收票四场景) + +**页面:无 TMS 页面**——履约数据由系统在各业务节点**自动**推送合同合规系统,业务侧只在**合规系统履约记录列表**核对(按标题前缀检索最快:付款履约- / 收款履约- / 开票履约- / 收票履约-;核对四项:合同、金额、日期口径、条数)。 + +**先看懂两个概念**: +- **履约计划**=合同收付款的**计划台账**(打算收/付多少):来源为合同表单「履约规则」页签;合同没维护时审批通过自动补默认行(承运商合同=付款计划,客户合同=收款计划)。 +- **履约记录**=计划行的**实际发生流水**(真正付/收/开/收了多少)。**先有计划(拿到合规阶段ID)才有记录**;记录必须挂某条计划下——合同没推合规或计划没推成功时记录暂时生成不了,条件满足后自动补上。 + +**计划金额什么时候变**(系统自动,无需操作): + +| 业务动作 | 变化 | 同步合规时点 | +|---|---|---| +| 付款申请审批通过 | 对应付款计划金额 += 申请金额(按款项性质精确匹配,预付款不加进度款) | **金蝶实际付款后**随履约记录一起推 | +| 收款流水认领完成 | 收款计划金额 += 本次分摊金额 | 认领提交后立即推 | +| 合同「履约规则」增删改 | 行级新增/更新/删除标记 | 随合同链/付款链/收款链下一次推送 | + +计划金额是「已批准/已认领」口径,实际发生额看履约记录——两者对不上属正常口径差异。 + +**四场景履约记录对照**(全部自动生成): + +| 场景 | 什么时候生成 | 生成条数 | 金额 | 日期 | 合规检索标题 | +|---|---|---|---|---|---| +| 付款 | **金蝶实际付款回写后**(4.10「已付款」状态就是触发点) | 一张付款申请一条,分次付款更新同一条的实际金额 | 申请金额/实付金额 | 申请日期 | 付款履约- | +| 收款 | **认领提交后**(4.14,事件驱动,失败不自动重试) | 每次认领 × 涉及合同各一条 | 本次分摊金额 | 认领日期 | 收款履约- | +| 开票 | **金蝶开票完成、发票回写后**(4.11 开票完成状态之后) | 每张发票一条 | 发票价税合计 | 发票开票日期 | 开票履约- | +| 收票 | **收票单推合规成功后**(4.12 推送成功后) | 每条结算分摊行(认领关系行)一条 | 分摊收票金额 | 发票日期 | 收票履约- | + +**与本文操作的对应关系与常见误区**: +- 付款:审批通过后去合规找付款履约记录——**还没有**,属正常;金蝶付款(4.10 第 4 步)后才会出现,计划金额累加也同步此时才推(提示「金蝶未实际付款,待付款后推送履约计划与履约记录」是正常结果说明,不是失败)。特批中的付款不推履约,特批通过同样要等实付。 +- 收款:认领提交后自动推(认领记录 C107-1 + 履约流水);**作废认领**只在合规侧把认领记录置作废(C107-3),计划金额与履约流水**不回冲**。 +- 开票:4.11 推送金蝶→开票完成回写→下一轮开票任务自动推;金蝶开票结果回写会自动把开票登记重置为待同步,无需人工重推。 +- 收票:收票登记推送金蝶后收票单推合规(C109)→ 成功后按**已认领的分摊行**自动推;未认领结算单的收票单没有分摊行,不生成履约记录(属正常)。 + +**异常与重试**: + +| 场景 | 失败自动重试 | 业务要做什么 | +|---|---|---| +| 付款/开票/收票 | 是(对应扫描任务每 5 分钟) | 一般无需处理;长期失败联系管理员 | +| 收款 | **否**(事件驱动) | 合规侧没数据时联系管理员排查 | + +暂时生成不了的常见原因(按顺序自查,条件满足后自动补推):合同未推合规 → 履约计划未推成功 → 单据未审批通过 →(开票)金蝶未开票完成 →(收票)收票单未推合规 →(付款)金蝶未实际付款。 + +> 作废/驳回的影响:付款审批驳回不产生记录;付款申请作废、认领作废**不回冲**计划金额与履约记录(认领仅合规侧认领记录作废);需要金额回退在合规侧人工处理或单独提需求。 + --- -## 4. 定时任务与开关(运维视角) +## 5. 定时任务与开关(运维视角) -### 4.1 任务清单(Nacos `thirdparty.bfi.*` 配置) +### 5.1 任务清单(Nacos `thirdparty.bfi.*` 配置) | 任务 | 干什么 | 开关键 | cron 键(默认值) | 手动替代按钮 | |---|---|---|---|---| -| 付款结果回写 | 付款状态(paid/closed)+ 付款记录/凭证回写;顺带应收勾稽纠偏、开票结果回写 | `thirdParty.bfi.payResult.syncEnabled`(样例已开) | `…payResult.cron`(默认 `0 1/10 * * * ?`) | 付款「同步付款结果」、开票结果手动端点、结算勾稽手动端点 | -| 收票结果回写 | 收票单状态回写、pushed 断点补发指定应付 | `…receivedInvoiceResult.syncEnabled` | `…receivedInvoiceResult.cron`(默认 `0 8/10 * * * ?`) | 收票「同步金蝶结果」 | +| 付款结果回写 | 付款状态(已付款/已关闭)+ 付款记录/凭证回写;顺带应收勾稽纠偏、开票结果回写 | `thirdParty.bfi.payResult.syncEnabled`(样例已开) | `…payResult.cron`(默认 `0 1/10 * * * ?`) | 付款「同步付款结果」、开票结果手动端点、结算勾稽手动端点 | +| 收票结果回写 | 收票单状态回写、发票状态异常回收、assigned 单号补登;**pushed+已认领自动补发指定应付受下述开关控制** | `…receivedInvoiceResult.syncEnabled` | `…receivedInvoiceResult.cron`(默认 `0 8/10 * * * ?`) | 收票「同步金蝶结果」、付款结算单「发票登记」/查看页「重新推送」 | +| —— A302-6 指定应付自动补偿 | pushed 且已认领的收票单由回写任务自动重发指定应付(★2026-11-02 起独立开关,**默认关闭**改人工兜底:发票登记推送/查看页重新推送) | `…receivedInvoiceResult.assignCompensationEnabled`(默认 false) | 随收票结果回写任务调度 | 付款结算单「发票登记」/查看页「重新推送」/手动端点 `POST /formal-settlement/invoice-assign-kingdee` | | 客商同步 | 金蝶审核状态复查(synced→exist)、停用联动、failed 重推 | `…customer.syncEnabled` | `…customer.cron`(默认 `0 0 2 * * ?`) | 客商「同步金蝶」 | -| 收款认领推送 | 自动推送认领完成的流水到金蝶 | `…receiptClaimPush.syncEnabled` | `…receiptClaimPush.cron`(默认 `0 4/10 * * * ?`) | 流水行内「推送金蝶」 | -| 全票池同步 | 拉金蝶全票池发票到本地镜像、补绑定 | `…invoiceReceipt.syncEnabled` | `…invoiceReceipt.cron`(默认 `0 16/30 * * * ?`) | 手动端点 `POST /invoice-receipt/sync-invoice-pool` | +| 全票池同步 | 拉金蝶全票池发票到本地镜像(含发票明细)、补绑定 | `…invoiceReceipt.syncEnabled` | `…invoiceReceipt.cron`(默认 `0 16/30 * * * ?`) | 手动端点 `POST /invoice-receipt/sync-invoice-pool` | | failed 重推 | 自动重推所有推送失败的结算/付款/开票/收票单 | `…autoSync.retryEnabled` | `…autoSync.retryCron`(默认 `0 5/10 * * * ?`) | 各页面「同步金蝶」 | | 锁单补偿 | 推送成功但金蝶锁定失败的自动补锁(≤5 次) | `…billLock.retryEnabled` | `…billLock.retryCron`(默认 `0 2/5 * * * ?`) | 手动端点 `POST /formal-settlement/retry-kingdee-bill-lock` | -### 4.2 各任务的运转逻辑 +> 收款认领推送定时任务(原 `receiptClaimPush`)已于 2026-10-29 随 A301-3 链路下线删除;相关 Nacos 配置 `claimAnnounce.*`、`receiptClaimPush.*` 可清理。 + +### 5.2 各任务的运转逻辑 **共性规则(先读这个)** -- **终态不轮询**:付款 paid(已付款)/closed(已关闭)、开票 invoiced(已开票)、收票 assigned(已指定应付)、客商 exist(金蝶已审核)、认领流水 pushed(已推送金蝶)后自动退出扫描范围——任务只处理「还没走完」的单据,越跑越轻,不会重复处理。 +- **终态不轮询**:付款 已付款/已关闭、开票 开票完成、客商 exist(金蝶已审核)后自动退出扫描范围;收票 已指定应付(终态)仅剩补登单号——任务只处理「还没走完」的单据,越跑越轻,不会重复处理。 - **定时与手动同一套逻辑**:每个任务与它的「手动替代按钮」调用同一个服务方法,效果完全一致;区别只是触发方式。 -- **单条失败不拖批次**:一轮里某条单据失败只记录原因,同批其余照常处理;失败的下轮自动再试(兜底见 ⑥ failed 重推)。 -- **默认执行时间错峰**(付款 :01、认领 :04、failed 重推 :05、收票 :08、锁补偿每 5 分钟的 :02、票池每 30 分钟的 :16),互不挤占;自行改 cron 时建议保留错峰。 +- **单条失败不拖批次**:一轮里某条单据失败只记录原因,同批其余照常处理;失败的下轮自动再试(兜底见 failed 重推)。 +- **默认执行时间错峰**(付款 :01、failed 重推 :05、收票 :08、锁补偿每 5 分钟的 :02、票池每 30 分钟的 :16),互不挤占;自行改 cron 时建议保留错峰。 **① 付款结果回写(10 分钟)** -扫「审批通过且已推送 / 付款中 / 推送失败」的付款申请 → 到金蝶查付款申请单和付款处理单 → 推进 synced(已推送受理)→ paying(金蝶付款中)→ **paid(已付款)/ closed(已关闭)**,paid 时回写付款记录/凭证,并联动刷新预结算垫资、结算付款记录(3.4 第 3 步的「正常等定时回写」就是它)。 -- 自愈一(应收勾稽):已推送但失败、或缺金蝶单号的**应收结算单**,查金蝶有则自动补单号、纠回 synced——「实际推成功但本地显示失败」的场景自动痊愈; -- 自愈二(开票结果):已受理的开票申请自动落发票信息并推进 **invoiced(已开票,终态)**(3.5 第 3 步的定时回写就是它); -- 防呆(金蝶删单):synced(已推送受理)/paying(金蝶付款中)的单据**连续 2 轮(约 20 分钟)在金蝶查不到** → 判定金蝶侧已删,自动解锁置 failed(推送失败)并清空金蝶单号,等人工核实后重推;重推带幂等预检,金蝶单据其实还在时会自动回填,不会重复生成。 +扫「审批通过且已推送 / 付款中 / 推送失败」的付款申请 → 到金蝶查付款申请单和付款处理单 → 推进 已生成 → 付款中 → **已付款 / 已关闭**,已付款时回写付款记录/凭证,并联动刷新预付结算垫资、结算付款记录(4.10 第 3 步的「正常等定时回写」就是它)。 +- 自愈一(应收勾稽):已推送但失败、或缺金蝶单号的**应收结算单**,查金蝶有则自动补单号、纠回已同步——「实际推成功但本地显示失败」的场景自动痊愈; +- 自愈二(开票结果):已受理的开票申请自动落发票信息并推进 **开票完成(终态)**(4.11 第 7 步的定时回写就是它); +- 防呆(金蝶删单):已生成/付款中的单据**连续 2 轮(约 20 分钟)在金蝶查不到** → 判定金蝶侧已删,自动解锁置生成失败并清空金蝶单号,等人工核实后重推;重推带幂等预检,金蝶单据其实还在时会自动回填,不会重复生成。 **② 收票结果回写(10 分钟)** -扫 pushed(金蝶收票单已生成)/ assigned(已指定应付)的收票登记,按近 **30 天**回看窗口查金蝶收票单,一轮处理三种情况: -- 票在金蝶被作废/红冲 → 置 failed(推送失败)并提示人工处理(**不要重推**,见 3.6); -- 卡在 pushed → 自动补发「指定应付」推进 **assigned(已指定应付,终态)**(3.6 说的「卡在 pushed 不用管」就是它); +扫 已推送(pushed)/ 已指定应付(assigned)的收票登记,按近 **30 天**回看窗口查金蝶收票单,一轮处理三种情况: +- 票在金蝶被作废/红冲 → 置 failed(推送失败)并提示人工处理(**不要重推**,见 4.12); +- pushed 且**已被付款结算单认领** → 自动补发「指定应付」推进 已指定应付(终态)——**受开关 `…receivedInvoiceResult.assignCompensationEnabled` 控制,默认关闭**:关闭时跳过补发并记日志,由人工入口(发票登记推送/查看页重新推送)指定;pushed 且未被认领 → 跳过不动(认领后由结算单侧接上,见 4.12); - assigned 但缺金蝶收票单号 → 自动补登单号。 窗口内查不到的保持现状不动(不判删除),由人工重推兜底。 **③ 客商同步(每天 02:00)** 一次扫全部审批通过客商,干三件事: -- **未推送/推送失败** → 先复查金蝶:已有且已审核的直接置 **exist(金蝶已有该客商且审核通过,终态)**(不重复建档);金蝶侧审核中则置 failed(推送失败)提示稍后;确实没有才推送; -- **已推送(synced,已推送受理)** → 金蝶审核通过自动升级 **exist(金蝶已审核,终态)**(3.1 第 5 步的定时升级就是它); +- **未推送/推送失败** → 先复查金蝶:已有且已审核的直接置 **exist(终态)**(不重复建档);金蝶侧审核中则置 failed 提示稍后;确实没有才推送; +- **已推送(synced)** → 金蝶审核通过自动升级 **exist(终态)**(4.1 第 5 步的定时升级就是它); - **金蝶侧停用(enable=0)** → TMS 档案联动停用;金蝶恢复可用**不会**自动启用(单向联动,需人工启用)。 -**④ 收款认领推送(10 分钟)** -扫「认领完成(claimed)且未推送/推送失败」的收款流水 → 按认领人逐人拉最新认领公告**对账**(本地认领金额与公告未认领金额一致才推)→ 推送成功回写金蝶收款单号、流水置 **pushed(已推送金蝶)**(3.7 第 3 步的「自动」就是它)。失败的下一轮自动重试,防重码按分摊结算单生成、重试不变,不会在金蝶重复生成收款单。 +**④ 全票池同步(30 分钟)** +按近 **30 天**增量窗口分页拉取金蝶全票池发票到本地镜像表(含发票明细行),收票选票、发票明细带出、票池查询全部走本地镜像——所以**刚在金蝶开的票要等同步后才能选到**;急用可让运维手动触发同步端点,或用票号精确搜索(会自动去金蝶现查,见 4.12)。 -**⑤ 全票池同步(30 分钟)** -按近 **30 天**增量窗口分页拉取金蝶全票池发票到本地镜像表,收票选票、票池查询全部走本地镜像——所以**刚在金蝶开的票要等同步后才能选到**;急用可让运维手动触发同步端点,或用票号精确搜索(会自动去金蝶现查,见 3.6)。 +**⑤ failed 重推(10 分钟)** +只重推 **failed(推送失败)** 单据(结算 → 付款 → 开票 → 收票四个域依次处理);**unsynced(审批通过但从未推送)不会自动推**——历史存量单不会被上线后批量补推,仍走手动按钮。前置没就绪(如销方供应商代码缺失、镜像明细缺失)会再次 failed、下轮继续;前置就绪后自动续推成功。 +⚠️ 重推**没有次数上限**:根因不修(联行号缺失、组织编码缺失等)会每 10 分钟失败一次刷日志,修复根因是唯一正解(呼应 5.4)。 -**⑥ failed 重推(10 分钟)** -只重推 **failed(推送失败)** 单据(结算 → 付款 → 开票 → 收票四个域依次处理);**unsynced(审批通过但从未推送)不会自动推**——历史存量单不会被上线后批量补推,仍走手动按钮。前置没就绪(如上游应付单还没生成)会再次 failed、下轮继续;前置就绪后自动续推成功。 -⚠️ 重推**没有次数上限**:根因不修(联行号缺失、组织编码缺失等)会每 10 分钟失败一次刷日志,修复根因是唯一正解(呼应 4.4)。 +**⑥ 锁单补偿(5 分钟)** +扫「推送成功但金蝶锁定失败」的锁记录,重试加锁,**最多 5 次**;超过 5 次放弃,需运维调手动补偿端点 `POST /formal-settlement/retry-kingdee-bill-lock`(对应 1.2 的「锁定失败·已达上限」标签)。 -**⑦ 锁单补偿(5 分钟)** -扫「推送成功但金蝶锁定失败」的锁记录,重试加锁,**最多 5 次**;超过 5 次放弃,需运维调手动补偿端点 `POST /formal-settlement/retry-kingdee-bill-lock`(对应 FAQ 的「锁定失败」标签)。 - -### 4.3 审批自动同步开关(按业务域独立) +### 5.3 审批自动同步开关(按业务域独立) `thirdParty.bfi.autoSync.settlement`(结算单)/ `…payment`(付款)/ `…invoiceApply`(开票)/ `…receipt`(收票),默认全关。**代码默认全部关闭,必须在 Nacos 显式开启**;客商档案审批推送不受此开关控制(始终自动)。 -### 4.4 运维注意 +### 5.4 运维注意 - 所有定时任务带**分布式锁**:多实例部署只跑一份;手动按钮和定时共用锁,定时运行中手动触发会直接跳过(属正常)。 - 开了「failed 重推」后,持续失败的单据每 10 分钟会重试一次,**修复根因**(配置/数据)比反复观察更重要。 - 手动触发全票池同步与定时是同一逻辑,数据量大时耐心等待,不要反复点。 +- **A302-6 指定应付补偿开关**默认关闭(安全兜底):pushed+已认领的收票单不会自动变"已指定应付",业务用「发票登记」/查看页「重新推送」人工指定;确需自动补偿时在 Nacos 开 `thirdParty.bfi.receivedInvoiceResult.assignCompensationEnabled=true`。 + +### 5.5 合同系统(HCS)定时任务与开关(Nacos `thirdParty.hcs.*` 配置) + +HCS 推送由**五个推送定时任务**(各 5 分钟、分钟位 0–4 错峰)、**一个客商风险每日刷新任务**和一组手动接口组成;定时任务、手动接口与审批即时推送共用同一套推送逻辑。 + +| 任务 | 扫描对象 | 推送链路(HCS 接口) | 独立开关(默认 false) | 默认 cron | 手动触发 | +|---|---|---|---|---|---| +| 项目立项 C102 | 审批通过/变更审批通过,待推送/失败未超限 | C102-3 判重 → C102-1 新建 / C102-2 更新 | `push.project.sync-enabled` | `0 */5 * * * ?` | `POST /hcs/sync/project/{id}`、`/hcs/sync/project`、`/batch` | +| 合同 C103 | 同上;**同轮补查特批结果**(仅此任务有) | C101-1 风险 → C103-2 附件 → C103-3 判重 → C103-1 起草 / C103-11 更新 → C103-9 履约规则(开关)→ 高风险挂特批 | `push.contract.sync-enabled` | `0 1/5 * * * ?` | `POST /hcs/sync/contract/{id}`、`/hcs/sync/contract`、`/batch` | +| 付款链 C106/C108 | 审批通过付款申请 | `hcs_spec_status=need` 时 C106-3/4 特批(受 `spec-pay.enabled` 门禁,默认关)→ **金蝶实付门禁** → C103-9 计划重推 → C108 履约记录(开关) | `push.payment.sync-enabled` | `0 2/5 * * * ?` | `POST /hcs/sync/payment/{id}`、`/hcs/sync/payment` | +| 开票登记 C111 | 审批通过开票申请 | C111-1 判重 → C111-2 新增 → C111-3 更新(金蝶开票结果回写后自动重置待同步触发) | `push.invoice-application.sync-enabled` | `0 3/5 * * * ?` | `POST /hcs/sync/invoice-application/{id}`、`/hcs/sync/invoice-application` | +| 收票单 C109 | 审批通过收票单 | C109-1 新建 / C109-2 更新 → 成功后按认领分摊行推履约收票记录 | `push.invoice-receipt.sync-enabled` | `0 4/5 * * * ?` | `POST /hcs/sync/invoice-receipt/{id}`、`/hcs/sync/invoice-receipt` | +| 客商风险刷新 | 期限内生效合同甲乙方客商 | 逐个 C101-1 风险等级 + 批量 A801-5 金蝶黑名单,落库客商档案 | `risk.refresh-enabled`(上线置 true) | `0 30 2 * * ?` | 无手动接口(可临时改 cron 补刷) | + +**公共规则**:扫描条件=审批通过 + 待推送/失败未超限(`push.max-retry` 默认 5,单轮 `push.batch-size` 默认 50 条);按业务域抢 Redis 锁(TTL 240 秒),多实例只跑一份;单条失败只记该条不拖批次;**手动接口不检查开关、不抢锁**(开关关闭时可临时补推,避免与定时同一时刻执行)。 + +**各任务运转逻辑** + +**① 项目立项任务(C102,每 5 分钟)** +扫「审批通过/变更审批通过 + 待推送/推送失败」的项目立项: +- **无合规项目ID(首推)**:先调 C102-3 按项目编号判重——命中直接回填已有合规ID置已推送(不重复建);未命中调 C102-1 新建(依赖 `project.template-code`,未配置整条失败),成功后回填合规项目ID。 +- **已有合规项目ID(变更)**:调 C102-2 更新。 +- **金蝶结果回调**(`projectLegal/result`):HCS 完成金蝶同步后按项目编号定位,成功回填「金蝶项目ID/编号」、失败记原因——**不改合规状态、不触发重推**,所以「已推送但金蝶项目ID空」只代表金蝶同步未完成。 +- 付款额度/开票额度按万元×10000 转元推送,计划保证金仍按万元直传。 + +**② 合同任务(C103,每 5 分钟)** +扫「审批通过/变更审批通过 + 待推送/推送失败」的合同,单条推送依次: +- **特批挂起门禁**:特批中(processing)的合同跳过推送;手动重推报「合同特批审核中,禁止重推」。 +- **客商风险 C101-1 + 金蝶黑名单 A801-5**:甲乙双方逐个查(客商档案匹配不到或内部组织则跳过该方不阻断);命中只做标记,**不阻断推送**。 +- **附件转传 C103-2**:失败会导致整条推送失败(查下载基址与附件)。 +- **判重/起草/更新**:无合规合同ID先 C103-3 按合同编号判重(命中回填置已推送),未命中 C103-1 起草(报文含业务合同号、归档状态;高风险/黑名单时随报文 `fdSepcailContract=true` 在合规侧发起特批);已有合规ID走 C103-11 更新(变更、归档即时同步同走此接口)。 +- **履约规则 C103-9**(`exe-stage.sync-enabled`):按合同「履约规则」页签的行级新增/更新/删除同步;无可同步行自动跳过。失败整条置 failed——因合规ID已先回填,下轮重推走 C103-11 更新补偿,**不会重复起草**。 +- **高风险挂起特批**:风险命中时置特批中、当前节点「特批审核」;合规回调(秒批/秒驳)先到时不覆盖已落地结果。 +- **★特批结果兜底查询(仅本任务有)**:每轮推完待同步合同后,另查「特批中且有合规ID」的合同(最多 50 条),逐条调 C103-4 查详情:合规侧已通过 → 恢复「审批通过」;已驳回 → 合同审批联动「已驳回」;其他状态继续等。手动接口不补查特批结果,需用「确认特批结果」人工兜底。 + +**③ 付款链任务(C106/C108,每 5 分钟)** +扫「审批通过 + 待推送/推送失败」的付款申请: +- **特批发起**(`spec-pay.enabled=true` 时):待特批(need)且无特批流程ID调 C106-3 发起(依赖 `spec-pay.template-id` 与关联合同的合规ID),已有流程ID调 C106-4 更新;发起成功回填流程ID、置特批中。**开关关闭(当前默认)时整步跳过**。 +- **★金蝶实际付款门禁**:付款审批通过只做**本地履约计划金额累加**;金蝶实付金额为空或 ≤0 时,履约计划与履约记录**都不推送**,整体照常置已推送,结果说明「金蝶未实际付款,待付款后推送履约计划与履约记录」(**正常状态不是失败**);金蝶付款回写(4.10「已付款」)后自动重新置待同步,下一轮扫描自动续推。 +- **实付后推送**:先 C103-9 重推累加后的付款计划行(按款项性质精确匹配),再推履约记录——无履约记录ID调 C108-1 新建(返回ID立即落库),已有则 C108-2 更新(分期付款推进实际金额,一张申请永远一条记录)。 +- **特批结果**:HCS 回调 `paymentLegal/result` 或人工 `POST /hcs/spec-pay/confirm`——通过恢复「审批通过」并触发金蝶自动同步;驳回置「已驳回」,重新提交后重新发起特批。 + +**④ 开票登记任务(C111,每 5 分钟)** +扫「审批通过 + 待推送/推送失败」的开票申请: +- 无合规ID先 C111-1 按申请编号判重,未命中 C111-2 新增——**必须能经应收结算链路回溯到合同编号**,回溯不到整条失败;币种映射 `invoice.currency-id-mapping` 的 RMB 必配,否则整条失败。 +- 新增成功后若金蝶状态已是开票完成,立即调 C111-3 更新票据日期/状态;已有合规ID同样走 C111-3。 +- **自愈**:金蝶开票结果回写(4.11「开票完成」)会把合规状态自动重置为待推送,下一轮扫描自动执行 C111-3 更新——无需人工重推。 +- 本任务**没有结果回调**,以列表合规状态与失败说明为准。 + +**⑤ 收票单任务(C109,每 5 分钟)** +扫「审批通过 + 待推送/推送失败」的收票登记: +- 无合规ID调 C109-1 新建,成功回填合规ID;已有合规ID调 C109-2 更新。 +- **合同关联回溯**:收票单 → 结算关系(付款结算单认领)→ 正式结算单 → 合同;先票后款下**未认领结算单的收票单回溯不到合同**,取人工补录字段(运维),都没有时合同字段传空只记警告、**不阻断推送**。 +- 推送成功后按**认领分摊行**逐条推履约收票记录(C108-1,每条分摊行一条);未认领的没有分摊行,不生成履约记录(属正常,见 4.18)。 + +**⑥ 客商风险每日刷新任务(C101-1 + A801-5,每天 02:30)** +- 刷新范围:**合同期限内**(起止或临时期限覆盖当天)且审核通过、未终止合同的甲乙方关联客商;内部组织客商跳过(默认无风险)。 +- 外部客商按 **200/批**:每批调一次 A801-5 金蝶黑名单批量查询 + 逐个调 C101-1 风险等级(该接口仅支持单编号);结果立即落库客商档案(风险等级、金蝶黑名单类别/事由、查询时间)并留痕校验日志;**批间互不影响,单批失败不中断其余批次**。 +- 该任务**不走推送框架**:不抢推送锁、不改单据合规状态、无手动触发接口;临时补刷可把 `risk.refresh-cron` 改为近时刻执行后改回。 +- 日志按「定时刷新客商风险」检索。 + +**关键业务开关**:`push.immediate-enabled`(项目/合同审批即时推送);`spec-pay.enabled`(★付款特批走合规,默认 false,见 4.6);`exe-stage.sync-enabled`(履约计划 C103-9,合同/付款/收款三条链共用);`perf-record.sync-enabled`(履约记录 C108,默认 false);`contract.require-risk-check`、`risk.check-on-payment`、`credit.check-enabled`(风险与额度校验);`attachment.transfer-enabled` + `attachment.file-download-base`(附件转传);`mock-enabled` 生产必须 false。 + +**运维注意**: +- 开关默认全关、按域灰度打开;**开启扫描前先看存量**——扫描不区分"从未推送/推送失败",一开所有审批通过且未同步的单据都入队(每轮 50 条)。 +- 失败每 5 分钟自动重试、超 5 次停止;人工重推清零计数,但**修复根因**(模板映射/币种映射/附件基址)才是正解。 +- **付款依赖合同**:付款特批与履约规则都以合同合规ID为前提;履约数据以**金蝶实际付款**为门禁——结果说明「金蝶未实际付款,待付款后推送履约计划与履约记录」是正常状态,不要当失败处理。 +- 开票登记必须配 `invoice.currency-id-mapping` 的 `RMB`,否则每轮失败。 +- 模板映射调整后可调 `POST /hcs/master-data/refresh` 刷新模板/分类/币种缓存并校验映射;日志按单据编号/`hcs` 关键字检索(本地目录 `F:\home\tms\blade-transport`),客商风险刷新按「定时刷新客商风险」检索。 --- -## 5. 常见问题与异常处理(FAQ) +## 6. 常见问题与异常处理(FAQ) | 现象 | 原因 | 处理 | |---|---|---| -| 推送结算单不带汇率 | 正式结算单没维护汇率(预结算单的汇率**不自动继承**到正式结算单) | 在正式结算单编辑页重选汇率日期自动带出后再推 | -| 推送报「客商编码」类错误 | 客商未推送金蝶或档案缺数据 | 先完成客商同步(3.1),再推单据 | -| 推送报「请先同步金蝶应付单/应收单」 | 前置单据未推送 | 顺序:结算单(应付/应收) → 付款/开票/收票;先把前置结算单推送 | +| 推送结算单不带汇率 | 结算单没维护汇率(预付结算单的汇率**不自动继承**到付款结算单) | 在结算单编辑页重选汇率日期自动带出后再推 | +| 推送报「客商编码」类错误 | 客商未推送金蝶或档案缺数据 | 先完成客商同步(4.1),再推单据 | +| 推送报「请先同步金蝶应付单/应收单」 | 前置单据未推送 | 顺序:结算单(应付/应收) → 付款/开票(收票登记已不依赖结算单,仅付款结算单指定应付需要应付单先推送) | | 推送报组织/部门编码类错误 | 部门没维护金蝶组织编码 | 管理员补维护后重推 | | 推送报联行号类错误 | 客商收款账户联行号缺失 | 财务补维护收款账户后重推 | | 提交客商审批被拦黑名单 | 纳税人识别号命中国资委黑名单(A 类) | 不可提交,走线下处置;B 类提示仅告警默认放行 | @@ -325,44 +1148,92 @@ | 列表「锁定失败」标签 | 推送成功但金蝶锁单接口失败 | 系统自动补偿(≤5 次);急用让运维调手动补偿端点 | | 单据要改但显示「金蝶锁定中」 | 已推送并被金蝶锁定 | 到金蝶侧解锁/删单 → TMS 自动还原后即可编辑;不可强行作废 | | 付款状态长时间不更新 | 回写定时未开启或金蝶侧未处理 | 点「同步付款结果」;仍无 → 金蝶侧查付款处理单 | -| 开票申请 synced 但没发票信息 | 开票结果未回写 | 等定时/联系运维手动触发;确认金蝶票易通已开票 | +| 付款申请审批通过了,但金蝶那边没有单 | 推送失败(生成失败)或走了付款特批 | 看「金蝶单据状态」:生成失败勾选后点「批量同步」或等 10 分钟自动重推;提示"已发起付款特批"(特批走合规开关开启时)要等特批完成 | +| 开票申请已同步但没发票信息 | 开票结果未回写 | 等定时/联系运维手动触发;确认金蝶票易通已开票 | | 票池选票查不到 | 镜像未同步到该票种 | 检查票种白名单配置;运维手动触发全票池同步;票号精确搜索可触发自动现查 | | 保存收票登记被拦「可用金额为0」 | 票已被占用/认领/红冲 | 换票或在金蝶确认票据状态 | -| 收票卡在 pushed 不动 | 「指定应付」步骤未完成 | 开启收票回写定时自动补发,或重推一次 | -| 收款流水同步为空 | 认领人配置与金蝶不一致 | 检查 Nacos `claimAnnounce.claimusers` | -| 认领推送报「认领公告id缺失」 | 流水没同步过就认领了 | 先「流水同步」再操作 | -| 认领推送报金蝶内码缺失 | 分摊的结算单没推送金蝶 | 先完成结算单(应收)推送(3.3) | -| 已推送认领想作废 | 金蝶已生成收款单 | 不能作废,去金蝶侧人工处置 | +| 收票推送报「供应商代码/往来户」类错误 | 发票销方未建档或未同步金蝶(取不到金蝶供应商代码) | 先完成客商建档与同步(4.1),再重推 | +| 收票推送报免税/零税率行 | 系统无法映射该税率行编码 | 人工核实票面,联系金蝶方约定编码后放开 | +| 收票推送报结算组织/平台公司类错误 | 手选部门上溯不到带金蝶编码的平台公司 | 找管理员维护组织金蝶编码后重推 | +| 收票卡在已推送(pushed)不动 | 未被付款结算单认领,或认领它的结算单应付单未推送成功;或已认领但指定失败且自动补偿开关关闭 | 在付款结算单「发票登记」/查看页「重新推送」处理;未认领的先认领;已认领失败的用人工入口重推(4.12) | +| 结算单/付款管理「已指定应付」列显示指定失败 | 认领的收票单 A302-6 指定失败 | **悬浮状态标签看聚合失败原因** → 付款结算单查看页对该票「重新推送」(可先改匹配金额),或工具栏「发票登记」重推 | +| 认领收票单被拦「一张收票单只能被一张付款结算单认领」 | 已被其他有效付款结算单认领 | 先在原结算单移除认领(金蝶侧人工解除指定)再认领 | +| 为什么选不到某张预付结算单发起预付 | 需要**应付**、**审批通过**、**未转付款结算**、且所选单据**同一合同** | 按 4.5 前提检查 | +| 付款管理工具栏的「预付申请」发起的是哪种预付 | 固定是**进度预付**(配合预付结算单) | **项目预付**要点「新增付款申请」后手动把付款类型选成"项目预付"(4.6) | +| 为什么核销弹窗里找不到那笔项目预付 | 必须与结算单**同一合同**、**审批通过**、**金蝶已付款**、还有**剩余可核销额** | 按 4.7 前提检查 | +| 为什么新增开票申请时选不到某张收款结算单 | 需要**审批通过、未作废、剩余可开票金额>0**;跨合同不能合并开票 | 已申请开票金额(未作废的占用)会扣减可开票额;部分开票过的单只要还有余额可以再选 | +| 开票提示要先去同步金蝶 | 开票前系统检查金蝶应收单 | 先在「收款结算单」列表点「同步金蝶」(4.9) | +| 结算单已付金额比我算的多/少 | 已付金额**含**进度预付、项目预付核销;核销金额同时计入"已申请付款金额"和"已付金额" | 属正常口径 | +| 结算单金额写错了怎么改 | 审批通过后不能直接改 | 走「结算调整单」(4.13) | +| 结算调整单选不到某张结算单 | 已完成收付款 / 已发起尾款 / 有关联付款申请在途 / 未审批通过 | 按 4.13 前提检查 | +| 结算调整单重推被拦「旧单未冲销」 | 原单已推送金蝶且财务还没在金蝶冲销 | 财务在金蝶手工冲销旧单后再点「重新推送(高危)」(部分冲销视同已冲销) | +| 结算调整单重推失败,新单显示同步失败 | 克隆新单已生成但推送金蝶失败 | 再点「重新推送」复用新单续推,不会重复建单;失败原因悬浮看新单 | +| 对「已调整」原单发起付款被拦 | 原单已作废(被调整单重推替代) | 使用调整后的新单发起付款 | +| 单据上的「作废」是灰的/没有 | 状态不是审批通过;结算单已同步金蝶;已被金蝶锁定 | 见 4.16 作废前提 | +| 收款流水同步为空/报组织编码错误 | 组织管理未维护「平台公司」金蝶编码,或窗口内无数据 | 找管理员维护组织金蝶编码;需要更早流水时调大同步窗口天数配置 | +| 认领/作废后合规侧没有记录 | 合同合规推送失败(只落标记不重试) | 让运维查合规推送记录失败原因,人工在合规侧补录;认领/作废本身不受影响 | +| 想作废认领 | — | 认领记录页本人记录直接「作废」:本地回退金额并自动同步合规作废;金蝶收款单本身不受影响(红冲由金蝶财务人工处置) | | 断网/金蝶宕机期间推了一半 | 部分单据 failed | 恢复后 failed 重推任务(或手动)自动续推,系统查重不会重复生成金蝶单据 | +| 合同审批通过后合规状态一直「待推送」 | 未开即时推送,合同扫描也未开启或未到执行时间 | 手动点「推送合规」;或让运维开启 `push.contract.sync-enabled`(5.5) | +| 合同推送失败提示「合同类别 [xxx] 未配置合规模板ID」 | Nacos 模板映射缺该类别 | 运维补 `contract.template-mapping` 后重推(注意中文键括号写法) | +| 合同「特批审核」节点长时间不动 | HCS 特批结果未回传 | 详情「合规信息」→「确认特批结果」人工确认 approved/rejected(4.3 第四步) | +| 特批中的合同想重推/想改内容 | 特批未结束;已推送合同不能直接编辑 | 不允许重推,等结果;改动走「变更」流程(4.3 第五步) | +| 合同已推送但金蝶合同ID为空 | HCS 金蝶同步有延迟 | 稍后刷新;长期为空联系管理员 | +| 高风险收款方提交付款被拦截 | 未上传「特批附件」 | 附件区新增一行类型选「特批附件」重新提交(**特批开关关闭也拦截**,属客商准入风控) | +| 付款/合同没触发特批但客商明明是高风险 | 客商名称与客商档案不匹配导致风险校验被跳过;或非高风险且未命中金蝶黑名单 | 核对甲乙方/收款方名称与客商档案全称或简称一致(1.8) | +| 客商档案/合同「客商风险」列显示「-」 | 客商不在合同期限内生效合同关联范围,或刚创建未被刷新过 | 等次日 02:30 刷新;当天可在客商表单实时查询(4.17) | +| 金蝶黑名单命中但列表没显示「黑名单」 | 金蝶黑名单类别/事由数据缺失,只显示等级段 | 等次日刷新或联系运维补刷;确认 `risk.refresh-enabled` 已开启 | +| 合规里没有付款履约计划/记录 | 履约数据以**金蝶实际付款**为门禁,未实付不推 | 正常等待金蝶付款回写后自动推(4.18) | +| 合规里没有收款履约记录 | 认领推送失败**不自动重试** | 联系运维查合规推送记录,人工在合规侧补录;认领本身不受影响 | +| 项目审批通过后合规状态「待推送」 | 未开即时推送或项目扫描未开启 | 行内「推送合规」;或运维开 `push.project.sync-enabled`(4.2) | +| 项目推送成功但金蝶项目ID为空 | HCS 金蝶同步结果未回传 | 稍后刷新;长期为空联系管理员 | --- -## 6. 附:操作入口速查表 +## 7. 附:操作入口速查表 | 页面 | 按钮/操作 | 作用 | |---|---|---| -| 客商档案 | (表单)录信用代码 | 自动查金蝶回填工商信息 | +| 客商档案 | (表单)录信用代码 | 自动查金蝶回填工商信息 + 金蝶供应商编码 | +| 客商档案 | (表单)收款信息开户行 | 失焦按行名从金蝶带出联行号;保存前按行号校验 | | 客商档案 | 行内「同步金蝶」 | 失败重推客商 | -| 预结算单 | 工具栏「预付申请」/「尾款结算」 | 审批通过后发起进度预付 / 生成尾款结算号(预结算本身不推金蝶) | -| 正式结算单 | 新建时选择来源预结算单 | 预结算转正式结算(转后预结算锁定) | -| 正式结算单 | 工具栏「同步金蝶」(单选) | 推送应付/应收单到金蝶 | -| 正式结算单 | (编辑)汇率日期 | 自动带出金蝶汇率 | -| 正式结算单 | (编辑)发票认领「查询/确认识领」 | 从金蝶票池选票认领到结算单 | +| 预付结算单 | 工具栏「预付申请」 | 审批通过后发起**进度预付**(跳转付款申请表单,类型锁定) | +| 预付结算单 | 工具栏「尾款结算」 | 转付款结算单(容纳预付与核销;转后本单锁定) | +| 付款管理 | 工具栏「新增付款申请」→ 类型=项目预付 | 发起**项目预付**(不挂结算单) | +| 预付/付款结算单编辑页 | 「项目预付核销」分区「选择」 | 选择金蝶已付款的项目预付单抵扣(本次核销金额可改小) | +| 付款结算单 | 工具栏「同步金蝶」 | 推送应付/应收单到金蝶(收款结算单同) | +| 付款结算单 | 工具栏「发票登记」 | 审批后认领收票单(回显已认领),保存认领后可推送「指定应付」 | +| 付款结算单查看页 | 发票行「重新推送」 | failed/pushed 票一步式重推收票单+指定应付(可带匹配金额) | +| 付款结算单查看页 | 发票行「匹配结算单金额」 | 指定失败行可编辑匹配金额,随重推生效 | +| 结算单(预付/付款/收款) | (编辑)汇率日期 | 自动带出金蝶汇率 | | 付款申请 | 工具栏「批量同步」 | 下推付款申请/预付款到金蝶 | | 付款申请 | 工具栏「同步付款结果」 | 立即回写付款状态与付款记录 | | 开票申请 | 工具栏「批量同步」 | 按发票张次推送开票申请 | -| 收票登记 | (表单)发票号放大镜 | 从金蝶票池选票 | -| 收票登记 | 工具栏「批量同步」 | 推送收票单+指定应付 | +| 开票申请新增 | 「分摊发票金额」 | 系统按选择顺序自动分摊(只读),底部「同步」刷新 | +| 收票登记 | (表单)发票号放大镜 | 从金蝶票池选票(带出发票明细) | +| 收票登记 | (表单)结算组织 | 手选合同承办部门(必选) | +| 收票登记 | 工具栏「批量同步」 | 推送金蝶收票单(终态已推送;指定应付由付款结算单完成) | | 收票登记 | (表单)附件查看/下载 | 从金蝶取发票原件(PDF 优先) | -| 收款流水 | 工具栏「手动同步流水」 | 拉取金蝶认领公告 | -| 收款流水 | (详情/工具栏「批量认领」)认领 | 本地认领到结算单 | -| 收款流水 | 行内「推送金蝶」 | 推送认领结果到金蝶 | -| 认领记录 | 工具栏「查询金蝶认领结果」 | 回写金蝶收款单状态 | +| 结算调整单 | 「新增/提交/审批」 | 对已审批通过的结算单调整费用金额(原单未推送场景审批后自动推金蝶) | +| 结算调整单 | 「推送金蝶」/「重新推送(高危)」 | 未同步直接推送;已同步需金蝶旧单已冲销后克隆新单重推 | +| 收款流水 | 工具栏「手动同步流水」 | 拉取金蝶收款单(A301-1,窗口默认 60 天) | +| 收款流水 | (详情/工具栏「批量认领」)认领 | 本地认领到结算单(不推金蝶,自动推合同合规) | +| 认领记录 | 行内「作废」 | 回退认领金额并同步作废合同合规系统认领记录 | | 运维专用 | `POST /invoice-receipt/sync-invoice-pool` | 手动全票池同步 | | 运维专用 | `POST /formal-settlement/retry-kingdee-bill-lock` | 手动补偿锁单 | | 运维专用 | `POST /formal-settlement/sync-kingdee-ar-result` | 手动应收勾稽纠偏 | | 运维专用 | `POST /invoice-application/sync-kingdee-invoice-result` | 手动开票结果回写 | +| 运维专用 | `POST /formal-settlement/invoice-assign-kingdee` | 手动触发结算单认领收票单的指定应付 | +| 项目管理 | 行内「推送合规」/工具栏「批量推送合规」 | 推送/重推项目立项到合同合规(C102,4.2) | +| 合同管理 | 行内「推送合规」/工具栏「批量推送合规」 | 推送/重推合同到合同合规(C103,4.3) | +| 合同详情 | 合规信息「确认特批结果」 | 特批中人工确认 approved/rejected(4.3) | +| 合同管理 | 行内「变更」/「附件上传」 | 合同变更更新 HCS(C103-11);补传附件与归档即时同步(4.3) | +| 客商档案 | 搜索「客商风险」筛选 / 红色标签悬浮 | 按风险筛选客商;看失信类别/列入事由/等级明细(4.17) | +| 运维专用 | `POST /hcs/sync/{bizType}/{id}` | 单条重推(bizType=project/contract/payment/invoice-application/invoice-receipt,不受开关限制) | +| 运维专用 | `POST /hcs/sync/{bizType}`(`/batch` 仅 project、contract) | 按扫描逻辑推一轮 / 按勾选ID批量重推 | +| 运维专用 | `POST /hcs/spec-pay/confirm`、`POST /hcs/contract/spec/confirm` | 人工确认付款/合同特批结果(参数 id、result) | +| 运维专用 | `POST /hcs/master-data/refresh` | 刷新 HCS 模板/分类/币种缓存并校验映射 | --- -> 本文基于 2026-10-14 代码现状整理;2026-10-19 依据当前代码核对并补充 4.2 定时任务运转逻辑(cron 默认值、错峰、终态不轮询、防呆与自愈行为)。系统升级后按钮与提示如有出入,以页面实际为准,并核对全链路总纲文档的更新说明。 +> 本文基于 2026-10-14 代码整理;2026-10-19 核对补充定时任务运转逻辑;2026-10-31 按先票后款改造与收款流水 A301-1 改造修订;2026-11-03 汇总《2026-10-17 结算调整单》《2026-10-18 预付与项目预付》两本操作手册,并按 2026-11-01/11-02 代码修订收票指定应付链路(发票登记工具栏入口、匹配金额可改、查看页重新推送、已指定应付列、A302-6 自动补偿开关默认关闭);**2026-11-04 合并合同合规系统(HCS)操作内容(项目立项、合同、特批、客商风险、履约计划/记录),并纳入付款特批走合规开关(`thirdParty.hcs.spec-pay.enabled` 默认关闭,2026-11-03)**。系统升级后按钮与提示如有出入,以页面实际为准,并核对全链路总纲文档与 HCS 手册的更新说明。 diff --git a/doc/docs/业务流程/2026-10-17-结算调整单操作流程说明(分场景).assets/01-结算调整单列表-各状态操作入口.png b/doc/docs/业务流程/2026-10-17-结算调整单操作流程说明(分场景).assets/01-结算调整单列表-各状态操作入口.png index 992bc639..90e1fb45 100644 Binary files a/doc/docs/业务流程/2026-10-17-结算调整单操作流程说明(分场景).assets/01-结算调整单列表-各状态操作入口.png and b/doc/docs/业务流程/2026-10-17-结算调整单操作流程说明(分场景).assets/01-结算调整单列表-各状态操作入口.png differ diff --git a/doc/docs/业务流程/2026-10-17-结算调整单操作流程说明(分场景).assets/02-新增调整单-选单回带与费用录入.png b/doc/docs/业务流程/2026-10-17-结算调整单操作流程说明(分场景).assets/02-新增调整单-选单回带与费用录入.png index 8410d6d1..550f31a2 100644 Binary files a/doc/docs/业务流程/2026-10-17-结算调整单操作流程说明(分场景).assets/02-新增调整单-选单回带与费用录入.png and b/doc/docs/业务流程/2026-10-17-结算调整单操作流程说明(分场景).assets/02-新增调整单-选单回带与费用录入.png differ diff --git a/doc/docs/业务流程/2026-10-17-结算调整单操作流程说明(分场景).assets/03-新增调整单-金蝶冲销预检提示.png b/doc/docs/业务流程/2026-10-17-结算调整单操作流程说明(分场景).assets/03-新增调整单-金蝶冲销预检提示.png index 4d3bf538..deda7005 100644 Binary files a/doc/docs/业务流程/2026-10-17-结算调整单操作流程说明(分场景).assets/03-新增调整单-金蝶冲销预检提示.png and b/doc/docs/业务流程/2026-10-17-结算调整单操作流程说明(分场景).assets/03-新增调整单-金蝶冲销预检提示.png differ diff --git a/doc/docs/业务流程/2026-10-17-结算调整单操作流程说明(分场景).assets/04-提交审批二次确认.png b/doc/docs/业务流程/2026-10-17-结算调整单操作流程说明(分场景).assets/04-提交审批二次确认.png index 5c1d4282..554af09b 100644 Binary files a/doc/docs/业务流程/2026-10-17-结算调整单操作流程说明(分场景).assets/04-提交审批二次确认.png and b/doc/docs/业务流程/2026-10-17-结算调整单操作流程说明(分场景).assets/04-提交审批二次确认.png differ diff --git a/doc/docs/业务流程/2026-10-17-结算调整单操作流程说明(分场景).assets/05-重新推送-高危二次确认.png b/doc/docs/业务流程/2026-10-17-结算调整单操作流程说明(分场景).assets/05-重新推送-高危二次确认.png index 6f20c8d9..461ac6c2 100644 Binary files a/doc/docs/业务流程/2026-10-17-结算调整单操作流程说明(分场景).assets/05-重新推送-高危二次确认.png and b/doc/docs/业务流程/2026-10-17-结算调整单操作流程说明(分场景).assets/05-重新推送-高危二次确认.png differ diff --git a/doc/docs/业务流程/2026-10-17-结算调整单操作流程说明(分场景).assets/06-审批通过二次确认.png b/doc/docs/业务流程/2026-10-17-结算调整单操作流程说明(分场景).assets/06-审批通过二次确认.png new file mode 100644 index 00000000..6d94b468 Binary files /dev/null and b/doc/docs/业务流程/2026-10-17-结算调整单操作流程说明(分场景).assets/06-审批通过二次确认.png differ diff --git a/doc/docs/业务流程/2026-10-17-结算调整单操作流程说明(分场景).assets/07-审批驳回填写原因.png b/doc/docs/业务流程/2026-10-17-结算调整单操作流程说明(分场景).assets/07-审批驳回填写原因.png new file mode 100644 index 00000000..4fad1844 Binary files /dev/null and b/doc/docs/业务流程/2026-10-17-结算调整单操作流程说明(分场景).assets/07-审批驳回填写原因.png differ diff --git a/doc/docs/业务流程/2026-10-17-结算调整单操作流程说明(分场景).assets/08-金蝶生成失败原因悬浮.png b/doc/docs/业务流程/2026-10-17-结算调整单操作流程说明(分场景).assets/08-金蝶生成失败原因悬浮.png new file mode 100644 index 00000000..22f02a7b Binary files /dev/null and b/doc/docs/业务流程/2026-10-17-结算调整单操作流程说明(分场景).assets/08-金蝶生成失败原因悬浮.png differ diff --git a/doc/docs/业务流程/2026-10-17-结算调整单操作流程说明(分场景).assets/09-调整单点结算单号跳转详情.png b/doc/docs/业务流程/2026-10-17-结算调整单操作流程说明(分场景).assets/09-调整单点结算单号跳转详情.png new file mode 100644 index 00000000..9fce7bde Binary files /dev/null and b/doc/docs/业务流程/2026-10-17-结算调整单操作流程说明(分场景).assets/09-调整单点结算单号跳转详情.png differ diff --git a/doc/docs/业务流程/2026-10-17-结算调整单操作流程说明(分场景).assets/10-编辑弹窗-附件材料与保存同步.png b/doc/docs/业务流程/2026-10-17-结算调整单操作流程说明(分场景).assets/10-编辑弹窗-附件材料与保存同步.png new file mode 100644 index 00000000..81f1d68d Binary files /dev/null and b/doc/docs/业务流程/2026-10-17-结算调整单操作流程说明(分场景).assets/10-编辑弹窗-附件材料与保存同步.png differ diff --git a/doc/docs/业务流程/2026-10-17-结算调整单操作流程说明(分场景).assets/11-正式结算单列表-调整后单标识.png b/doc/docs/业务流程/2026-10-17-结算调整单操作流程说明(分场景).assets/11-正式结算单列表-调整后单标识.png new file mode 100644 index 00000000..6e6bdedb Binary files /dev/null and b/doc/docs/业务流程/2026-10-17-结算调整单操作流程说明(分场景).assets/11-正式结算单列表-调整后单标识.png differ diff --git a/doc/docs/业务流程/2026-10-17-结算调整单操作流程说明(分场景).assets/12-正式结算单列表-原单已调整标识.png b/doc/docs/业务流程/2026-10-17-结算调整单操作流程说明(分场景).assets/12-正式结算单列表-原单已调整标识.png new file mode 100644 index 00000000..b1dcb78f Binary files /dev/null and b/doc/docs/业务流程/2026-10-17-结算调整单操作流程说明(分场景).assets/12-正式结算单列表-原单已调整标识.png differ diff --git a/doc/docs/业务流程/2026-10-17-结算调整单操作流程说明(分场景).md b/doc/docs/业务流程/2026-10-17-结算调整单操作流程说明(分场景).md index 516b1617..0511c37f 100644 --- a/doc/docs/业务流程/2026-10-17-结算调整单操作流程说明(分场景).md +++ b/doc/docs/业务流程/2026-10-17-结算调整单操作流程说明(分场景).md @@ -1,8 +1,14 @@ # 结算调整单操作流程说明(分场景) -> 整理日期:2026-10-17 +> 整理日期:2026-10-17(2026-11-01 按当前代码修订并补全截图) > 定位:面向操作者的流程说明,只讲"谁在哪个页面做什么、系统发生什么、结果是什么",不含代码细节。 -> 依据:`2026-10-16-结算调整单接入金蝶设计(未推送自动推-已推送冲销后重推新单).md` 及当前已落地实现。 +> 依据:`2026-10-16-结算调整单接入金蝶设计(未推送自动推-已推送冲销后重推新单).md`、`2026-10-18-正式结算单预付整合设计(进度预付+项目预付核销·金额口径与记录流转).md` 及当前已落地实现。 + +## 修改记录 + +| 日期 | 修改内容 | +|---|---| +| 2026-11-01 | ① 全部截图按当前界面重截,新增审批确认(06)、驳回原因(07)、同步失败悬浮(08)、结算单号跳详情(09)、编辑弹窗附件与同步按钮(10)、正式结算单"已调整/调整后单"标识(11、12)共 7 张;② 金蝶同步状态文案按页面改为"未同步 / 已同步 / 同步失败",失败原因支持悬浮查看;③ 修正推送按钮口径:"推送金蝶"(未同步/同步失败)无二次确认,仅"重新推送(高危)"有高危确认;④ 补充项目预付核销相关约束:调整后结算金额不得低于核销合计、重推克隆时核销行迁移到新单;⑤ 补充"已调整"原单禁止再发起付款的拦截;⑥ 补充编辑弹窗「同步」按钮、附件材料、负数调整金额等界面说明。 | --- @@ -11,10 +17,11 @@ 正式结算单**审批通过后不允许直接改金额**,一切费用调整都必须通过「结算调整单」完成。 - **作用**:对一张已审批通过的正式结算单,调整其费用项目和金额(调增、调减、新增手工费用)。 -- **单号规则**:`TZ + 日期 + 4 位流水`,保存草稿时自动生成。 -- **只能手工新增**:系统不会自动生成调整单,全部由业务人员在页面发起。 +- **单号规则**:`TZ + 日期 + 4 位流水`,保存草稿时自动生成(取号计入已删除单据,同日不重号)。 +- **只能手工新增**:系统不会自动生成调整单,全部由业务人员在页面发起(菜单:结算管理 → 结算调整单)。 - **没有作废功能**:调整单一旦提交走审批,不支持作废;错了只能走"驳回 → 修改"或重新建单。 -- **推送金蝶按钮是高危操作**:点击前有二次确认,请确认金蝶侧状态后再操作。 +- **支持附件材料**:新建/编辑时可上传附件(pdf、图片、office、zip 等格式,单个 ≤500M,最多 20 个),支持单个下载与批量下载。 +- **"重新推送(高危)"是高危操作**:点击前有二次确认;而"推送金蝶"(未同步/同步失败时)点击后直接执行。 --- @@ -44,60 +51,46 @@ ▼ ▼ 未推送(场景 A) 已推送(场景 B) ① 调整回写原结算单 ① 原结算单业务数据【零改动】 - ② 原单追加变更记录 ② 原单仅追加 1 条变更记录 - ③ 调整单 → 审批通过 ③ 调整单 → 审批通过 - ④ 自动推送金蝶 ④ 提示:请财务在金蝶手工冲销旧单 - (应付/应收单 + 落锁 + 附件) 【财务在金蝶手工冲销旧单】 - ⑤ 回填「已同步 + 金蝶单据号」 点「重新推送(高危)」并二次确认 - ↓ ① 校验调整单已审批通过 - 金蝶应付/应收单(含调整金额) ② 旧单必须已冲销,否则拒绝 - ③ 克隆新正式结算单(应用调整) - ④ 用新单号推金蝶 → 全新应付/应收单 - ⑤ 新单落锁 + 附件 - ⑥ 调整单改挂新单 + 回填金蝶单据号 - ⑦ 原单标「已调整」+ 旧锁解锁 - ⑧ 推送失败置「生成失败」,可重试 - ↓ - 金蝶【全新】应付/应收单(旧单已冲销) + (校验:调整后金额 ≥ 项目预付核销合计) ② 原单仅追加 1 条变更记录 + ② 原单追加变更记录 ③ 调整单 → 审批通过 + ③ 调整单 → 审批通过 ④ 提示:请财务在金蝶手工冲销旧单 + ④ 重算收付汇总(含核销) 【财务在金蝶手工冲销旧单】 + ⑤ 自动推送金蝶 点「重新推送(高危)」并二次确认 + (应付/应收单 + 落锁 + 附件) ① 校验调整单已审批通过、原单已推送 + ⑥ 回填「已同步 + 金蝶单据号」 ② 硬闸门:金蝶旧单必须已冲销 + ↓ ③ 克隆新正式结算单(应用调整) + 金蝶应付/应收单(含调整金额) ④ 项目预付核销行随迁新单,原单核销删除 + ⑤ 用新单号推金蝶 → 全新应付/应收单 + ⑥ 新单落锁 + 附件 + ⑦ 调整单改挂新单 + 回填金蝶单据号 + ⑧ 原单标「已调整」+ 旧锁解锁 + ⑨ 推送失败置「同步失败」,可重试 + ↓ + 金蝶【全新】应付/应收单(旧单已冲销) ``` ### 2.3 各状态下的按钮 | 状态 | 可用操作 | 说明 | |---|---|---| -| 草稿 | 编辑 / 删除 / 提交 | 删除为物理删除,含明细 | +| 草稿 | 编辑 / 删除 / 提交 | 删除为物理删除,含明细,删除前有确认 | | 已驳回 | 编辑 / 提交 | 须先"编辑"保存一次(保存自动回到草稿)才能提交,直接提交会被拒绝 | | 审批中 | 通过 / 驳回 | 审批人操作 | -| 审批通过 | 推送金蝶 / 重新推送(合一按钮)| 按金蝶状态自动分流,高危确认 | +| 审批通过 | 推送金蝶 / 重新推送(合一按钮)| 按金蝶状态自动分流;仅"重新推送"为高危确认 | -![结算调整单列表:草稿行显示「查看 / 编辑 / 删除 / 提交」,审批通过行只剩「查看」与推送类操作;金蝶状态为「已同步」时按钮变为红色的「重新推送(高危)」,未同步时为「推送金蝶」](./2026-10-17-结算调整单操作流程说明(分场景).assets/01-结算调整单列表-各状态操作入口.png) +![结算调整单列表:草稿行显示「查看 / 编辑 / 删除 / 提交」,已驳回行为「查看 / 编辑 / 提交」,审批中行为「查看 / 通过 / 驳回」,审批通过行只剩推送类操作;金蝶状态为「已同步」时按钮变为红色的「重新推送(高危)」,未同步/同步失败时为「推送金蝶」](./2026-10-17-结算调整单操作流程说明(分场景).assets/01-结算调整单列表-各状态操作入口.png) + +> 列表里「单据号」和「结算单号」都是蓝色链接:点单据号打开本单查看弹窗;点结算单号会**跳转到付款/收款结算单列表页并自动打开该结算单详情**(见第五节截图 09)。 ### 2.4 金蝶同步状态(调整单上的展示字段) | 状态 | 含义 | |---|---| -| 未同步 | 原结算单还没推过金蝶,或推送尚未发生 | +| 未同步 | 关联正式结算单还没推过金蝶,或推送尚未发生 | | 已同步 | 已在金蝶生成应付/应收单,调整单上可看到金蝶单据号 | -| 生成失败 | 推送失败,可用"推送金蝶"重试或等定时重推 | +| 同步失败 | 推送失败;**鼠标悬浮状态标签可直接看到失败原因**,可用"推送金蝶"重试或等定时重推 | -### 2.4 正式结算单列表的「金蝶冲销状态」(2026-10-30 新增) - -正式结算单列表在"金蝶单据状态"旁新增一列,显示该单在金蝶侧的旧应付/应收单是否已被财务冲销: - -| 显示 | 含义 | -|---|---| -| 已冲销(绿) | 金蝶侧旧单已冲销(含部分冲销),场景 B 此时可以点"重新推送" | -| 未冲销(黄) | 金蝶侧旧单还在,需先由财务在金蝶完成冲销 | -| 未校验(灰) | 已推送金蝶但系统还没查过(上线初期存量单的初始态,跑一轮定时任务后自动补齐) | -| - | 未推送金蝶的单,没有冲销状态可言 | - -状态由系统在三个途径自动更新,**无需人工维护**: - -1. 定时任务每约 10 分钟批量核对一次金蝶; -2. 新建调整单**选单预检**时即时刷新该单; -3. 点**"重新推送"**时即时刷新该单(被拦截未冲销也会同步为"未冲销")。 - -> 列表状态允许滞后约一个定时周期(10 分钟);点"重新推送"时系统以**当下实时查询**结果为准,不会因列表滞后而误放行。 +![金蝶同步失败原因悬浮:状态列「同步失败」标签上悬浮即可看到后端落库的失败原因(如"调用金蝶A102-2财务应付单保存接口超时"),无需翻日志](./2026-10-17-结算调整单操作流程说明(分场景).assets/08-金蝶生成失败原因悬浮.png) --- @@ -108,53 +101,70 @@ | 项 | 说明 | |---|---| | 入口 | 结算调整单列表 → 新增 | -| 前置 | 必须先选定一张**可调整的正式结算单**,条件缺一不可:① 审批通过;② 未完成收款/付款;③ 非尾款结算;④ 未发起尾款付款/付款申请。不满足的单在选单列表里根本搜不到 | -| 系统动作 | 选定结算单后自动回带:客商、调整类型(应收/应付)、合同、原结算金额、原单费用明细;同时做一次**金蝶冲销预检(软提示)** | -| 预检提示 | ① 未推送金蝶 → 正常继续;② 已推送且旧单已冲销 → 提示"审批后可重新推送新单";③ 已推送且旧单未冲销 → 醒目提示"审批后需先由财务在金蝶完成冲销,才能重新推送";④ 金蝶查不到/超时 → 提示"未校验",**不阻断建单**。预检查询结果同时回写该结算单列表的「金蝶冲销状态」(见 2.4) | -| 结果 | 生成一张草稿,获得单号 | +| 前置 | 必须先选定一张**可调整的正式结算单**,条件缺一不可:① 审批通过;② 未完成收款/付款(收付款状态非"已收/付款",且已收/付金额未达结算金额);③ 未发起尾款结算/尾款付款;④ 无生效中的结算类付款申请关联。不满足的单在选单列表里根本搜不到("预结算合并"来源的正式单是常规来源,不受影响) | +| 系统动作 | 选定结算单后自动回带:客商、结算单类型(应收/应付)、合同、项目、原结算金额,并带出原单费用明细供录入调整金额;同时做一次**金蝶冲销预检(软提示)** | +| 调整费用 | 原单明细行直接在"调整金额(含税)"填数,**可为负数**(负数红色显示);也可点「添加费用」新增手工费用行,费用类型/费用项目可从下拉选择或自由录入 | +| 预检提示 | ① 未推送金蝶 → 提示"审批通过后将自动推送原单";② 已推送且旧单已冲销 → 提示"审批后可直接重新推送新单";③ 已推送且旧单未冲销 → 醒目黄色警告"审批通过后需先由财务在金蝶完成冲销,才能重新推送";④ 金蝶查不到/超时 → 提示"未校验",**不阻断建单** | +| 结果 | 点「保存」生成草稿并获得单号 | > 预检只是提示,不拦人;真正的拦截在"重新推送"那一步。填单时必须至少录入一条调整费用,否则无法提交。 -![新增调整单:选定关联正式结算单后自动回带客商、结算单类型、合同、原结算金额,并带出原单费用明细供录入调整金额](./2026-10-17-结算调整单操作流程说明(分场景).assets/02-新增调整单-选单回带与费用录入.png) +![新增调整单:选定关联正式结算单后自动回带客商、结算单类型、合同、项目、原结算金额,带出原单费用明细并录入调整金额;顶部同时弹出金蝶冲销预检提示](./2026-10-17-结算调整单操作流程说明(分场景).assets/02-新增调整单-选单回带与费用录入.png) -![选单后的金蝶冲销预检:该结算单已推送金蝶且旧单未冲销时,顶部弹出醒目提示"审批通过后需先由财务在金蝶完成冲销,才能重新推送",但建单不受影响](./2026-10-17-结算调整单操作流程说明(分场景).assets/03-新增调整单-金蝶冲销预检提示.png) +![选单后的金蝶冲销预检:该结算单已推送金蝶且旧单未冲销时,顶部弹出黄色警告"该结算单已推送金蝶且旧单尚未冲销,审批通过后需先由财务在金蝶完成冲销,才能重新推送",但建单不受影响](./2026-10-17-结算调整单操作流程说明(分场景).assets/03-新增调整单-金蝶冲销预检提示.png) -### 3.2 保存草稿 / 编辑 / 删除 +### 3.2 保存草稿 / 编辑 / 同步 / 删除 - 草稿和已驳回的单可反复编辑;删除仅限草稿,删除后单号作废、不留痕迹。 +- 编辑弹窗底部有「取消 / 同步 / 保存」三个按钮: + - **同步**:重新拉取关联结算单的**最新信息与费用明细**(原金额按最新数据刷新),已录入的调整金额按费项保留不丢;期间结算单若被其他人调整过,用它可以刷新到最新底数。 + - **保存**:保存草稿(已驳回单保存后自动回到草稿状态)。 - 手工费用行(不对应原单明细的费用)与关联原单明细的费用行在明细里混排,提交时会校验关联完整性。 +![编辑结算调整单:滚动到附件材料区,可上传/下载/批量下载附件并填写附件描述;弹窗底部为「取消 / 同步 / 保存」](./2026-10-17-结算调整单操作流程说明(分场景).assets/10-编辑弹窗-附件材料与保存同步.png) + ### 3.3 提交 - 草稿 / 已驳回 → 审批中,进入审批人的待办。 +- 提交前系统校验:至少一条调整费用;非手工行必须正确关联原单明细费项;费用类型、费用项目必填(≤100 字),备注 ≤200 字。 - 提交后单据锁定,提交人不能再改。 -![提交审批:草稿行点「提交」后需二次确认,确认后单据进入审批中并锁定](./2026-10-17-结算调整单操作流程说明(分场景).assets/04-提交审批二次确认.png) +![提交审批:草稿行点「提交」后需二次确认"确认提交审批?",确认后单据进入审批中并锁定](./2026-10-17-结算调整单操作流程说明(分场景).assets/04-提交审批二次确认.png) ### 3.4 审批通过 ★ 核心分流点 -审批人点"通过"后,系统按**原结算单是否推送过金蝶**自动走两条完全不同的路(见第四、五节场景 A / B): +审批人点"通过",先弹确认"**审批通过后将更新关联正式结算单,确认继续?**"。确认后,系统按**原结算单是否推送过金蝶**自动走两条完全不同的路(见第四、五节场景 A / B): - **原单未推送**:把调整直接回写到原结算单,然后自动推金蝶。 - **原单已推送**:原单数据一律不动,只留一条变更记录,等财务冲销后再人工重推。 -> 注:审批接口会顺带返回自动推送的失败原因(如有),前端直接提示,不用去翻日志。 +审批完成后页面会按结果给出不同提示: + +| 结果 | 提示样式与文案 | +|---|---| +| 未推送场景,自动推送成功 | "审批通过,已同步金蝶:<金蝶单据号>" | +| 未推送场景,自动推送失败 | 黄色警告"审批通过,但自动同步金蝶失败:<原因>"(审批结果不受影响,走场景 D) | +| 已推送场景 | "审批通过;原结算单已推送金蝶,请财务人员在金蝶手工完成旧应付/应收单冲销处理后再重新推送" | + +![审批通过二次确认:明确提示"审批通过后将更新关联正式结算单",确认后系统按原单金蝶状态自动分流](./2026-10-17-结算调整单操作流程说明(分场景).assets/06-审批通过二次确认.png) ### 3.5 驳回 -- 审批中 → 已驳回,驳回原因写入备注。 +- 审批中 → 已驳回,驳回需填写原因(≤200 字),驳回原因写入单据备注。 - 驳回后单据解锁,提交人可修改后重新提交,回到审批中。注意:重新提交前必须先点一次「编辑」并保存(保存会把单据重置回草稿),对已驳回单直接点提交会被拒绝。 -### 3.6 推送金蝶 / 重新推送(合一按钮,高危) +![审批驳回:点「驳回」后必须填写驳回原因(200 字以内),确认后单据回到"已驳回"并解锁](./2026-10-17-结算调整单操作流程说明(分场景).assets/07-审批驳回填写原因.png) + +### 3.6 推送金蝶 / 重新推送(合一按钮) 列表和详情里只有一个按钮,系统按金蝶状态自动分流: -| 调整单金蝶状态 | 按钮实际行为 | 前置闸门 | -|---|---|---| -| 未同步 / 生成失败 | 推送其关联的**原结算单**到金蝶 | 无冲销要求 | -| 已同步 | **重新推送(高危)**:确认金蝶旧单已冲销后,克隆一张新正式结算单并推送 | 硬闸门:金蝶旧单必须已冲销,否则直接拒绝并提示先冲销 | +| 调整单金蝶状态 | 按钮显示 | 按钮实际行为 | 前置闸门 | +|---|---|---|---| +| 未同步 / 同步失败 | 推送金蝶(蓝色) | 推送其关联的**正式结算单**到金蝶,成功后自动回填调整单状态与金蝶单号 | 无冲销要求,点击后直接执行 | +| 已同步 | 重新推送(高危)(红色) | 确认金蝶旧单已冲销后,克隆一张新正式结算单并推送 | 硬闸门:金蝶旧单必须已冲销,否则直接拒绝并提示先冲销 | -按钮点击均有高危二次确认弹窗。 +仅"重新推送(高危)"有高危二次确认弹窗。 ![重新推送(高危):确认金蝶旧应付/应收单已由财务手工冲销后才继续,系统将克隆新单并生成全新应付/应收单](./2026-10-17-结算调整单操作流程说明(分场景).assets/05-重新推送-高危二次确认.png) @@ -170,8 +180,8 @@ |---|---|---|---| | 1 | 业务 | 新增调整单,选定结算单,录调整费用,保存草稿 | 回带信息、生成 TZ 单号 | | 2 | 业务 | 提交 | 草稿 → 审批中 | -| 3 | 审批人 | 审批通过 | ① 调整直接回写原结算单(费用、明细、合计、结算金额全部更新);② 原单追加变更记录;③ 调整单 → 审批通过;④ 事务提交后**自动触发金蝶推送**(走正式结算单既有的推送链路:生成财务应付/应收单 + 单据落锁 + 附件上传) | -| 4 | 系统 | — | 推送成功:调整单回填"已同步 + 金蝶单据号";推送失败:调整单置"生成失败"+ 原因,**审批结果不受影响**,转入场景 D 处理 | +| 3 | 审批人 | 审批通过 | ① 调整直接回写原结算单(费用、明细、合计、结算金额全部更新),**调整后结算金额不得低于该项目预付核销合计**,否则审批被拒绝并提示"请先调减项目预付核销";② 原单追加变更记录;③ 调整单 → 审批通过;④ 按最新收付款数据重算收付汇总;⑤ 事务提交后**自动触发金蝶推送**(走正式结算单既有的推送链路:生成财务应付/应收单 + 单据落锁 + 附件上传) | +| 4 | 系统 | — | 推送成功:调整单回填"已同步 + 金蝶单据号";推送失败:调整单置"同步失败"+ 原因,**审批结果不受影响**,转入场景 D 处理 | 结束后:原结算单即调整后的金额,金蝶里是一张含调整金额的应付/应收单。 @@ -184,12 +194,12 @@ | 步骤 | 操作人 | 操作 | 系统动作 | |---|---|---|---| | 1 | 业务 | 新增调整单(选单时预检会提示"旧单未冲销"或"已冲销"),提交 | 同场景 A 步骤 1-2 | -| 2 | 审批人 | 审批通过 | ① 原结算单业务数据**零改动**,只追加 1 条变更记录("调整单XX审批通过,调整金额XX,待重推金蝶");② 调整单 → 审批通过,金蝶状态保持"已同步"快照("重新推送"按钮因此亮起);③ 前端提示:**"原结算单已推送金蝶,请财务人员在金蝶手工完成旧应付/应收单冲销处理"** | -| 3 | 财务 | **在金蝶系统内手工冲销**旧的应付/应收单 | TMS 不参与,这是金蝶侧的人工操作;冲销完成后**无需通知 TMS**——正式结算单列表的「金蝶冲销状态」约 10 分钟内自动变为"已冲销"(见 2.4),看到变绿即可回 TMS 重推 | -| 4 | 业务 | 回到 TMS,点"重新推送(高危)"并二次确认 | 系统依次:① 硬闸门——按原结算单号去金蝶查旧单,**必须已冲销**(部分冲销也算已冲销),未冲销直接拒绝;② **克隆一张新的正式结算单**(全新 FJS/SJS 单号),把调整应用上去,直接置为审批通过(新单不再走审批流);③ 用新单号推送金蝶,生成**全新**的财务应付/应收单 + 落锁 + 附件;④ 调整单改挂到新单,回填"已同步 + 金蝶单据号";⑤ 原单标记"已调整",原单在金蝶的旧锁解锁 | -| 5 | 系统 | — | 完成。此时列表里**新旧两张正式结算单并存**,带标识区分(见第六节) | +| 2 | 审批人 | 审批通过 | ① 原结算单业务数据**零改动**,只追加 1 条变更记录("调整单XX审批通过,调整金额XX,待重推金蝶");② 调整单 → 审批通过,金蝶状态保持"已同步"快照("重新推送"按钮因此亮起);③ 前端提示:**"原结算单已推送金蝶,请财务人员在金蝶手工完成旧应付/应收单冲销处理后再重新推送"** | +| 3 | 财务 | **在金蝶系统内手工冲销**旧的应付/应收单 | TMS 不参与,这是金蝶侧的人工操作 | +| 4 | 业务 | 回到 TMS,点"重新推送(高危)"并二次确认 | 系统依次:① 硬闸门——按原结算单号去金蝶查旧单,**必须已冲销**(部分冲销也算已冲销),未冲销直接拒绝;② **克隆一张新的正式结算单**(全新 FJS/SJS 单号),把调整应用上去,直接置为审批通过(新单不再走审批流);③ **项目预付核销行迁移**:原单的核销记录随迁到新单并从原单删除,避免预付可核销额度被新旧两单双占(收付口径以新单为准);④ 用新单号推送金蝶,生成**全新**的财务应付/应收单 + 落锁 + 附件;⑤ 调整单改挂到新单,回填"已同步 + 金蝶单据号";⑥ 原单标记"已调整",原单在金蝶的旧锁解锁 | +| 5 | 系统 | — | 完成。此时列表里**新旧两张正式结算单并存**,带标识区分(见第五节截图 11、12) | -失败处理:第 4 步若推送失败,已克隆的新单不回滚,新单置"生成失败";再点"重新推送"会**复用已克隆的新单**从推送步骤继续,不会重复建单(幂等)。调整单已改挂新单后再点重推,会被冲销闸门拦截,无副作用。 +失败处理:第 4 步若推送失败,已克隆的新单不回滚,新单置"同步失败";调整单自身状态仍显示"已同步"(按钮仍是"重新推送(高危)"),失败原因看**新单**。再点"重新推送"会**复用已克隆的新单**从推送步骤继续,不会重复建单(幂等)。调整单已改挂新单后再点重推,会被冲销闸门拦截,无副作用。另外克隆应用调整时同样校验"调整后结算金额不得低于项目预付核销合计",不满足则整次重推失败回滚。 > 为什么第 2 步不自动推:金蝶侧已有一张旧单,直接再推会重复生成应付/应收单;必须先冲销旧单,且冲销只能由财务在金蝶完成,TMS 无法代办。 @@ -206,8 +216,8 @@ | 触发 | 现象 | 处理 | |---|---|---| -| 场景 A 审批后自动推送失败 | 调整单状态"生成失败",审批接口直接返回失败原因,原结算单也有失败标记 | ① 等定时重推任务自动扫描重试;② 或人工在调整单列表点"推送金蝶"立即重试 | -| 场景 B 重推新单失败 | 新正式结算单"生成失败",调整单显示失败状态 | 再点"重新推送",系统复用已克隆的新单从推送步骤续推,不会重复建单 | +| 场景 A 审批后自动推送失败 | 调整单状态"同步失败",审批接口直接返回失败原因(黄色警告),失败原因落库、列表状态标签悬浮可见 | ① 等定时重推任务自动扫描重试;② 或人工在调整单列表点"推送金蝶"立即重试 | +| 场景 B 重推新单失败 | 新正式结算单"同步失败"(原因落库可悬浮查看),调整单仍显示"已同步" | 再点"重新推送",系统复用已克隆的新单从推送步骤续推,不会重复建单 | 审批结果与推送结果相互独立:推送失败不影响"审批通过",只影响金蝶侧是否生成单据。 @@ -216,12 +226,14 @@ | 情况 | 系统行为 | |---|---| | 未审批就点重推 | 拒绝,要求先审批通过 | +| 原单未推送金蝶就点重推 | 拒绝,提示"请先执行「推送金蝶」" | | 金蝶旧单未冲销就点重推 | 硬拦截,提示先冲销 | | 金蝶旧单被删除(非冲销)| 查不到旧单 → 同样拦截 | | 部分冲销 | 视同已冲销,放行重推 | | 审批时原单刚好已被推送到金蝶 | 按**当下**状态走场景 B,选单时的预检结论不作数 | | 选单时金蝶超时/不可用 | 预检降级"未校验",不阻断建单 | -| 列表「金蝶冲销状态」与金蝶实际有出入 | 列表为定时同步值,允许滞后约 10 分钟;重推一律以闸门实时校验为准,不影响拦截准确性 | +| 调整后结算金额低于项目预付核销合计 | 审批(未推送场景)或重推克隆时直接拒绝,提示"请先调减项目预付核销" | +| 对"已调整"原单发起付款/付款申请 | 拦截,提示"结算单[XX]已因结算调整作废,不可再发起付款,请使用调整后的新单" | | 重推成功前原单旧锁残留 | 重推成功时统一解锁 | | 应收结算单 | 与应付完全同一套流程,仅接口不同(应收 A302 系 / 应付 A102 系) | @@ -231,17 +243,25 @@ | 单据/字段 | 含义 | |---|---| -| 正式结算单标"**已调整**" | 该单已被调整单重推过,金额已作废(数据保留),不要再用它做下游业务 | -| 正式结算单标"**调整后单**"并显示原单号 | 这是重推克隆出的新单,金额 = 原单 + 调整 | +| 正式结算单"调整标识"= **已调整** | 该单已被调整单重推过,金额已作废(数据保留),**不可再对其发起付款**,不要再用它做下游业务 | +| 正式结算单"调整标识"= **调整后单** + "来源原单号"列 | 这是重推克隆出的新单,金额 = 原单 + 调整 | +| 调整单"结算单号"链接 | 点击跳转到付款/收款结算单列表并自动打开该结算单详情 | | 调整单"金蝶单据号" | 该调整最终落到金蝶哪张应付/应收单上 | -| 调整单"金蝶同步状态" | 见 2.3 | +| 调整单"金蝶单据状态" | 见 2.4;悬浮可看失败原因 | | 调整金额 / 原结算金额 / 调整后金额 | 调整后金额 = 原结算金额 + 调整金额 | +![调整单列表点「结算单号」链接:自动跳转到付款结算单列表页并打开该单查看详情(结算合计中可看到调整金额与调整后结算金额)](./2026-10-17-结算调整单操作流程说明(分场景).assets/09-调整单点结算单号跳转详情.png) + +![正式结算单列表(新单):重推克隆出的 FJS2026083100004 调整标识显示「调整后单」,来源原单号列显示原单号 FJS2026101700001](./2026-10-17-结算调整单操作流程说明(分场景).assets/11-正式结算单列表-调整后单标识.png) + +![正式结算单列表(原单):被重推过的 FJS2026101700001 调整标识显示「已调整」,金蝶单据号保留,此单仅作存档不可再用于付款](./2026-10-17-结算调整单操作流程说明(分场景).assets/12-正式结算单列表-原单已调整标识.png) + --- ## 六、重要业务口径(须知) 1. **原单冻结原则**:原结算单一经推送金蝶,TMS 本地对其零改动;调整金额只体现在重推克隆的新单上。 -2. **新单是全新独立单**:原单的下游关联(开票申请、收款认领、对账)**不迁移**,留在原单;新单发票状态、收付款状态均从零开始(未收票、未付款)。应收场景下新单会显示"未开票/未收款",属正常现象。 +2. **新单是全新独立单**:原单的下游关联(开票申请、收款认领、对账)**不迁移**,留在原单;新单发票状态、收付款状态均从零开始(未收票、未付款)。应收场景下新单会显示"未开票/未收款",属正常现象。**唯一例外是项目预付核销**:核销记录会随迁到新单并从原单删除,避免预付额度双占。 3. **新单不走审批流**:克隆的新正式结算单直接是审批通过状态,无需再审批。 4. **冲销是财务的人工操作**:TMS 不自动冲销金蝶单据,也不会因为调整单驳回/删除而自动恢复冲销——所以建单阶段只做软提示,把硬拦截留在重推。 +5. **"已调整"原单只能看不能用**:原单标记"已调整"后,直接付款和付款申请选单都会被拦截,付款请一律使用调整后的新单。 diff --git a/doc/docs/业务流程/2026-10-18-预付转正式结算单与项目预付操作手册(纯操作·带状态).assets/01-预付结算单列表.png b/doc/docs/业务流程/2026-10-18-预付转正式结算单与项目预付操作手册(纯操作·带状态).assets/01-预付结算单列表.png new file mode 100644 index 00000000..12fa37e3 Binary files /dev/null and b/doc/docs/业务流程/2026-10-18-预付转正式结算单与项目预付操作手册(纯操作·带状态).assets/01-预付结算单列表.png differ diff --git a/doc/docs/业务流程/2026-10-18-预付转正式结算单与项目预付操作手册(纯操作·带状态).assets/02-付款申请表单-进度预付.png b/doc/docs/业务流程/2026-10-18-预付转正式结算单与项目预付操作手册(纯操作·带状态).assets/02-付款申请表单-进度预付.png new file mode 100644 index 00000000..cef411e6 Binary files /dev/null and b/doc/docs/业务流程/2026-10-18-预付转正式结算单与项目预付操作手册(纯操作·带状态).assets/02-付款申请表单-进度预付.png differ diff --git a/doc/docs/业务流程/2026-10-18-预付转正式结算单与项目预付操作手册(纯操作·带状态).assets/03-付款申请-选择预付结算单弹窗.png b/doc/docs/业务流程/2026-10-18-预付转正式结算单与项目预付操作手册(纯操作·带状态).assets/03-付款申请-选择预付结算单弹窗.png new file mode 100644 index 00000000..d0d1b6cc Binary files /dev/null and b/doc/docs/业务流程/2026-10-18-预付转正式结算单与项目预付操作手册(纯操作·带状态).assets/03-付款申请-选择预付结算单弹窗.png differ diff --git a/doc/docs/业务流程/2026-10-18-预付转正式结算单与项目预付操作手册(纯操作·带状态).assets/04-付款管理列表.png b/doc/docs/业务流程/2026-10-18-预付转正式结算单与项目预付操作手册(纯操作·带状态).assets/04-付款管理列表.png new file mode 100644 index 00000000..9c58777e Binary files /dev/null and b/doc/docs/业务流程/2026-10-18-预付转正式结算单与项目预付操作手册(纯操作·带状态).assets/04-付款管理列表.png differ diff --git a/doc/docs/业务流程/2026-10-18-预付转正式结算单与项目预付操作手册(纯操作·带状态).assets/05-付款申请表单-项目预付.png b/doc/docs/业务流程/2026-10-18-预付转正式结算单与项目预付操作手册(纯操作·带状态).assets/05-付款申请表单-项目预付.png new file mode 100644 index 00000000..b231f3dc Binary files /dev/null and b/doc/docs/业务流程/2026-10-18-预付转正式结算单与项目预付操作手册(纯操作·带状态).assets/05-付款申请表单-项目预付.png differ diff --git a/doc/docs/业务流程/2026-10-18-预付转正式结算单与项目预付操作手册(纯操作·带状态).assets/06-付款结算单列表-应付.png b/doc/docs/业务流程/2026-10-18-预付转正式结算单与项目预付操作手册(纯操作·带状态).assets/06-付款结算单列表-应付.png new file mode 100644 index 00000000..4933e831 Binary files /dev/null and b/doc/docs/业务流程/2026-10-18-预付转正式结算单与项目预付操作手册(纯操作·带状态).assets/06-付款结算单列表-应付.png differ diff --git a/doc/docs/业务流程/2026-10-18-预付转正式结算单与项目预付操作手册(纯操作·带状态).assets/07-付款结算单编辑页.png b/doc/docs/业务流程/2026-10-18-预付转正式结算单与项目预付操作手册(纯操作·带状态).assets/07-付款结算单编辑页.png new file mode 100644 index 00000000..dc8a021e Binary files /dev/null and b/doc/docs/业务流程/2026-10-18-预付转正式结算单与项目预付操作手册(纯操作·带状态).assets/07-付款结算单编辑页.png differ diff --git a/doc/docs/业务流程/2026-10-18-预付转正式结算单与项目预付操作手册(纯操作·带状态).assets/08-项目预付核销分区.png b/doc/docs/业务流程/2026-10-18-预付转正式结算单与项目预付操作手册(纯操作·带状态).assets/08-项目预付核销分区.png new file mode 100644 index 00000000..ee55bc41 Binary files /dev/null and b/doc/docs/业务流程/2026-10-18-预付转正式结算单与项目预付操作手册(纯操作·带状态).assets/08-项目预付核销分区.png differ diff --git a/doc/docs/业务流程/2026-10-18-预付转正式结算单与项目预付操作手册(纯操作·带状态).assets/09-选择项目预付单弹窗.png b/doc/docs/业务流程/2026-10-18-预付转正式结算单与项目预付操作手册(纯操作·带状态).assets/09-选择项目预付单弹窗.png new file mode 100644 index 00000000..054d808f Binary files /dev/null and b/doc/docs/业务流程/2026-10-18-预付转正式结算单与项目预付操作手册(纯操作·带状态).assets/09-选择项目预付单弹窗.png differ diff --git a/doc/docs/业务流程/2026-10-18-预付转正式结算单与项目预付操作手册(纯操作·带状态).assets/10-收款结算单列表-应收.png b/doc/docs/业务流程/2026-10-18-预付转正式结算单与项目预付操作手册(纯操作·带状态).assets/10-收款结算单列表-应收.png new file mode 100644 index 00000000..79d6d17f Binary files /dev/null and b/doc/docs/业务流程/2026-10-18-预付转正式结算单与项目预付操作手册(纯操作·带状态).assets/10-收款结算单列表-应收.png differ diff --git a/doc/docs/业务流程/2026-10-18-预付转正式结算单与项目预付操作手册(纯操作·带状态).assets/11-开票管理列表.png b/doc/docs/业务流程/2026-10-18-预付转正式结算单与项目预付操作手册(纯操作·带状态).assets/11-开票管理列表.png new file mode 100644 index 00000000..c3fadad5 Binary files /dev/null and b/doc/docs/业务流程/2026-10-18-预付转正式结算单与项目预付操作手册(纯操作·带状态).assets/11-开票管理列表.png differ diff --git a/doc/docs/业务流程/2026-10-18-预付转正式结算单与项目预付操作手册(纯操作·带状态).assets/12-开票申请表单.png b/doc/docs/业务流程/2026-10-18-预付转正式结算单与项目预付操作手册(纯操作·带状态).assets/12-开票申请表单.png new file mode 100644 index 00000000..31226718 Binary files /dev/null and b/doc/docs/业务流程/2026-10-18-预付转正式结算单与项目预付操作手册(纯操作·带状态).assets/12-开票申请表单.png differ diff --git a/doc/docs/业务流程/2026-10-18-预付转正式结算单与项目预付操作手册(纯操作·带状态).assets/13-选择应收正式结算单弹窗.png b/doc/docs/业务流程/2026-10-18-预付转正式结算单与项目预付操作手册(纯操作·带状态).assets/13-选择应收正式结算单弹窗.png new file mode 100644 index 00000000..0c9bb2bf Binary files /dev/null and b/doc/docs/业务流程/2026-10-18-预付转正式结算单与项目预付操作手册(纯操作·带状态).assets/13-选择应收正式结算单弹窗.png differ diff --git a/doc/docs/业务流程/2026-10-18-预付转正式结算单与项目预付操作手册(纯操作·带状态).assets/14-付款结算单新增页.png b/doc/docs/业务流程/2026-10-18-预付转正式结算单与项目预付操作手册(纯操作·带状态).assets/14-付款结算单新增页.png new file mode 100644 index 00000000..83336602 Binary files /dev/null and b/doc/docs/业务流程/2026-10-18-预付转正式结算单与项目预付操作手册(纯操作·带状态).assets/14-付款结算单新增页.png differ diff --git a/doc/docs/业务流程/2026-10-18-预付转正式结算单与项目预付操作手册(纯操作·带状态).md b/doc/docs/业务流程/2026-10-18-预付转正式结算单与项目预付操作手册(纯操作·带状态).md index 2dbb4807..8b037d1e 100644 --- a/doc/docs/业务流程/2026-10-18-预付转正式结算单与项目预付操作手册(纯操作·带状态).md +++ b/doc/docs/业务流程/2026-10-18-预付转正式结算单与项目预付操作手册(纯操作·带状态).md @@ -1,9 +1,39 @@ # 预付转正式结算单与项目预付操作手册(纯操作·带状态) -> 整理日期:2026-10-18 +> 整理日期:2026-10-18 | 最近校对:2026-11-01(按最新代码与前端界面全面校对,见下方修改记录) > 定位:**给操作者和实施/培训用的纯操作手册**。只讲"在哪个页面、点哪个按钮、填什么、会看到什么状态",不含接口、表名、代码。 > 配套逻辑说明见同目录《2026-10-18-预付转正式结算单与项目预付操作流程说明(分场景·前端后端).md》。 +## 修改记录 + +| 日期 | 修改内容 | +|---|---| +| 2026-10-18 | 初版。 | +| 2026-11-01 | 按最新代码与前端界面全面校对重排:① 单据/菜单名称对齐系统实际(预付结算单、付款结算单、收款结算单、付款管理、开票管理),见「〇、页面名称对照」;② 付款申请金蝶状态更正为「未生成 / 已生成 / 付款中 / 已付款 / 已关闭 / 生成失败」;③ **项目预付入口更正**:从「付款管理 → 新增付款申请 → 付款类型=项目预付」进入,列表上的「预付申请」按钮固定为**进度预付**;④ 正式结算单发起付款由"弹窗确认"改为**跳转付款申请表单**;⑤ 开票申请放开"必须未开票"限制,改为按**剩余可开票金额**累计控制(支持部分开票后再开),「分摊发票金额」由系统按选择顺序**自动分摊**、不可手工填写;⑥ 新增「客商风险与付款特批」说明;⑦ 金蝶锁定状态展示(锁定失败·补偿中 / 已达上限)与补偿规则细化;⑧ 全篇嵌入实际页面截图(本地环境示例数据);⑨ 作废规则按最新校验更正(付款结算单仅"审批通过且未同步金蝶"可作废;金蝶侧删单对结算单/开票申请的自动联动作废)。 | + +--- + +## 〇、页面名称对照(先看这个) + +系统菜单已按"应付 / 应收"拆分,本手册全部使用系统里的实际名称: + +| 本手册场景 | 系统菜单位置 | 页面名称 | +|---|---|---| +| 预结算单 | 结算管理 | **预付结算单** | +| 应付正式结算单 | 结算管理 | **付款结算单** | +| 应收正式结算单 | 结算管理 | **收款结算单** | +| 付款申请列表 | 收付款管理 | **付款管理** | +| 开票申请列表 | 收付款管理 | **开票管理** | +| 结算调整单 | 结算管理 | 结算调整单 | + +付款申请的**付款类型**有三种,列表和表单里都按这个名称显示: + +| 类型值 | 页面显示 | 用途 | +|---|---|---| +| 进度预付 | 进度预付 | 挂预付结算单提前付款 | +| 项目预付 | 项目预付 | 不挂结算单,按项目+承运商合同预付 | +| 尾款付款 | 尾款付款 | 挂付款结算单付尾款 | + --- ## 一、先认状态(每个场景都对着这几张表看) @@ -24,199 +54,248 @@ | 草稿 | 编辑 / 删除 / 提交 | | 已驳回 | 编辑(保存后回到草稿)/ 提交 | | 审批中 | 通过 / 驳回(审批人操作) | -| 审批通过 | 看后续:同步金蝶、付款申请、开票申请、作废(未锁定时) | +| 审批通过 | 看后续:同步金蝶、付款申请、开票申请、作废(未同步金蝶时) | | 已作废 | 终态,只读 | -### 1.2 付款申请的金蝶状态 +### 1.2 付款申请的金蝶状态(付款管理列表「金蝶单据状态」列) ``` -未同步 ──推送──→ 已同步 ──→ 付款中 ──→ 已付款 - │ │ - └────────── 生成失败(可重推) └──→ 已关闭 +未生成 ──推送──→ 已生成 ──→ 付款中 ──→ 已付款 + │ │ + ├── 生成失败(每10分钟自动重推;也可勾选后点「批量同步」) + └── 已关闭 ←──── 金蝶侧作废/关闭 ``` - **已付款**:钱已付出。**项目预付只有到"已付款"才能在结算单上核销**。 -- **生成失败**:系统每 10 分钟自动重推;也可在列表勾选后点「批量同步」手动重推。 +- **生成失败**:系统每 10 分钟自动重推一次;也可在列表勾选后点「**批量同步**」手动重推。失败原因把鼠标悬停在状态标签上即可看到。 +- 列表还有「**金蝶锁状态**」和「**附件同步**」两列,见 1.4 和场景 5。 -### 1.3 正式结算单上的四个状态 +### 1.3 付款结算单 / 收款结算单上的四个状态 | 状态位 | 取值 | 看什么 | |---|---|---| | 审批状态 | 草稿 / 审批中 / 审批通过 / 已驳回 / 已作废 | 单据走到哪一步 | -| 金蝶同步状态 | 未同步 / 已同步 / 同步失败 | 有没有在金蝶生成应付单(应付)/应收单(应收) | +| 金蝶单据状态 | 未同步 / 已同步 / 同步失败 | 有没有在金蝶生成应付单(付款结算单)/ 应收单(收款结算单) | | 收付款状态 | 未收/付款 / 部分收/付款 / 已收/付款 | 收付进度(**已含预付、核销**) | -| 发票状态 | 应付=未收票/部分收票/已收票;应收=未开票/部分开票/已开票 | 票开/收到哪一步 | +| 发票状态 | 付款结算单=未收票/部分收票/已收票;收款结算单=未开票/部分开票/已开票 | 票收到/开出哪一步 | -### 1.4 金蝶锁定状态 +### 1.4 金蝶锁定状态(付款管理 / 付款·收款结算单 / 开票管理列表都有此列) -| 显示 | 含义 | 影响 | +| 列显示 | 含义 | 影响 | |---|---|---| -| 锁定中 | 正在向金蝶加锁 | 稍等 | -| 已锁定 | 金蝶已锁 | **不可编辑、不可作废** | -| 锁定失败·补偿中 / 已达上限 | 加锁未成功 | 定时自动补偿;必要时联系管理员 | +| 金蝶锁定中(红标) | 金蝶已锁单 | **不可编辑、不可作废** | +| 锁定失败·补偿中 | 加锁没成功 | 系统每 5 分钟自动补偿重锁 | +| 锁定失败·已达上限 | 自动补偿 5 次都没成功 | 不再自动重试;重新点一次「同步金蝶」即可重置计数补锁,必要时联系管理员 | +| 金蝶删单 / 金蝶解锁(留痕) | 金蝶侧把单删了或解锁过 | 付款申请→金蝶状态变"生成失败"待重推;结算单/开票申请→系统自动作废(见第四节) | ### 1.5 开票申请的金蝶状态 ``` -未同步 ──推送──→ 已同步 ──金蝶回写──→ 开票完成 +未同步 ──推送──→ 已同步 ──金蝶逐张回写──→ 开票完成(全部张次都有结果才到这一步) │ - └── 生成失败(可重推) + ├── 同步失败(可重推) + └── 金蝶侧删单 → 系统自动作废 ``` --- ## 二、应付侧操作(钱付出去) -### 场景 1:预结算单直接发起「进度预付」 +### 场景 1:预付结算单直接发起「进度预付」 -**前提**:预结算单是**应付**、状态**审批通过**、还没转正式结算。 +**前提**:预付结算单是**应付**、状态**审批通过**、还没转付款结算。 -1. 进「**预结算单**」列表,勾选预结算单(**同一合同**可多选)。 -2. 点工具栏「**预付申请**」。 -3. 自动跳到「付款申请」页面(类型已固定为进度预付):结算单只读,"结算金额/可付款金额"自动带出。 -4. 填「申请付款金额」(不超过可付款金额)、「付款方式」→「保存」。 -5. 回到「付款申请」列表 → 找到该单 →「**提交**」。 -6. 审批人点「**通过**」。 +![预付结算单列表:工具栏为「新增 / 预付申请 / 尾款结算 / 打印结算单 / 导出」,需先勾选才能点预付申请或尾款结算;行内操作按状态显示 查看/编辑/删除/提交/通过/驳回/作废](./2026-10-18-预付转正式结算单与项目预付操作手册(纯操作·带状态).assets/01-预付结算单列表.png) -**状态变化**:付款申请 `草稿 → 审批中 → 审批通过`;随后系统自动推金蝶,金蝶状态 `未同步 → 已同步 → 已付款`。 -**同时发生**:预结算单上出现「申请预付金额 / 已预付金额」;预结算单仍然可以继续「尾款结算」。 +1. 进「**预付结算单**」列表(结算管理),勾选单据(**同一合同**可多选;不满足"应付+审批通过+未转付款结算+同合同"会按钮拦截提示)。 +2. 点工具栏「**预付申请**」→ 自动跳到「付款申请」新增页:**付款类型固定为"进度预付"不可改**,所选预付结算单已带入、结算单号只读,"结算金额 / 可付款金额"自动带出。 -> 若提交/通过后看到黄色警告写着金蝶推送失败:审批结果**不受影响**,稍后自动重推,或到付款申请列表点「批量同步」。 +![付款申请表单(进度预付):付款类型=进度预付且锁定,结算单号只读带出,结算金额/可付款金额自动显示,需填申请付款金额、付款方式、收款方](./2026-10-18-预付转正式结算单与项目预付操作手册(纯操作·带状态).assets/02-付款申请表单-进度预付.png) ---- +3. 如果是空表单进来(见场景 2 的入口说明),可点「结算单号」右侧「**选择**」,在弹窗「选择预付结算单」里按结算单号 / 项目 / 合同名称搜索后勾选。 + +![选择预付结算单弹窗:按结算单号/项目/合同名称搜索,列示结算金额与可付款金额,可付款金额为 0 的单选了也付不了](./2026-10-18-预付转正式结算单与项目预付操作手册(纯操作·带状态).assets/03-付款申请-选择预付结算单弹窗.png) + +4. 填「申请付款金额」(不超过可付款金额)、「付款方式」(选承兑汇票时要再选汇票台账)、「收款方」→「**保存**」。 +5. 回到「**付款管理**」列表 → 找到该单 →「**提交**」→ 审批人「**通过**」。 + +**状态变化**:付款申请 `草稿 → 审批中 → 审批通过`;随后系统自动推金蝶,金蝶状态 `未生成 → 已生成 → 付款中 → 已付款`。 +**同时发生**:预付结算单列表会出现「**申请预付金额 / 已付款金额**」两列并随付款进度更新;预付结算单仍然可以继续「尾款结算」。 + +> 若提交/通过后看到黄色警告写着金蝶推送失败:审批结果**不受影响**,稍后自动重推,或到付款管理列表勾选后点「批量同步」。 ### 场景 2:发起「项目预付」(不挂结算单) **前提**:项目已建、**承运商合同**已建;金额不超过项目额度与客户额度。 -1. 进「**付款申请**」列表 → 点工具栏「**预付申请**」(注意:这个入口是从付款申请页进的,和场景 1 从预结算单进不同)。 -2. 页面类型已固定为项目预付,填写: - - **所属项目**(必选) - - **合同名称**(必选,**只能选承运商合同**,选了项目后才能选) - - 申请付款金额、付款方式、付款方/收款方等 - - 本类型不挂结算单,不显示发票信息与付款记录 -3. 「保存」→ 列表「提交」→ 审批人「通过」。 +**入口(注意)**:进「**付款管理**」列表 → 点「**新增付款申请**」→ 把「**付款类型**」选成「**项目预付**」。 +付款管理工具栏还有一个「预付申请」按钮——那个是**进度预付**(配合预付结算单用的),项目预付**必须**走新增+手选类型。 -**会被拦截的情况**: -- "申请付款金额不能超过项目额度"(项目额度 = 项目额度 + 有效期内的临时额度) -- "申请付款金额不能超过客户额度" +![付款申请表单(项目预付):付款类型=项目预付,需先选所属项目再选合同名称,不显示结算单/结算信息区](./2026-10-18-预付转正式结算单与项目预付操作手册(纯操作·带状态).assets/05-付款申请表单-项目预付.png) + +1. 填写: + - **所属项目**(必选) + - **合同名称**(必选,**选了项目后才能选**,只能选承运商合同) + - 申请付款金额、付款方式、付款方/收款方等 + - 本类型不挂结算单,不显示结算信息与发票相关内容 +2. 「保存」→「付款管理」列表「提交」→ 审批人「通过」。 + +**会被拦截的情况(提示原文)**: +- "申请付款金额不能超过项目额度"(项目额度 = 项目资金需求额度 + **审批通过且在有效期内**的临时额度;按单笔申请金额比较) +- "申请付款金额不能超过客户额度"(按收款方匹配客商档案的信用额度) +- "项目预付必须选择有效合同"、"付款申请只能选择承运商合同" +- "申请付款金额不能超过可付款金额"(项目预付的可付款金额=项目资金需求×10000) **状态变化**:同场景 1。**务必等到金蝶状态「已付款」**,否则下一步核销做不了。 ---- +### 客商风险与付款特批(所有付款申请提交时通用) + +- 提交时如果**收款方是高风险/黑名单客商**:会被拦截——"收款方[x]为高风险/黑名单客商,必须上传「特批附件」类型附件后再提交审批"。先在附件区上传类型为「特批附件」的文件再提交。 +- 如果触发**合规额度超额**(HCS 口径):**不会拦截提交**,单据转入「付款特批」流程,特批完成后才会推金蝶。 +- 审批人点「通过」后,弹窗提示可能带一句话,常见三种: + - "审批通过,已发起付款特批,特批通过后将自动推送履约数据并同步金蝶" → 等特批完成,系统自动推; + - "审批通过,已同步金蝶:XXX" → 已经推成功; + - "审批通过,但自动同步金蝶失败:…" → 审批不受影响,稍后自动重推或手动「批量同步」。 ### 场景 3:结算单上「项目预付核销」 **前提**:有一笔**已付款**的项目预付(金蝶状态=已付款),且与本次结算单**同一合同**,还有剩余可核销金额。 -1. 打开结算单(**预结算单**或**正式结算单**都行)的编辑页。 -2. 找到「**项目预付核销**」分区 → 点「**选择**」。 -3. 弹窗「选择项目预付单」里勾选要抵扣的预付单(已选的会灰掉,不能重复选)→「确定」。 -4. 在「本次核销金额」列:默认已按"可核销金额"填满,可以改小。 +![付款结算单编辑页的「项目预付核销」分区:右上角「选择」按钮带出核销行,列为 预付单号/项目预付金额/可核销金额/本次核销金额/金蝶预付单号/创建人/创建时间;下方发票信息区的「选择收票单」是收票功能,不要点错](./2026-10-18-预付转正式结算单与项目预付操作手册(纯操作·带状态).assets/08-项目预付核销分区.png) + +1. 打开结算单(**预付结算单**或**付款结算单**都行)的编辑页,滚动到「**项目预付核销**」分区。 +2. 点分区右上角「**选择**」(没选合同会提示"请先选择合同")。 +3. 弹窗「**选择项目预付单**」里勾选要抵扣的预付单(已选的行会灰掉,不能重复选)→「确定」。 + +![选择项目预付单弹窗:只列示 同合同+审批通过+金蝶已付款+仍有可核销余额 的项目预付单,列含 预付单号/项目预付金额/可核销金额/金蝶预付单号](./2026-10-18-预付转正式结算单与项目预付操作手册(纯操作·带状态).assets/09-选择项目预付单弹窗.png) + +4. 在「**本次核销金额**」列:默认已按"可核销金额"填满,可以改小。 5. 「保存」。 -**会被拦截的情况(提示文案)**: +**会被拦截的情况(提示原文)**: - "预付单 XXX 本次核销金额必须大于 0" -- "本次核销金额不能超过可核销金额" +- "预付单 XXX 本次核销金额不能超过可核销金额" - "项目预付核销合计不能超过结算单金额" - "仅可选择审核通过的项目预付单" - "预付单 XXX 尚未付款,项目预付须金蝶付款完成后方可核销" - "同一项目预付单不可重复核销" - "项目预付单与结算单合同不一致" -**状态变化**:结算单「**已付金额(含预付、核销)**」立刻增大,「剩余未付金额」减小;列表上"申请付款金额(含预付)/已收已付合计(含预付)/剩余可付/收付款状态"同步变化。 +**状态变化**:结算单「**已付金额(含预付、核销)**」立刻增大、「剩余未付金额」减小;核销金额会同时计入"已申请付款金额"和"已付金额"两个口径;列表上"申请付款金额(含预付)/已收已付合计(含预付)/剩余可付/收付款状态"同步变化。 ---- +### 场景 4:预付结算单转「付款结算单」(尾款结算,容纳预付) -### 场景 4:预结算单转「正式结算单」(容纳预付) +**前提**:预付结算单**审批通过**,且所选单据**同一项目**、**应付**。 -**前提**:预结算单**审批通过**,且**同一项目**。 +![新增付款结算单页(尾款结算跳入):结算合计区底部一行同时显示 结算合计(原金额)/已付金额(含预付、核销)/剩余未付金额 三个口径,结算合计始终是费用全额](./2026-10-18-预付转正式结算单与项目预付操作手册(纯操作·带状态).assets/14-付款结算单新增页.png) -1. 进「**预结算单**」列表,勾选(可多选,须同项目)→ 点工具栏「**尾款结算**」。 -2. 系统跳到「正式结算单」新增页,把所选预结算单的明细、费用项合并进来。 -3. 检查三处金额(**应付结算单才有**): +1. 进「**预付结算单**」列表,勾选(可多选,须同项目)→ 点工具栏「**尾款结算**」。 +2. 系统跳到「**新增付款结算单**」页,把所选预付结算单的明细、费用项合并进来。 +3. 检查结算合计区底部的三个金额(**付款结算单才有**): - **结算合计(原金额)**:费用合计,**始终全额**,不会因为预付/核销而变小; - - **已付金额(含预付、核销)**:把来源预结算的已付预付 + 本次核销都算进去; + - **已付金额(含预付、核销)**:把来源预付结算的已付预付 + 本次核销都算进去; - **剩余未付金额** = 结算合计 − 已付。 -4. 「项目预付核销」区会**自动带出来源预结算上已核销的行**,可增/删/改金额。 +4. 「项目预付核销」区会**自动带出来源预付结算上已核销的行**,可增/删/改金额。 + +![编辑付款结算单:结算合计区底部一行显示 结算合计(原金额)/已付金额(含预付、核销)/剩余未付金额,下方依次为 结算明细/附件/项目预付核销/发票信息 分区](./2026-10-18-预付转正式结算单与项目预付操作手册(纯操作·带状态).assets/07-付款结算单编辑页.png) + 5. 「保存」(草稿)→「提交」→ 审批人「通过」。 -**状态变化**:正式结算单 `草稿 → 审批中 → 审批通过`;预结算单会被标记为已转正式结算(来源被锁定)。 -**注意**:如果预结算单上还有**审批中或已审批但没结束的预付申请**,走"轻量转正式"会被拦住,需要先把预付处理完。 +**会被拦截的情况**: +- "仅审核通过的预付结算单可转付款结算" +- "尾款结算只能选择同一项目下的预付结算单" +- "该预结算单存在在途预付申请,无法尾款结算"(**审批中或已审批但还没付完款的预付申请都算在途**,先把预付处理完/付完再来转) ---- +**状态变化**:付款结算单 `草稿 → 审批中 → 审批通过`;预付结算单会被标记为已转付款结算(来源被锁定:不能再发起预付、不能作废)。 -### 场景 5:正式结算单「同步金蝶」 +### 场景 5:付款结算单「同步金蝶」 **前提**:结算单**审批通过**。 -1. 进「**正式结算单**」列表,切到「**应付**」页签。 -2. 勾选 **1 条**(按钮只允许选一条)→ 点工具栏「**同步金蝶**」。 -3. 成功后会提示"同步成功,金蝶单据号:XXX";列表「金蝶同步状态」变为**已同步**,「金蝶锁定状态」变为**已锁定/锁定中**。 +![付款结算单列表(应付):工具栏 新增/付款申请/同步金蝶/打印结算单/导出;审批通过行有 发票登记、作废 等行内操作](./2026-10-18-预付转正式结算单与项目预付操作手册(纯操作·带状态).assets/06-付款结算单列表-应付.png) -**生成失败怎么办**:列表金蝶状态显示"同步失败",重新点「同步金蝶」即可。 -**提示"该单附件未全部上传,可重推补传"**:单据已同步成功,附件稍后重推会自动补传。 +1. 进「**付款结算单**」列表。 +2. 勾选**一条或多条** → 点工具栏「**同步金蝶**」。 +3. 成功后会提示"同步成功";列表「金蝶单据状态」变为**已同步**,并出现金蝶单据号;「金蝶锁状态」变为**金蝶锁定中**(锁定失败时见 1.4)。 + +**生成失败怎么办**:列表金蝶状态显示"同步失败"(悬浮可见原因),重新点「同步金蝶」即可。 +**提示"该单附件未全部上传,可重推补传"**:单据已同步成功,只是附件没传全;把附件补齐后**再点一次「同步金蝶」**即可补传。 > 审批通过时系统也会自动推一次;若失败,只弹黄色警告,审批照样通过,按上面手动补推。 ---- +### 场景 6:付款结算单「付款申请」(付尾款) -### 场景 6:正式结算单「付款申请」(付尾款) +**前提**:付款结算单是**应付**、**审批通过**、**同一合同**(可多选)、还有剩余可申请金额。 -**前提**:正式结算单是**应付**、**审批通过**、**同一合同**、还有剩余可付金额。 +![付款管理列表:工具栏 新增付款申请/批量同步/申请付尾款/预付申请/导出;付款类型列显示 进度预付/项目预付/尾款付款;金蝶单据状态、金蝶锁状态列在此页](./2026-10-18-预付转正式结算单与项目预付操作手册(纯操作·带状态).assets/04-付款管理列表.png) -1. 「正式结算单」列表(应付)→ 勾选 1 条或多条(同合同)→ 工具栏「**付款申请**」。 -2. 弹窗里确认各单的「申请付款金额」(默认已填=剩余可申请金额,**已扣掉核销**)→「提交」。 -3. 到「付款申请」列表「提交」→ 审批人「通过」。 +1. 「**付款结算单**」列表 → 勾选 1 条或多条(同合同)→ 工具栏「**付款申请**」→ **跳转到付款申请新增页**(不是弹窗):付款类型固定为"尾款付款",所选结算单带入「结算信息」表,各单「付款金额」默认按剩余可申请金额分配,可调整。 +2. 也可以从「**付款管理**」列表点「**申请付尾款**」进入同一表单,再点「结算单号」右侧「选择」自己挑结算单(同样必须同一合同)。 +3. 「保存」→「付款管理」列表「提交」→ 审批人「通过」。 -**会被拦截的情况**: -- "仅审批通过的应付正式结算单允许发起付款申请" -- "付款申请必须选择同一个合同的正式结算单" -- 提示需要先在正式结算单页面同步金蝶(说明该单在金蝶还没有应付单,尾款是从应付单下推的,**必须先做场景 5**) +**会被拦截的情况(提示原文)**: +- "仅审批通过的付款结算单允许发起付款申请" +- "付款申请必须选择同一个合同的付款结算单" +- "所选付款结算单均须存在剩余可申请金额" +- "已调整的结算单不可再发起付款,请使用调整后的新单" +- "该结算单正在申请结算尾款,不允许再次提交申请付款" +- 推金蝶时提示"正式结算单[x]尚未同步金蝶财务应付单(A102-2),请先在正式付款单页面同步金蝶"——说明该单还没做**场景 5**,尾款是从金蝶应付单下推的,**必须先同步金蝶**。 -**状态变化**:正式结算单「收付款状态」`未付款 → 部分付款 → 已付款`,金额按付款结果自动重算。 -**金额错了怎么办**:正式结算单**审批通过后不能直接改金额**,走「**结算调整单**」(见《2026-10-17-结算调整单操作流程说明(分场景).md》)。 +**状态变化**:付款结算单「收付款状态」`未收/付款 → 部分收/付款 → 已收/付款`,金额按付款结果自动重算。 +**金额错了怎么办**:付款结算单**审批通过后不能直接改金额**,走「**结算调整单**」(见《2026-10-17-结算调整单操作流程说明(分场景).md》)。 --- ## 三、应收侧操作(票开出去) -### 场景 7:应收正式结算单「同步金蝶」 +### 场景 7:收款结算单「同步金蝶」 -1. 「正式结算单」列表切到「**应收**」页签 → 勾选 1 条 → 工具栏「**同步金蝶**」。 -2. 列表「金蝶同步状态」变**已同步**,可见金蝶单据号。 +![收款结算单列表(应收):与付款结算单同构,工具栏含 同步金蝶;金蝶单据状态需为「已同步」才能开票](./2026-10-18-预付转正式结算单与项目预付操作手册(纯操作·带状态).assets/10-收款结算单列表-应收.png) -**必须做这一步**:正式结算单开票要求先有金蝶应收单,没做这步,开票申请推不出去。 +1. 进「**收款结算单**」列表 → 勾选 → 工具栏「**同步金蝶**」。 +2. 列表「金蝶单据状态」变**已同步**,可见金蝶单据号。 ---- +**必须做这一步**:开票前系统会先检查金蝶里有没有这张单的应收单,没做这步,开票申请推不出去(提示"正式结算单[x]财务应收单未在金蝶生成,请先在正式付款单页面同步金蝶")。 ### 场景 8:创建并推送「开票申请」 -**前提**:有一张**应收**正式结算单,**审批通过**、未作废、发票状态还是「**未开票**」,且已完成场景 7。 +**前提**:有一张**收款结算单**(应收),**审批通过**、未作废、**剩余可开票金额 > 0**,且已完成场景 7。 +> 2026-10-19 起已放开"必须未开票"的限制:**部分开票过的结算单只要还有剩余可开票金额,可以再次选择继续开**,累计额度自动控制。 -1. 进「**开票申请**」列表 → 点「**新增**」。 -2. 「开票基本信息」里点「结算单」右侧的「**选择**」→ 弹窗「选择应收正式结算单」→ 搜索并勾选 →「确定」。 - - 「项目」「开票单位」自动带出;「**可开票金额**」= 所选结算单剩余可开票额之和。 +![开票管理列表:工具栏 批量同步/新增/导出;列含 金蝶单据号/金蝶单据状态/金蝶锁状态/附件同步](./2026-10-18-预付转正式结算单与项目预付操作手册(纯操作·带状态).assets/11-开票管理列表.png) + +1. 进「**开票管理**」列表 → 点「**新增**」。 +2. 「开票基本信息」里点「结算单」右侧的「**选择**」→ 弹窗「**选择应收正式结算单**」→ 按结算单号/项目/合同搜索并勾选 →「确定」(**合并开票必须同一合同**)。 + +![选择应收正式结算单弹窗:列示 结算金额/已申请开票金额/可开票金额,已申请开票金额是所有未作废开票申请的累计占用](./2026-10-18-预付转正式结算单与项目预付操作手册(纯操作·带状态).assets/13-选择应收正式结算单弹窗.png) + + - 「项目」「开票单位」自动带出;「**可开票金额**」= 所选结算单结算金额 − 已被其他未作废开票申请占用的金额。 - 受票方开票信息按客商档案自动带出。 3. 填「发票类型」等;选**电子专票**时,纳税人识别号、开户行、开户账号、注册地址**必填**。 -4. 填「开票内容」(发票张次 + 商品行),系统自动算「**本次开票金额**」。 -5. 在「结算信息」表里给每张结算单填「**分摊开票金额**」(不超过该单剩余可开票金额)。 -6. 「开票明细」里勾选明细 →「保存」→「提交」→ 审批人「通过」。 +4. 填「开票内容」(发票张次 + 商品行),系统自动算「**本次开票金额**」;注意"运费不能与其他费用合并开在同一张发票中"。 -**会被拦截的情况**: -- "只能选择审批通过、未作废、未收票的应收正式结算单" -- "结算单 XXX 的分摊金额超过剩余可开票金额" +![开票申请新增页:开票基本信息 + 开票内容(按发票张次分 sheet)+ 结算信息](./2026-10-18-预付转正式结算单与项目预付操作手册(纯操作·带状态).assets/12-开票申请表单.png) + +5. 「结算信息」表里的「**分摊发票金额**」由**系统按选择顺序自动分摊**(先选的先摊、摊满为止),该列**只读**;点表单底部「**同步**」可刷新分摊结果。 +6. 「保存」→「提交」→ 审批人「通过」→ 系统按发票张次逐张推金蝶。 + +**会被拦截的情况(提示原文)**: +- "只能选择审批通过、未作废的应收正式结算单" +- "合并开票的结算单必须属于同一合同" +- "本次开票金额不能超过可开票金额" - "本次开票金额必须大于 0" -- "请选择受票方有效的开票信息" -- "未找到正式结算单受票方对应的客商档案" +- "电子专票的纳税人识别号/开户行/开户账号/注册地址…"(必填校验) +- "运费不能与其他费用合并开在同一张发票中" +- 推金蝶时提示"正式结算单[x]财务应收单未在金蝶生成,请先在正式付款单页面同步金蝶"(先做场景 7) **状态变化** + | 对象 | 状态变化 | |---|---| -| 开票申请 | 审批 `草稿 → 审批中 → 审批通过`;金蝶 `未同步 → 已同步 → 开票完成` | -| 来源正式结算单 | 发票状态 `未开票 → 部分开票 → 已开票`(按分摊与回写自动算) | +| 开票申请 | 审批 `草稿 → 审批中 → 审批通过`;金蝶 `未同步 → 已同步 → 开票完成`(**全部发票张次都有回写结果**才到开票完成) | +| 来源收款结算单 | 发票状态 `未开票 → 部分开票 → 已开票`(按累计开票金额与结算金额自动算,全额开满即"已开票") | -**看发票结果**:开票申请详情页的发票信息里能看到发票号、开票日期、价税合计、发票文件。金蝶回写是定时的,没出来可以先等,或在开票申请列表点「**批量同步**」推一把。 +**看发票结果**:开票申请详情页的发票行里有「开票状态」「金蝶开票结果备注」,发票号、开票日期、价税合计、发票文件在回写后展示。金蝶回写是定时的,没出来可以先等,或在开票管理列表勾选审批通过的单据点「**批量同步**」推一把(系统会逐张调用同步)。 **限制**:开票申请一旦推到金蝶(已同步/开票完成)就**锁定不能改**;数电票已经开出来的只能走红冲,不能在本系统作废。**预付和核销不影响开票金额**,开票始终按结算单的**全额**来匹配。 @@ -226,12 +305,15 @@ | 我想…… | 前提 | 操作位置 | 状态变化 | |---|---|---|---| -| 作废付款申请 | 状态=审批通过,且**未被金蝶锁定** | 付款申请列表「作废」 | 单据→已作废;**未付的预付记录联动作废,相关核销行被删除**,受影响结算单的已付金额回落(已付出去的钱不会回退) | -| 作废正式结算单 | 未审批通过,或审批通过但**未锁定** | 正式结算单列表「作废」 | 单据→已作废;本单的核销行被删除,可核销额度释放 | -| 作废预结算单 | 无关联预付申请(有则先作废预付) | 预结算单列表「作废」 | 单据→已作废;预结算上的核销行被删除 | -| 作废开票申请 | 状态=审批通过,且**未锁定**(即还没推金蝶/还没开票) | 开票申请列表「作废」 | 单据→已作废 | -| 删除 | 状态=草稿 | 各列表「删除」 | 草稿被删除 | -| 金蝶侧反审核删单 | — | 金蝶操作后由系统处理 | 付款申请金蝶状态→生成失败,金蝶单号被清空;**未付的预付记录联动作废**;人工核实后重推,重推成功会重新建档;**已核销的行不会自动删**,需要人工作废该预付款才回落 | +| 作废付款申请 | 状态=审批通过,且**未被金蝶锁定** | 付款管理列表行内「作废」(需填作废原因) | 单据→已作废;**未付的预付记录联动作废,相关项目预付核销行被删除**,受影响结算单的已付金额回落(已付出去的钱不会回退) | +| 作废付款/收款结算单 | **审批通过且未同步金蝶**(已同步金蝶的不能直接作废) | 结算单列表行内「作废」(需填作废原因) | 单据→已作废;本单的项目预付核销行被删除,可核销额度释放;未审批通过的单据不走作废,走驳回后删除 | +| 作废预付结算单 | 审批通过、未转付款结算,且**无关联预付申请**(有则提示"请先作废预付申请") | 预付结算单列表行内「作废」(需填作废原因) | 单据→已作废;预付结算上的核销行被删除 | +| 作废开票申请 | 状态=审批通过,且**未被金蝶锁定**(即还没推金蝶/还没开票) | 开票管理列表行内「作废」 | 单据→已作废;来源结算单的开票状态自动回落 | +| 删除 | 状态=草稿 | 各列表行内「删除」 | 草稿被删除 | +| 金蝶侧删付款申请 | — | 金蝶操作后系统自动处理 | 付款申请金蝶状态→生成失败,金蝶单号被清空;**未付的预付记录联动作废**;人工核实后重推,重推成功会重新建档;**已核销的行不会自动删**,需要人工作废该预付款才回落 | +| 金蝶侧删结算单 | — | 金蝶操作后系统自动处理 | 结算单被**系统自动作废**,同步状态还原,核销行删除 | +| 金蝶侧删开票申请 | — | 金蝶操作后系统自动处理 | 开票申请被**系统自动作废**,来源结算单开票状态回落 | +| 结算调整 | 结算单已审批通过(含已同步金蝶) | 结算调整单 | 金额调整走调整单:原单标记"已调整"并禁止再付款,调整后的新单自动迁移核销行与金蝶推送(见《2026-10-17-结算调整单操作流程说明(分场景).md》) | > 一句话记住:**预付一旦真付出去(已付款),作废不会把钱退回**,只会把台账和核销额度清掉。 @@ -241,14 +323,15 @@ | 问题 | 答案 | |---|---| -| 为什么选不到某张预结算单发起预付? | 需要**应付**、**审批通过**、**未转正式结算**、且所选单据**同一合同**。 | -| 为什么核销弹窗里找不到那笔项目预付? | 它必须已**金蝶付款完成(已付款)**,且与结算单**同一合同**、还有剩余可核销额。 | -| 为什么新增开票申请时选不到某张应收结算单? | 需要**审批通过、未作废、发票状态=未开票**;已被别的开票申请占用的会扣掉可开票额。 | -| 开票提示要先去同步金蝶? | 正式结算开票前,必须先在该应收结算单上点「同步金蝶」生成金蝶应收单。 | -| 付款申请审批通过了,但金蝶那边没有单? | 看金蝶状态:若"生成失败",点「批量同步」重推或等自动重推。 | -| 结算单已付金额比我算的多/少? | 已付金额**含**进度预付、项目预付核销;核销金额同时计入"已付"和"剩余可付"的计算,属正常口径。 | +| 为什么选不到某张预付结算单发起预付? | 需要**应付**、**审批通过**、**未转付款结算**、且所选单据**同一合同**。 | +| 付款管理工具栏的「预付申请」发起的是哪种预付? | 固定是**进度预付**(配合预付结算单)。**项目预付**要点「新增付款申请」后手动把付款类型选成"项目预付"。 | +| 为什么核销弹窗里找不到那笔项目预付? | 它必须与结算单**同一合同**、**审批通过**、**金蝶已付款**、还有**剩余可核销额**。 | +| 为什么新增开票申请时选不到某张收款结算单? | 需要**审批通过、未作废、剩余可开票金额>0**;跨合同不能合并开票;已申请开票金额(未作废的占用)会扣减可开票额。部分开票过的单只要还有余额可以再选。 | +| 开票提示要先去同步金蝶? | 开票前系统检查金蝶应收单,先在「收款结算单」列表点「同步金蝶」。 | +| 付款申请审批通过了,但金蝶那边没有单? | 看「金蝶单据状态」:若"生成失败",勾选后点「批量同步」重推或等每 10 分钟自动重推;若弹窗提示"已发起付款特批",要等特批完成才推。 | +| 结算单已付金额比我算的多/少? | 已付金额**含**进度预付、项目预付核销;核销金额同时计入"已申请付款金额"和"已付金额",属正常口径。 | | 结算单金额写错了怎么改? | 审批通过后不能直接改,走「结算调整单」。 | -| 单据上的「作废」是灰的? | 可能已被金蝶锁定,或状态不满足(只有审批通过且未锁定才能作废)。 | +| 单据上的「作废」是灰的/没有? | 常见三种原因:状态不是审批通过;结算单已同步金蝶(需先在金蝶删单联动);已被金蝶锁定(金蝶锁状态列显示"金蝶锁定中")。 | --- @@ -256,24 +339,24 @@ ``` 【应付·进度预付】 -预结算单(审批通过) →「预付申请」→ 付款申请(提交→通过) → 金蝶付款(已付款) - └ 预结算单出现"已预付金额" +预付结算单(审批通过·应付) →「预付申请」→ 付款申请(提交→通过) → 金蝶付款(已付款) + └ 预付结算单出现"申请预付金额/已付款金额" 【应付·项目预付】 -付款申请页「预付申请」→ 选项目+承运商合同 → 提交→通过 → 金蝶付款(已付款) +付款管理「新增付款申请」→ 付款类型=项目预付 → 选项目+承运商合同 → 提交→通过 → 金蝶付款(已付款) -【预结算 → 正式结算(容纳预付)】 -预结算单(审批通过·同项目) →「尾款结算」→ 正式结算单 - 结算合计(全额) / 已付(含预付、核销) / 剩余未付 +【预付结算 → 付款结算单(容纳预付)】 +预付结算单(审批通过·同项目·应付) →「尾款结算」→ 新增付款结算单 + 结算合计(原金额·全额) / 已付金额(含预付、核销) / 剩余未付金额 项目预付核销自动带入 → 保存 → 提交 → 通过 - └ 「同步金蝶」(应付单已同步) + └ 「同步金蝶」(金蝶应付单已同步) 【应付·尾款】 -正式结算单(应付·审批通过) →「付款申请」→ 付款申请 → 金蝶应付下推 → 已付款 - └ 结算单收付款状态:未付款 → 部分付款 → 已付款 +付款结算单(审批通过) →「付款申请」→ 付款申请表单(尾款付款) → 提交→通过 → 金蝶应付下推 → 已付款 + └ 结算单收付款状态:未收/付款 → 部分收/付款 → 已收/付款 【应收·开票】 -应收正式结算单(审批通过·未开票) →「同步金蝶」(应收单已同步) - →「开票申请」新增 → 选结算单 → 填开票内容与分摊 → 提交 → 通过 - → 金蝶开票(已同步 → 开票完成) → 结算单发票状态:未开票 → 部分开票 → 已开票 +收款结算单(审批通过·剩余可开票金额>0) →「同步金蝶」(金蝶应收单已同步) + → 开票管理「新增」→ 选择应收正式结算单(同合同) → 填开票内容(分摊自动算) → 提交 → 通过 + → 金蝶开票(未同步 → 已同步 → 开票完成) → 结算单发票状态:未开票 → 部分开票 → 已开票 ``` diff --git a/doc/docs/业务流程/2026-10-31-先票后款改造方案V2(收票登记A303-2+付款结算单A102-2+A302-6指定应付·实现流程与逻辑).md b/doc/docs/业务流程/2026-10-31-先票后款改造方案V2(收票登记A303-2+付款结算单A102-2+A302-6指定应付·实现流程与逻辑).md new file mode 100644 index 00000000..d3f8c943 --- /dev/null +++ b/doc/docs/业务流程/2026-10-31-先票后款改造方案V2(收票登记A303-2+付款结算单A102-2+A302-6指定应付·实现流程与逻辑).md @@ -0,0 +1,184 @@ +# 先票后款改造方案 V2(收票登记 A303-2 + 付款结算单 A102-2 + A302-6 指定应付 · 实现流程与逻辑) + +> 日期:2026-10-31 +> 分支:feature/20261031-bfi-收票收款改造 +> 需求来源:禅道 Bug #15850《拆分收票登记,不要求关联结算单,改为在结算单认领收票单》+ 补充说明 +> 前版方案:2026-10-31-先票后款改造方案(收票登记A303-2+付款结算单A102-2+A302-6指定应付).md(本版经需求核对后重排,口径不变) +> 接口文档:doc/docs/业财融合系统接口文档.xlsx(A303-2 / A302-6 / A901-1 / A102-2) +> 关联代码:blade-service/blade-transport(收票登记 InvoiceReceipt、正式结算单 FormalSettlement)、blade-service-api/blade-transport-api + +--- + +## 1. 需求还原(原文逐条) + +**禅道 #15850**:拆分收票登记,不要求关联结算单,改为在结算单认领收票单,认领结果关联到收票单上展示。 + +**补充说明(2026-10-31)**: + +| # | 需求原文 | 理解 | +| --- | --- | --- | +| 1 | 收票登记:需要选择发票,推金蝶收票单批量保存 A303-2 | 收票登记改为纯"收票"动作:从票池选发票 → 推 A303-2 生成金蝶收票单 | +| 2 | 往来户:发票的销售方,在金蝶的供应商代码 | A303-2 主表 receivablessupp_number = 销方名称经客商档案→A901-1 查供应商编码 | +| 3 | 结算组织:合同的承办部门 | 收票登记不挂合同后由**页面手选承办部门**,沿部门树上溯平台公司取金蝶组织编码 | +| 4 | 明细就是发票上的 | A303-2 分录逐行取发票明细,不再从结算明细/费用项拆分 | +| 5 | 在付款结算单上做发票跟结算单的指定:选收票单 + 付款结算单 | 结算单侧认领收票单;财务应付单(A102-2)生成后调 A302-6 完成指定应付 | +| 6 | 整体就是先票后款 | 收票登记=先票(只推收票单),付款结算单=后款(生成应付单+指定应付) | + +**原始需求截图中的两处口径,与简单实现有偏差,已拍板**: +- 截图写"可选**属于该合同**…的收票单"——因收票登记不再挂合同,选票**不按合同过滤**(拍板口径:收票登记先不挂合同); +- 截图付款申请单部分——本期不动(拍板口径 8)。 + +## 2. 需求符合性核对结论 + +前版方案(V1)10 条拍板口径与需求逐条对得上,**无缺口**;3 项默认(明细取镜像不新建接口 / 结算组织页面手选 / 币别固定 CNY)即"简单改造"的取值口径,本版直接沿用并落地。已预改 3 处代码(supplierCode 字段、SaveRequest deptId/deptName、kingdeeOrgNumberByDept)与本方案一致,直接生效。 + +## 3. 总体链路(先票后款两段式) + +### 阶段一:先票——收票登记(不挂结算单/合同) + +``` +选票(A801-1 票池镜像)→ 查发票明细(镜像 items_json)→ 手选结算组织 → 保存草稿 + (销方供应商代码尽力回填:销方名称→客商档案→A901-1 supplierCode) +→ 提交审批 → 审批通过 → A303-2 收票单批量保存(分录=发票明细行,往来户=销方供应商代码) +→ 终态【pushed 已保存收票单】(不再立即 A302-6,也删除 A102-4 应付单预检) +→ 附件联带上传 A902 +→ A801-3 定时回写:红冲/作废回收 failed;金蝶缺单号补登 +``` + +### 阶段二:后款——付款结算单 + +``` +路径A 新增/编辑时关联:发票信息"选择"收票单(候选=审批通过+未被认领)→ 全额认领 +路径B 审批后补关联:列表"发票登记" → 选票保存 → 弹窗"是否将发票匹配数据推送金蝶?" → 确认调 A302-6 + → 两处落关系:FormalSettlementInvoice(结算单侧展示)+ InvoiceReceiptSettlement(收票登记"结算信息"区展示) +→ 结算单审核通过 → A102-2 财务应付单(现状保留)→ 对本单认领且收票单已推(pushed)的逐单 A302-6 指定应付 +→ 收票单【assigned 已指定应付】;单笔失败不阻断结算同步,由 A801-3"指定补偿"自动重试 +``` + +### 关键时序(后款指定应付) + +| 事件 | 动作 | +| --- | --- | +| 结算单审核通过 | A102-2 推送成功后,遍历本单认领关系:收票单 pushed → 调 A302-6 → 置 assigned;失败仅记日志汇总,不阻断 | +| 收票单尚未推送(A303-2 未完成) | 跳过;收票重推任务推成 pushed 后,A801-3"指定补偿"按认领关系自动补指定 | +| 发票登记确认推送 | 手动触发同上指定逻辑(A102-2 已同步 synced 是前置) | +| 结算单作废 / 认领移除 | **不同步金蝶**(不调 A302-6 unassign),金蝶侧人工解除指定后重推 | + +## 4. 数据模型变更与 SQL + +| 变更 | 内容 | 说明 | +| --- | --- | --- | +| `blade_invoice_receipt` 加列 | `supplier_code VARCHAR(50)` | 往来户金蝶供应商代码(发票销售方,保存时尽力回填、推送前强校验) | +| 新表 | 无 | 明细复用 `blade_kingdee_invoice_pool.items_json`(行号 seq 做稳定分录键),不建快照表 | +| 关系表 | 复用 `InvoiceReceiptSettlement` | 收票登记↔结算单认领关系,由结算单侧重建 | +| 菜单 | 付款结算单(code=payment_settlement)下加"发票登记"按钮 | code=payment_settlement_invoice_register | + +SQL:`doc/sql/transport/lqj/temp/06_blade_invoice_receipt_supplier_code_menu_20261031.sql`,**可重复执行**: +1. `supplier_code` 列:information_schema 判断 + PREPARE 动态 ALTER(utf8mb4_general_ci); +2. 菜单:parent 按 code=payment_settlement 定位,NOT EXISTS 防重。 + +## 5. A303-2 报文 + +### 5.1 主表(对比现状) + +| 字段 | 现状 | 目标 | +| --- | --- | --- | +| receivablessupp_number 往来户.编码 | 合同乙方客商编码(bfiAsstactNumber) | **receipt.supplierCode**(销售方金蝶供应商代码);缺失实时查一次 A901-1,仍缺失报错阻断 | +| asstact_number 销售方名称.编码(非必填) | 不送 | 同 supplierCode | +| org_number 结算组织.编码 | 合同项目业务部门反查平台公司 | **页面手选部门**沿部门树反查平台公司(`kingdeeOrgNumberByDept`,已预改) | +| currency_number 结算币别 | 合同结算币种 | 固定 **CNY** | +| asstacttype 往来类型 | 固定常量 | 固定常量不变(不挂合同直接取常量) | +| 其余(awrp_src_code=登记id、receivedate=当日、issuedate、biztype=PUR、invoicetypef7、paymenttype、税率表默认、销购双方名称/税号/银行) | — | 不变 | + +### 5.2 分录(镜像明细行逐行一条) + +| 金蝶字段 | 取值 | +| --- | --- | +| e_pricetaxtotal | 明细金额 detailAmount + 税额 taxAmount(合计为 0 拦截) | +| quantity | num(≤0/空兜底 1) | +| price | unitPrice(空按 价税合计/数量 反算) | +| material_number | goodsCode(行商品编码,缺失拦截) | +| e_taxclass_number | goodsCode(数电票行商品编码即税收分类编码) | +| taxrateid_number | "V"+整数税率(13/13%/0.13→V13;缺失、0/免税行拦截提示人工) | +| e_amount / e_tax | detailAmount / taxAmount(空不送) | +| awrp_entry_code | **发票号码_seq**(候选键,重推幂等) | + +> `BfiReceivedInvoiceSaveEntryDTO` 已有 eTaxclassNumber / taxrateidNumber 字段,只差赋值。明细数据源与进项发票明细接口(rim_inv_electric_qu)行结构一致,**不新建 BFI Feign 接口**;镜像缺票种/明细时按票号回源 fetchMirrorItemsFromPool 兜底。 + +## 6. 后端改造清单(按文件) + +### blade-transport-api(2 处已预改 ✓) + +| 文件 | 改动 | +| --- | --- | +| pojo/entity/InvoiceReceipt.java | +`supplierCode`(✓ 已改) | +| pojo/dto/InvoiceReceiptSaveRequest.java | +`deptId/deptName`;`settlements` 注释改为"兼容历史入参"(✓ 已改) | + +### blade-service/blade-transport + +| 文件 | 改动 | +| --- | --- | +| support/KingdeeApBillSyncSupport.java | +`kingdeeOrgNumberByDept(deptId, context)`,kingdeeOrgNumber 重构复用 `platformCompanyOrgNumber`(✓ 已改) | +| support/KingdeeReceivedInvoiceSyncSupport.java | pushReceivedInvoice 签名改 `(receipt, mirror, context)`(去 contract/entryGroups);主表按 §5.1 取值;buildEntries 改镜像明细行(§5.2)+ parseMirrorItems / taxRateCode / parseMirrorAmount 辅助 | +| service/impl/InvoiceReceiptServiceImpl.java | 见下方①~⑤ | +| service/IInvoiceReceiptService.java | +4 方法声明,推送方法 Javadoc 更新 | +| service/impl/FormalSettlementServiceImpl.java | 见下方⑥~⑧ | +| service/IFormalSettlementService.java | +pushInvoiceAssignKingdee | +| controller/InvoiceReceiptController.java | +GET `/invoice-receipt/invoice-detail-query?invoiceNo=`(读镜像 items_json,缺则回源解析);+GET `/invoice-receipt/claim-candidates?keyword=`(approved+未被认领分页,limit 200);/push-kingdee 文案更新 | +| controller/FormalSettlementController.java | +POST `/formal-settlement/invoice-assign-kingdee` | + +**InvoiceReceiptServiceImpl**: +1. saveDraft:settlements 可选,未传时**不动**既有分摊关系(关系只由结算单侧维护);结算组织取手选 deptId/deptName;付款方=受票单位、收款方=开票单位;supplierCode 经 queryCustomerCode(销方名称) 尽力回填; +2. validateRequest 去掉必选结算单校验;validateStoredAllocation 无关联时跳过; +3. 推送计划缩为 (receipt, mirror, blockedReason):删 A102-4 预检与 A302-6 立即指定(推送终点=pushed);推送前置补齐镜像明细(itemsJson/invoiceTypeCode 空则按票号回源); +4. 回写 reassignPushedReceivedInvoice:无认领关系 → 跳过(不再置 failed); +5. 新增 invoiceDetailQuery / claimCandidates / syncSettlementReceiptRelations / assignClaimedReceipts; +6. `poolMirrorChanged` 比对集补 `itemsJson`(filled 为 null 时跳过比对):明细行变化而票面金额未变(单价变总价不变、goodsCode 修正、行拆合)时不再被"无变化"跳过——先票后款后 items_json 是 A303-2 分录唯一取数来源,必须跟随票池刷新。 + +**FormalSettlementServiceImpl**: +6. rebuildInvoices(payable)与 claimInvoices 末尾挂钩 `invoiceReceiptService.syncSettlementReceiptRelations(settlementId, invoiceRows)`:按行发票号匹配审批通过收票登记 + **一票一结算单校验**(已挂其他非作废应付结算单则拦截)+ 重建 InvoiceReceiptSettlement + 刷新结算单收票状态; +7. doSyncKingdee payable 分支:A102-2 成功后调 `assignClaimedReceipts(settlementId, apBillSrcCode)`(失败不阻断); +8. 新增 pushInvoiceAssignKingdee(settlementId):前置 payable + approved + kingdeeSyncStatus=synced,调 A302-6。 + +**新方法语义**: +- `syncSettlementReceiptRelations(settlementId, invoiceRows)`:一票一结算单校验 → 删本结算单旧关系 → 按 invoiceNo 匹配 approved 收票登记重建关系(allocatedInvoiceAmount=matchedAmount 全额认领)→ 刷新本结算单及被移除关系结算单的收票状态; +- `assignClaimedReceipts(settlementId, apBillSrcCode)`:遍历认领关系,收票单 pushed → A302-6 指定 → assigned;未推送/已指定分别跳过;失败收集汇总返回; +- `claimCandidates(keyword)`:approved + 未被任何非作废应付结算单认领 + 关键字(发票号/销方/购方),limit 200。 + +## 7. 定时任务影响 + +| 任务 | 变化 | +| --- | --- | +| A801-3 收票结果回写(syncKingdeeReceivedInvoiceResult) | "指定补偿"升级为重试引擎:pushed + 已认领 → 自动补发 A302-6;pushed + 未认领 → 跳过不再置 failed;异常回收/单号补登不变 | +| A801-1 票池同步(syncKingdeeInvoicePool) | 明细本已随 fillInvoicePool 落 items_json(新增/更新均写,源侧无明细维持原值);改造点仅在 poolMirrorChanged 比对集补 itemsJson(见 §6-6) | +| 收票推送重推(retryKingdeeFailed / 自动重推) | 不再依赖 A102-4,重推仅需镜像+供应商代码+结算组织齐备 | +| 结算单自动重推(KingdeeAutoSyncRetryTask) | 不变(重推 A102-2 后仍会触发指定) | + +## 8. 前端改造点(../tms-erp-web,另仓库) + +- 收票登记:去税率、去关联结算单选择;加"结算组织"部门选择(必选,即合同承办部门);加发票明细区块(invoice-detail-query 按票号带出);详情"结算信息"区展示认领数据(reference-information/detail 已有出参); +- 付款结算单:新增页发票信息"选择"数据源切 `/invoice-receipt/claim-candidates`(审批通过+未认领),匹配金额固定=发票金额;编辑/查看去税率列;查看页去"认领查询"按钮;列表加"发票登记"入口;发票登记页=结算基本信息只读 + 选票 + 保存后"是否将发票匹配数据推送金蝶?"弹窗 → 确认调 `/invoice-assign-kingdee`。 + +## 9. 兼容性与风险 + +| 点 | 说明 | +| --- | --- | +| 历史收票登记 | 已 assigned 单据不受影响;历史编辑保存未传 settlements 时保留原分摊关系不动 | +| 历史分摊链路 | 传 settlements 的老入参仍走原校验/落关系逻辑(前端切换期间兼容) | +| pushed 状态语义变化 | 收票推送终态由 assigned 变 pushed,KingdeeBfiConstant.RECEIVED_PUSH_PUSHED 相关判断需同步梳理(锁后幂等判断、A801-3 回写分支等) | +| 明细时点 | 明细取镜像最新值:登记→推送间金蝶侧若修正明细行,推送用新明细;红冲/作废由回写回收状态兜底 | +| 免税/零税率行 | taxrateid_number 无法映射,推送时报错人工核实(如业务出现需与金蝶约定编码后放开) | +| currency_number=CNY | 不挂合同后的默认,出现外币票需补充取值来源 | +| 反向操作 | 结算单作废/认领移除不同步金蝶(已拍板),金蝶侧人工解除指定后重推 | + +## 10. 编码顺序与验证 + +1. blade-transport-api:实体/DTO(✓ 已预改); +2. 支撑组件:KingdeeReceivedInvoiceSyncSupport 报文改造; +3. 收票服务:InvoiceReceiptServiceImpl ①~⑤ + 接口声明; +4. 结算服务:FormalSettlementServiceImpl ⑥~⑧ + 接口声明; +5. 控制器:两个端点收票侧 + 一个端点结算侧; +6. SQL:06_blade_invoice_receipt_supplier_code_menu_20261031.sql(可重复执行); +7. 编译:`mvn clean compile -DskipTests -pl blade-service-api/blade-transport-api,blade-service/blade-transport -am`;测试交由用户执行。 + +> 付款申请单(A103-1)链路本期不动。 diff --git a/doc/docs/业务流程/2026-11-01-先票后款业务流程与逻辑状态说明(收票登记A303-2+付款结算单A102-2+A302-6).md b/doc/docs/业务流程/2026-11-01-先票后款业务流程与逻辑状态说明(收票登记A303-2+付款结算单A102-2+A302-6).md new file mode 100644 index 00000000..d51d0bc1 --- /dev/null +++ b/doc/docs/业务流程/2026-11-01-先票后款业务流程与逻辑状态说明(收票登记A303-2+付款结算单A102-2+A302-6).md @@ -0,0 +1,149 @@ +# 先票后款业务流程与逻辑状态说明(收票登记 A303-2 + 付款结算单 A102-2 + A302-6) + +> 更新日期:2026-11-03 | 适用系统:运输 TMS(业财融合·金蝶星瀚) | 读者:业务/产品/开发 +> +> 关联文档:《先票后款纯操作手册》(同目录,纯操作步骤+截图)、《先票后款改造方案V2》(方案与改造清单) +> +> 需求来源:禅道 #15850——收票登记与结算单解耦,改为"先票后款"两段式 + +--- + +## 修改记录 + +| 日期 | 版本 | 修订内容 | 依据 | +| --- | --- | --- | --- | +| 2026-11-01 | V1.0 | 初版:先票后款全链路(A303-2/A102-2/A302-6)流程、状态机与遗留项 | 代码至 933fb37d(10-31) | +| 2026-11-03 | V1.1 | ① A303-2 分录定稿:送税收分类编码(行商品编码),不送物料编码/税率表编码/税额,§6 移除物料编码与免税零税率遗留项;② 匹配结算单金额支持手动修改(默认发票全额,0≤金额≤发票金额,未匹配回退全额),§2/§3.2 同步;③ 新增查看页重推(/repush-claimed-invoice,A303-2+A302-6 一步式,failed/pushed 通吃);④ A801-3 指定补偿增加开关 assignCompensationEnabled 默认关闭,改人工入口兜底;⑤ 接口表补充 claimed-list 回显、候选分页、列表指定失败悬浮(kingdeeAssignFailReason)等 | 代码 c82ce7ec、6c5169ea、49ccb435、34e90301/746fd3e9、b7d417f3、fe0ba8e8、da1ddf7b/2943a7c3(11-02) | + +--- + +## 1. 一页总览 + +收票登记不再强制关联结算单,拆成两段:**先票**(收票登记只负责把发票推进金蝶收票单),**后款**(付款结算单认领收票单,推财务应付单后完成"发票指定应付"勾稽)。 + +```mermaid +flowchart TD + subgraph 先票-收票登记 + A1[票池选发票
明细自动带出] --> A2[手选结算组织
往来户=销方供应商代码] + A2 --> A3[保存草稿/提交审批] + A3 --> A4{审批通过} + A4 --> A5[A303-2 推金蝶收票单
分录=发票明细行] + A5 --> A6[pushed 已保存收票单
附件联带上传 A902] + end + subgraph 后款-付款结算单 + B1[认领收票单
路径A 新增/编辑时选择
路径B 审批后发票登记] --> B2[一票一结算单校验
全额认领] + B2 --> B3[审批通过] + B3 --> B4[A102-2 财务应付单] + B4 --> B5[A302-6 逐单指定应付] + B5 --> B6[assigned 已指定应付] + end + A6 -.收票单已推.pushed.-> B1 + B4 -.推送失败/漏指定.-> C[A801-3 定时回写
指定补偿(开关默认关闭
默认人工重推兜底)] --> B5 +``` + +## 2. 名词与状态字典 + +| 名词/状态 | 含义 | +| --- | --- | +| pushed(已保存收票单) | 收票登记推送**终态**:A303-2 保存成功。不再立即指定应付 | +| assigned(已指定应付) | 金蝶侧已与财务应付单完成勾稽(A302-6 成功)。历史数据终态,新链路由结算单侧驱动 | +| failed(推送失败) | A303-2 保存失败 / 金蝶侧发票异常回收。列表"推送"按钮或定时任务自动重推 | +| unsynced(未推送) | 审批通过但尚未推送(存量历史单不自动补推,走手动按钮) | +| unreceived / partial / completed | 结算单收票状态:未收票 / 匹配合计未覆盖结算金额 / **匹配合计(各票匹配金额之和)≥ 结算金额即完成**(允许超过) | +| 匹配结算单金额 | 认领时每张发票的认领金额,**默认=发票金额全额,支持手动修改**(0 ≤ 匹配金额 ≤ 发票金额);未传时分摊金额回退发票全额 | +| 往来户 | A303-2 主表 receivablessupp_number = **发票销售方**在金蝶的供应商代码(客商档案 supplier_code) | +| 结算组织 | 收票登记页面**手选部门**(合同承办部门口径),沿部门树上溯平台公司取金蝶组织编码 | +| 一票一结算单 | 一张收票单只能被一张付款结算单认领;认领金额=匹配结算单金额(默认发票全额,可改) | + +## 3. 各环节业务规则 + +### 3.1 收票登记(先票) + +- **不挂结算单、不挂合同**:表单无"关联结算单";结算组织=页面必选手选部门。 +- **选票即带明细**:从金蝶票池(A801-1 镜像)选发票后,明细区块按票号带出发票明细行(goodsCode/数量/单价/金额/税额/税率),与推送分录同源。 +- **分录口径(联调已定)**:分录送税收分类编码(取行商品编码去尾部 0,填 `eTaxclassNumber`),**不送物料编码、税率表编码、税额**;税额仅参与价税合计;单价按 4 位小数舍入。 +- **往来户回填**:保存时按销方名称查客商档案回填 `supplier_code`(尽力回填,不阻断保存)。 +- **推送前置**:审批通过 + 已绑定镜像 + 查验通过 + 已选结算组织;往来户供应商代码缺失时**推送时实时重查档案**,仍缺失报错阻断。 +- **推送成功即终态 pushed**:删除了旧链路的"应付单已生成预检(A102-4)"和"立即指定应付"。 +- 附件联带上传 A902(登记附件 + 票池发票文件),失败不影响推送状态,重推自动补传。 + +### 3.2 付款结算单认领收票单(后款) + +- **两条路径,同一套关系同步**: + - 路径 A:新增/编辑结算单时"选择收票单"(编辑器); + - 路径 B:审批通过后列表**工具栏**【发票登记】(选中一条审批通过的付款结算单)→弹窗选票→【保存认领】。 +- **候选范围**:审批通过 + 未被有效应付结算单认领(关联仍指向作废结算单的票可重新选);候选弹窗为**分页查询**,行上悬浮展示该票推送失败原因。 +- **一票一结算单**:认领时校验,已挂其他有效应付结算单的票拦截(行锁防并发认领)。 +- **匹配结算单金额可手动修改**:默认=发票金额全额;手动修改范围 0 ≤ 匹配金额 ≤ 发票金额(后端兜底校验);结算单侧分摊金额取发票行匹配结算单金额,未匹配到的行回退发票全额;**匹配合计允许超过结算金额**,收票状态"匹配合计 ≥ 结算金额即 completed"。 +- **双边落关系**:结算单侧 FormalSettlementInvoice(发票信息区)+ 收票登记侧 InvoiceReceiptSettlement("结算信息"区展示)。 +- **路径 B 先确认后保存**:发票登记弹窗打开即回显本单已认领票("已认领"标记,不可移除);【保存认领】后弹出确认"确认认领并推送金蝶指定应付?"——**确定=认领保存与推送指定应付一步完成;取消=不保存任何数据**(不存在只认领不推送的中间态)。 +- 历史兼容:编辑历史结算单仍走原增量认领;收票登记传 settlements 的老入参仍走原分摊校验。 + +### 3.3 财务应付单与指定应付 + +- 结算单审核通过 →"同步金蝶"推 A102-2 → **成功后自动对本单认领且收票单为 pushed 的逐单调 A302-6** → assigned。 +- 收票单尚未推送(unsynced/failed):跳过不报错;收票侧推成 pushed 后由**人工入口**(发票登记推送/查看页重推)指定,或开启 A801-3 补偿开关后自动补指定。 +- **单笔指定失败不阻断结算同步**,失败明细进日志;已指定的幂等跳过。 +- 路径 B 的"发票登记"确认推送(`/invoice-assign-kingdee`)前置:付款结算单 + 审批通过 + 应付单已同步(synced)。 +- **查看页重推认领收票单**(`/formal-settlement/repush-claimed-invoice`,A303-2+A302-6 一步式):前置=付款结算单 + 审批通过 + 应付单已同步 + 该票认领在本单名下;**failed/pushed 通吃**(已 assigned 拦截),failed 行可同步修改匹配结算单金额随重推生效;收票单重推复用 A303-2 状态机(A801-3 幂等预检防重复保存),成功后按应付单防重键指定应付。 + +### 3.4 定时回写与补偿(A801-3) + +| 场景 | 处理 | +| --- | --- | +| 金蝶侧发票状态异常(作废/红冲) | 回收本地状态为 failed,提示人工删除/解除指定后重推 | +| pushed + 已认领 | **指定补偿(重试引擎)**:按认领关系自动补发 A302-6——受开关 `thirdParty.bfi.receivedInvoiceResult.assignCompensationEnabled` 控制,**默认关闭**;关闭时 pushed 单据由人工入口(发票登记推送/查看页重推按钮)指定 | +| pushed + 未认领 | 跳过,**不再置 failed**(未指定不属异常) | +| assigned 缺收票单号 | 仅补登单号 | + +### 3.5 反向操作 + +- 结算单作废、认领移除**不同步金蝶**(不调 unassign),金蝶侧人工解除指定后重推。 +- 收票登记作废:金蝶已生成收票单(pushed/assigned)时本地禁止作废,须先在金蝶删除/解除指定。 + +## 4. 状态机 + +### 4.1 收票登记 kingdeePushStatus + +``` +unsynced ──推送成功(A303-2)──▶ pushed ──结算单A102-2成功自动指定/发票登记确认/查看页重推/A801-3补偿(开关默认关)──▶ assigned + │ │ + ├──手动/自动推送失败──▶ failed ◀──金蝶侧异常回收(作废/红冲) + │ │ + └────────────重推(列表按钮/定时/查看页重推)◀──┘ +``` + +- pushed 是新链路推送终点;assigned 只能由结算单侧指定动作(A102-2 成功自动指定、发票登记确认、查看页重推)或 A801-3 补偿(开关默认关闭)到达。 +- failed 重推不再依赖结算单/A102-4,仅需镜像明细 + 往来户编码 + 结算组织齐备;查看页重推对 failed/pushed 均可一步完成收票单重推+指定应付。 + +### 4.2 结算单收票状态(invoice_status) + +``` +unreceived ──首次认领──▶ partial ──Σ认领金额 ≥ 结算金额──▶ completed +``` + +- 认领/移除/编辑保存时实时重算(refreshSettlementInvoiceStatus)。 + +## 5. 接口与数据落点 + +| 端点/任务 | 说明 | +| --- | --- | +| GET `/invoice-receipt/invoice-detail-query?invoiceNo=` | 发票明细区块取数(镜像 items_json,缺则回源 A801-1) | +| GET `/invoice-receipt/claim-candidates?keyword=&page=` | 认领候选**分页**(approved+未认领) | +| GET `/invoice-receipt/claimed-list?formalSettlementId=` | 结算单已认领收票登记列表(发票登记弹窗回显) | +| POST `/formal-settlement/invoice-assign-kingdee?id=` | 发票登记确认推送(A302-6) | +| POST `/formal-settlement/repush-claimed-invoice?id=&invoiceNo=` | 查看页重推认领收票单(A303-2+A302-6 一步式,failed/pushed 通吃,携带匹配金额校验) | +| POST `/invoice-receipt/push-kingdee` | 收票登记推送(A303-2,终态 pushed) | +| 定时 A801-1 票池同步 | 镜像含明细 items_json;明细变化即刷新(含金额未变场景) | +| 定时 A801-3 收票结果回写 | 异常回收 / 指定补偿(开关默认关)/ 单号补登 | +| 列表展示 | 结算单/付款申请单列表"金蝶指定状态"标签(未指定/已指定应付/指定失败,failed 悬浮展示失败原因 `kingdeeAssignFailReason`);付款申请单列表增加"已指定应付"聚合列;结算单发票明细回显收票单推送状态(`kingdeePushStatus`/`kingdeePushMsg`) | +| 表 | `blade_invoice_receipt.supplier_code`(新列)、`blade_invoice_receipt_settlement`(认领关系)、`blade_formal_settlement_invoice`(结算单发票区,含匹配金额/推送状态回显) | + +## 6. 已知边界与联调遗留 + +| 项 | 说明 | +| --- | --- | +| ~~物料编码口径~~ | **已解决(2026-11-02)**:分录不送物料编码,改送税收分类编码(行商品编码) | +| ~~免税/零税率行~~ | **已解决(2026-11-02)**:税率表编码不再上送,无税率表映射报错 | +| 币别 | 固定 CNY(不挂合同,数电票人民币) | +| 付款申请单 A103-1 | 本期不动(仅列表新增"已指定应付"聚合列与指定失败悬浮展示) | diff --git a/doc/docs/业务流程/2026-11-01-先票后款操作手册.assets/01-收票登记列表.png b/doc/docs/业务流程/2026-11-01-先票后款操作手册.assets/01-收票登记列表.png new file mode 100644 index 00000000..8ad3651e Binary files /dev/null and b/doc/docs/业务流程/2026-11-01-先票后款操作手册.assets/01-收票登记列表.png differ diff --git a/doc/docs/业务流程/2026-11-01-先票后款操作手册.assets/02-收票登记新增-结算组织与明细区块.png b/doc/docs/业务流程/2026-11-01-先票后款操作手册.assets/02-收票登记新增-结算组织与明细区块.png new file mode 100644 index 00000000..69db666a Binary files /dev/null and b/doc/docs/业务流程/2026-11-01-先票后款操作手册.assets/02-收票登记新增-结算组织与明细区块.png differ diff --git a/doc/docs/业务流程/2026-11-01-先票后款操作手册.assets/03-收票登记选票后-发票明细带出.png b/doc/docs/业务流程/2026-11-01-先票后款操作手册.assets/03-收票登记选票后-发票明细带出.png new file mode 100644 index 00000000..8cbad1f2 Binary files /dev/null and b/doc/docs/业务流程/2026-11-01-先票后款操作手册.assets/03-收票登记选票后-发票明细带出.png differ diff --git a/doc/docs/业务流程/2026-11-01-先票后款操作手册.assets/04-收票登记详情-结算信息认领展示.png b/doc/docs/业务流程/2026-11-01-先票后款操作手册.assets/04-收票登记详情-结算信息认领展示.png new file mode 100644 index 00000000..b997b0a5 Binary files /dev/null and b/doc/docs/业务流程/2026-11-01-先票后款操作手册.assets/04-收票登记详情-结算信息认领展示.png differ diff --git a/doc/docs/业务流程/2026-11-01-先票后款操作手册.assets/05-付款结算单列表-发票登记入口.png b/doc/docs/业务流程/2026-11-01-先票后款操作手册.assets/05-付款结算单列表-发票登记入口.png new file mode 100644 index 00000000..595dc5f0 Binary files /dev/null and b/doc/docs/业务流程/2026-11-01-先票后款操作手册.assets/05-付款结算单列表-发票登记入口.png differ diff --git a/doc/docs/业务流程/2026-11-01-先票后款操作手册.assets/06-发票登记弹窗-结算信息与选票.png b/doc/docs/业务流程/2026-11-01-先票后款操作手册.assets/06-发票登记弹窗-结算信息与选票.png new file mode 100644 index 00000000..1388da99 Binary files /dev/null and b/doc/docs/业务流程/2026-11-01-先票后款操作手册.assets/06-发票登记弹窗-结算信息与选票.png differ diff --git a/doc/docs/业务流程/2026-11-01-先票后款操作手册.assets/07-选择收票单候选弹窗.png b/doc/docs/业务流程/2026-11-01-先票后款操作手册.assets/07-选择收票单候选弹窗.png new file mode 100644 index 00000000..dbfc6bb6 Binary files /dev/null and b/doc/docs/业务流程/2026-11-01-先票后款操作手册.assets/07-选择收票单候选弹窗.png differ diff --git a/doc/docs/业务流程/2026-11-01-先票后款操作手册.assets/08-结算单编辑-选择收票单按钮与发票信息.png b/doc/docs/业务流程/2026-11-01-先票后款操作手册.assets/08-结算单编辑-选择收票单按钮与发票信息.png new file mode 100644 index 00000000..b036ba3d Binary files /dev/null and b/doc/docs/业务流程/2026-11-01-先票后款操作手册.assets/08-结算单编辑-选择收票单按钮与发票信息.png differ diff --git a/doc/docs/业务流程/2026-11-01-先票后款操作手册.assets/09-结算单查看页-发票信息重推.png b/doc/docs/业务流程/2026-11-01-先票后款操作手册.assets/09-结算单查看页-发票信息重推.png new file mode 100644 index 00000000..4a388fe3 Binary files /dev/null and b/doc/docs/业务流程/2026-11-01-先票后款操作手册.assets/09-结算单查看页-发票信息重推.png differ diff --git a/doc/docs/业务流程/2026-11-01-先票后款纯操作手册(收票登记-结算单认领-发票登记).md b/doc/docs/业务流程/2026-11-01-先票后款纯操作手册(收票登记-结算单认领-发票登记).md new file mode 100644 index 00000000..625117a1 --- /dev/null +++ b/doc/docs/业务流程/2026-11-01-先票后款纯操作手册(收票登记-结算单认领-发票登记).md @@ -0,0 +1,134 @@ +# 先票后款纯操作手册(收票登记 → 结算单认领 → 发票登记) + +> 更新日期:2026-11-03 | 适用系统:运输 TMS | 读者:业务/财务操作人员 +> +> 配套阅读:《先票后款业务流程与逻辑状态说明》(同目录) +> +> 菜单路径:**收付款管理 → 收票管理**(收票登记);**结算管理 → 付款结算单**(认领与发票登记) +> 按钮权限:收票登记新增/编辑/提交/推送;付款结算单"发票登记"(invoice_register) + +--- + +## 修改记录 + +| 日期 | 版本 | 修订内容 | +| --- | --- | --- | +| 2026-11-01 | V1.0 | 初版:收票登记→结算单认领→发票登记操作手册(8 图) | +| 2026-11-03 | V1.1 | ① 场景二/三:认领金额改"匹配结算单金额"口径(默认发票全额,可手动修改);② 场景三重写:发票登记入口移至列表**工具栏**(勾选一条审批通过付款结算单),弹窗自动回显已认领票,确认后认领+推送一步完成(取消不保存任何数据);③ 新增场景四"查看页重推认领收票单"(指定失败/漏指定补救,失败行可改匹配金额随重推生效),原异常补偿顺延为场景五;④ 场景五:指定应付自动补偿默认关闭改人工重推,"指定失败"标签悬浮查看原因,失败对照表移除已消除的物料编码/税率表映射报错;⑤ 截图重截 05-08 并新增 09 查看页重推图(收票登记 01-04 无变化) | + +--- + +## 场景一:收票登记(先票——把发票推进金蝶) + +### 1.1 新增收票登记 + +1. 菜单进入【收票管理 → 收票登记】,点【新增】。 +2. 点发票号码右侧 🔍 从**金蝶票据池**选一张发票——选票后**开票信息与发票明细自动带出**(明细与推送金蝶的分录同源,请核对)。 +3. **手选"结算组织"(必选)**:即该合同的承办部门,系统沿部门树向上找平台公司作为金蝶结算组织。 +4. 核对受票单位(购方)、开票单位(销方);填备注、附件(如有)。 +5. 点【保存】。 + +![收票登记列表](./2026-11-01-先票后款操作手册.assets/01-收票登记列表.png) + +![新增收票登记-结算组织与发票明细](./2026-11-01-先票后款操作手册.assets/02-收票登记新增-结算组织与明细区块.png) + +![选票后发票明细带出](./2026-11-01-先票后款操作手册.assets/03-收票登记选票后-发票明细带出.png) + +> 说明:收票登记**不再选择/关联结算单**;与结算单的关联在"场景二/三"由付款结算单侧认领建立。 + +### 1.2 提交与审批(审批通过自动推金蝶) + +1. 表单点【提交】(或列表勾选后提交),走收票审核流程。 +2. **审批通过后系统自动推送金蝶收票单(A303-2)**,无需手工操作;推送成功后推送状态变为 **已保存收票单(pushed)**。 +3. 列表"推送状态"列查看:未推送 / 已保存收票单 / 已指定应付 / 推送失败。 +4. 推送失败的单据:列表勾选点【推送】重推(也可等定时任务自动重推);常见失败原因见文末对照表。 + +### 1.3 查看认领结果 + +收票登记详情页"结算信息"区展示该发票被哪张付款结算单认领(由结算单侧认领后自动写入,收票登记上不能手工维护)。 + +![详情-结算信息认领展示](./2026-11-01-先票后款操作手册.assets/04-收票登记详情-结算信息认领展示.png) + +--- + +## 场景二:付款结算单认领收票单(路径 A:新增/编辑时选择) + +1. 菜单进入【结算管理 → 付款结算单】,【新增】。 +2. 填写结算基本信息与明细后,在**发票信息**区点【选择收票单】。 +3. 候选弹窗**分页**列出**审批通过且未被其他结算单认领**的收票单(支持发票号/销方/购方关键字查询,行上可悬浮查看推送失败原因),勾选后【确定】。 +4. 认领金额即"**匹配结算单金额**":默认=**发票金额全额**,支持手动修改(不能为负、不能超过发票金额),一张收票单只能被一张付款结算单认领。 +5. 保存结算单 → 提交 → 审批通过。 +6. 审批通过后点【同步金蝶】推财务应付单(A102-2);**应付单推送成功后,系统自动把本单认领的收票单逐单指定应付(A302-6)**,收票登记状态随之变为"已指定应付"。 + +![结算单编辑-选择收票单](./2026-11-01-先票后款操作手册.assets/08-结算单编辑-选择收票单按钮与发票信息.png) + +![选择收票单候选](./2026-11-01-先票后款操作手册.assets/07-选择收票单候选弹窗.png) + +--- + +## 场景三:发票登记(路径 B:结算单审批后补认领) + +适用:结算单**已审批通过**,事后才拿到发票/收票单。 + +1. 付款结算单列表,**勾选一条**目标单(审批通过的付款结算单),点列表上方工具栏【发票登记】。 +2. 弹窗上半部为结算基本信息(只读),下半部"认领收票单"区**自动回显本单已认领的收票单**("已认领"标记,不可移除)。 +3. 点【选择收票单】,候选弹窗**分页**列出审批通过且未被其他结算单认领的收票单,勾选后【确定】(选票先暂存在弹窗表格里,点保存认领才落库)。 +4. 核对/修改每张票的"匹配结算单金额"(默认=发票金额全额,可手动改,不能为负、不能超过发票金额;不需要的暂存票点【移除】)。 +5. 点【保存认领】→ 系统弹窗"**确认认领并推送金蝶指定应付?**": + - 【确定】:认领保存 + 逐单执行收票单指定应付(A302-6)**一步完成**,成功提示带结算单号; + - 【取消】:**不保存任何数据**(已勾选的票不会认领)。 +6. 已认领过票的结算单再次打开发票登记,可直接【保存认领】对漏指定的票补推送;已指定的收票单重复推送自动跳过(幂等)。 +7. 指定失败的票:结算单列表"金蝶指定状态"列显示红色"指定失败"标签,**鼠标悬浮可见失败原因**;修复后按场景四重推。 + +![付款结算单列表-发票登记入口](./2026-11-01-先票后款操作手册.assets/05-付款结算单列表-发票登记入口.png) + +![发票登记弹窗](./2026-11-01-先票后款操作手册.assets/06-发票登记弹窗-结算信息与选票.png) + +--- + +## 场景四:查看页重推认领收票单(指定失败/漏指定的补救) + +适用:付款结算单已同步金蝶应付单,但名下收票单**指定失败(failed)或未指定(pushed)**。 + +1. 打开付款结算单**查看页**,"发票信息"区每行展示收票单推送状态("是否已指定应付"列:未指定 / 已指定应付 / 指定失败)。 +2. 未指定应付的行(推送失败/未指定)操作列有【重新推送】链接;**指定失败的行还可直接编辑"匹配结算单金额"**,修改随重推一并生效。 +3. 点【重新推送】:系统一步完成收票单重推金蝶(A303-2)+ 指定应付(A302-6),完成后刷新详情查看状态。 +4. 前置条件(不满足时按钮不可用/报错):付款结算单 + 审批通过 + 金蝶应付单已同步 + 该票认领在本单名下;已指定应付的票重复重推会被拦截。 + +> 补充:系统定时回写任务(A801-3)的"指定应付自动补偿"**默认关闭**,漏指定的票不会自动补推,需按本场景人工重推(或由管理员开启补偿开关)。 + +![查看页-发票信息重推入口](./2026-11-01-先票后款操作手册.assets/09-结算单查看页-发票信息重推.png) + +--- + +## 场景五:异常与补偿 + +| 情况 | 系统行为 | 操作人要做的事 | +| --- | --- | --- | +| 收票推送失败(pushed 前任何一步) | 状态置"推送失败",定时任务自动重推 | 按失败原因处理(常见见下表),或列表手动重推 | +| 结算单同步应付单时收票单还没推送 | 自动跳过,不影响结算单同步 | 收票推送成功后,按场景四【重推】或重新点【发票登记】推送 | +| 收票单已推送但漏了指定 | 自动补偿**默认关闭**,不会自动补推 | 按场景四在查看页【重推】,或再次【发票登记】推送 | +| 指定应付失败 | 列表"金蝶指定状态"红色"指定失败"标签 | **悬浮标签查看失败原因**,修复后按场景四重推 | +| 金蝶侧发票被作废/红冲 | 回收为"推送失败"并提示 | 在金蝶删除/解除指定后,处理完重推 | +| 结算单作废 / 认领移除 | **不同步金蝶** | 需要时在金蝶人工解除指定 | +| 金蝶收票单已生成后本地作废 | 禁止作废 | 先在金蝶删除收票单/解除指定 | + +### 常见推送失败原因对照 + +| 报错关键字 | 原因 | 处理 | +| --- | --- | --- | +| 未建档金蝶供应商编码 | 发票销售方未维护客商档案/未金蝶建档 | 客商档案新增销方(登记税号)→ 金蝶建档/同步 → 列表直接重推 | +| 税收分类…导入不成功 | 金蝶税收分类基础资料缺对应编码 | 联系金蝶侧初始化/分配基础资料后重推 | +| 单价格式不正确 | 明细单价小数超 4 位 | 已由系统按 4 位舍入,如仍出现请联系开发 | + +> 物料编码、税率表映射两类历史报错已随分录口径调整(2026-11-02:不送物料编码/税率表编码/税额)消除,不再出现。 + +--- + +## 附:一张票的完整旅程(速查) + +``` +票池选票 → 收票登记(选结算组织) → 审批通过 → 金蝶收票单[pushed] + → 付款结算单认领(匹配金额默认全额可改) → 审批通过 → 金蝶财务应付单 + → 自动指定应付(失败/漏指定→查看页重推) → [assigned] 金蝶勾稽完成 +``` diff --git a/doc/docs/业务流程/2026-11-03-金蝶仓储费用独立分录与仓储物料映射设计.md b/doc/docs/业务流程/2026-11-03-金蝶仓储费用独立分录与仓储物料映射设计.md new file mode 100644 index 00000000..767350d4 --- /dev/null +++ b/doc/docs/业务流程/2026-11-03-金蝶仓储费用独立分录与仓储物料映射设计.md @@ -0,0 +1,130 @@ +# 金蝶仓储费用独立分录与仓储物料映射设计(修订版) + +> 日期:2026-11-03(修订:去掉名称关键字兜底;明确仅字典+Nacos 配置不生效,需最小代码改动) +> 范围:财务应付单(A102-2)、财务应收单(A302-2)、付款申请(A103-1)分录拆分口径(三方共用 `resolveEntryGroups`,一处改动三方生效) +> 关联文档:`2026-10-13-金蝶应付应收收票单明细分录拆分与物料映射逻辑梳理.md` + +--- + +## 1. 背景与现状问题 + +当前分录拆分维度为 **货物 → 费用大类(装卸/运输)→ 税率**,费用大类只有二分类: +- 装卸大类:费用项档案分类编码 = A10 +- 运输大类:**其余全部**(含仓储等) + +| # | 场景 | 现状行为 | 问题 | +| --- | --- | --- | --- | +| 1 | 结算明细行的仓储费用项(如「仓储堆存保管费」,档案分类 A02) | 落入「运输费」组,映射运输物料(XDWL11010001 等),数量取运单运输量 | 物料错误、数量口径错误 | +| 2 | 结算合计手动添加的仓储费用 | 非运费类统一映射装卸物料 XDWL11002004 | 物料错误 | + +数据事实(本地库 transport):业务字典 `fee_category` 无「仓储」编码(现有 A01/A02/A03/A04/A09/A10);档案「仓储堆存保管费」挂在 A02。 + +--- + +## 2. 为什么仅加字典 + Nacos 配置不生效(结论先行) + +代码里仓储分支不存在,新增的字典编码与配置项没有消费方: + +| # | 证据(`KingdeeApBillSyncSupport` / `BfiProperties`) | 后果 | +| --- | --- | --- | +| 1 | `BfiProperties.Material` 只有 `domestic`/`international`/`loading` 三个属性(BfiProperties.java L211-233),**无 `warehouse` 字段**;Spring 对未知配置项静默忽略 | Nacos 加 `material.warehouse` 为死配置,不报错也不生效 | +| 2 | `splitFeeItems` 大类判定仅 `isLoadingCategory`(A10)二分类(L682-712),仓储费用项仍落入运输费组 | 明细行仓储费照旧映射运输物料、数量取运输量 | +| 3 | `resolveMaterialNumber` 仅 装卸/国际/国内 三分支(L573-599);手动合计行非运费类固定走 `loading`(L546-547) | 手动仓储费照旧映射装卸物料 XDWL11002004 | +| 4 | 字典编码 A01/A10 为硬编码常量(L104-106),档案里的 A05 无代码比较它 | 字典加了也没有判定逻辑消费 | + +因此:**字典 + Nacos 是必要的数据/配置部分,但必须配合最小代码改动**(约 6 处、20 行以内,见第 5 节)。 + +--- + +## 3. 需求口径 + +1. **仓储大类费用**:独立成组生成一行明细;同大类税率不同按税率拆多行 +2. **结算合计手动添加费用**(manual_flag=1):每行单独一条分录(现状保留,仅物料映射修正) +3. **一车多货**:按货物拆分多行(现状保留,仓储同样适用) +4. **仓储物料编码**:`XDWL11002001`(Nacos `thirdparty.bfi.material.warehouse` 可覆盖) + +--- + +## 4. 目标方案 + +### 4.1 大类判定(无关键字兜底,纯数据驱动) + +- 费用项档案 `blade_fee_item.fee_category = A05` → 仓储大类(新增字典) +- **不做名称关键字兜底**:档案未配 A05 的费用项照旧走现有分类(运输/装卸),分类准确性由档案维护负责 +- 代码侧仅新增常量 `FEE_CATEGORY_WAREHOUSE = "A05"` 与档案编码比较 + +### 4.2 拆分与合并(`splitFeeItems`) + +- 分组键 `大类@税率` 扩为三态:`运输费@税率` / `仓储@税率` / `装卸@税率` +- 税率来源不变(合同计费方案),未配置归该大类无税率组;残差仍并入运输费无税率组 +- 同货物行内同大类同税率合并一行、不同税率拆行 —— 满足需求 1 +- 分录标识:`单据号-货物序号-仓储[-税率]` +- `FeeLineSplit.boolean loading` 升级为三态枚举 `FeeCategory{FREIGHT, WAREHOUSE, LOADING}`,保留 `isLoading()` 便捷方法 + +### 4.3 分录字段口径 + +| 分录字段 | 运输大类行 | 仓储大类行(新增) | 装卸大类行 | 结算合计手动费用行 | +| --- | --- | --- | --- | --- | +| 物料编码 | 国际/国内按运输方式映射 | **`material.warehouse`(XDWL11002001)** | `material.loading` | 运费类→同运输行;仓储类(档案A05)→`material.warehouse`;其余→`material.loading` | +| 数量 | 运单运输量(兜底1) | **1**(同装卸) | 1 | 1 | +| 运输货物/车号 | 货物名/车号 | 货物名/车号 | 同左 | 不传 | +| 不含税金额 | 仅运输类首行携带 | 不传(同装卸) | 不传 | 不传 | +| 分录标识 | 单据号-货物序号-运输费[-税率] | 单据号-货物序号-仓储[-税率] | 单据号-货物序号-装卸[-税率] | 结算单号-费用类型[-费用项] | + +收票单(A303-2)按票行生成、不经 `splitFeeItems`,不受影响。 + +--- + +## 5. 改动清单 + +| # | 类型 | 位置 | 内容 | +| --- | --- | --- | --- | +| 1 | SQL | `doc/sql/transport/lqj/temp/`(可重复执行) | 字典 `fee_category` 新增 A05 仓储(见第 6 节) | +| 2 | SQL | 同上 | 档案修正:仓储类费用项 A02→A05(无兜底后此步为分类生效前提) | +| 3 | Nacos | `doc/nacos/blade-dev.yaml`、`doc/nacos/third-party-api.yaml` | `material.warehouse: XDWL11002001` | +| 4 | 代码 | `BfiProperties.Material` | +1 字段 `warehouse = "XDWL11002001"` | +| 5 | 代码 | `KingdeeApBillSyncSupport` | +常量 `FEE_CATEGORY_WAREHOUSE="A05"`;`splitFeeItems`/`splitGroupKey` 三分类;`appendCargoLines` 仓储行物料取 `warehouse`、数量 1;`appendSummaryEntry` 仓储判定映射仓储物料;`resolveMaterialNumber` +仓储分支(参数 boolean→大类枚举) | +| 6 | 代码 | `KingdeeEntrySplitTest` | 追加仓储用例(第 7 节) | + +> 第 4、5 项为代码不可省的最小集;缺任一项则 XDWL11002001 永远不会被推送(见第 2 节)。 + +--- + +## 6. 数据脚本(可重复执行) + +```sql +-- 1. 业务字典 fee_category 新增「仓储 A05」 +INSERT INTO blade_dict_biz (id, tenant_id, parent_id, code, dict_key, dict_value, sort, remark, is_sealed, status, is_deleted) +SELECT 2084286398513100005, '000000', -1, 'fee_category', 'A05', '仓储', 1, '金蝶分录仓储大类', 0, 1, 0 +FROM DUAL +WHERE NOT EXISTS (SELECT 1 FROM (SELECT 1 FROM blade_dict_biz WHERE code = 'fee_category' AND dict_key = 'A05' AND is_deleted = 0) t); + +-- 2. 费用项档案:仓储类费用项归入 A05(无兜底方案下,分类生效的唯一数据来源) +UPDATE blade_fee_item +SET fee_category = 'A05' +WHERE is_deleted = 0 + AND fee_category <> 'A05' + AND (name LIKE '%仓储%' OR name LIKE '%堆存%' OR name LIKE '%保管%'); +``` + +> 字典 id 为手工雪花段,落地前校验不冲突;`blade_fee_item` 在 system 与 transport 两库均有,按实际部署库执行。 + +--- + +## 7. 测试用例(`KingdeeEntrySplitTest` 追加) + +| 用例 | 输入要点 | 断言 | +| --- | --- | --- | +| 仓储同税率合并 | 同货物行:档案 A05 的仓储费 300 + 堆存费 200,税率 6% | 1 行仓储组金额 500 | +| 仓储不同税率拆行 | 仓储费 300@6% + 保管费 200@9%(均档案 A05) | 2 行,后缀 -6 / -9 | +| 仓储不混入运输组 | 运输费 1000@9% + 仓储费(档案A05) 500@6% | 2 行:运输组 1000、仓储组 500 | +| 档案未配 A05 不兜底 | 「仓储服务费」档案 A02 | 归运输组(无兜底,纯档案驱动) | +| 残差勾稽不变 | 运输费 2000@9% + 仓储费 500,结算金额 2520 | 残差 20 仍入运输无税率组 | + +--- + +## 8. 待确认项 + +1. **仓储字典编码**:建议 A05(A05~A08 空缺),业务确认后定稿 +2. **仓储行数量**:默认固定 1(同装卸);如需按吨/方计量另行明确 +3. **档案批量 UPDATE**:按名称关键字一次性归仓,执行前需核对命中清单(含「保管」字样但非仓储的费用项需剔除) diff --git a/doc/nacos/blade-dev.yaml b/doc/nacos/blade-dev.yaml index dfdec81f..1be026b9 100644 --- a/doc/nacos/blade-dev.yaml +++ b/doc/nacos/blade-dev.yaml @@ -156,6 +156,9 @@ thirdParty: username: ${BFI_USERNAME:XDWL-03024} accountId: ${BFI_ACCOUNT_ID:2454399096727798784} language: ${BFI_LANGUAGE:zh_CN} + # Feign 超时(秒):金蝶保存类接口偶发超 60 秒,超时后远端可能已生成单据,重推会自动勾稽防重 + connectTimeoutSeconds: ${BFI_CONNECT_TIMEOUT_SECONDS:10} + readTimeoutSeconds: ${BFI_READ_TIMEOUT_SECONDS:120} # 数据来源系统编码(推送金蝶公共参数:客商、付款申请等统一使用),默认 WLTMS(运输TMS) srcSys: ${BFI_SRC_SYS:WLTMS} @@ -166,7 +169,7 @@ thirdParty: # 应付/应收/收票单分录物料映射(A102-2/A302-2/A303-2 共用,未配置使用代码默认值): # 运输类按项目业务模式映射——国内运输按运单运输方式映射、国际运输映射国际货代服务; - # 装卸大类及手动添加的非运费杂费映射装卸物料 + # 装卸大类及手动添加的非运费杂费映射装卸物料;仓储大类(档案编码 A05)及手动添加仓储费用映射仓储物料 material: # 国内运输:运单运输方式 -> 金蝶物料编码(公路运输/铁路运输/水路运输/航空运输四种) domestic: @@ -178,6 +181,8 @@ thirdParty: international: ${BFI_MATERIAL_INTERNATIONAL:Item-00001081} # 装卸大类、手动添加非运费杂费物料编码(装卸) loading: ${BFI_MATERIAL_LOADING:XDWL11002004} + # 仓储大类、手动添加仓储费用物料编码(仓储) + warehouse: ${BFI_MATERIAL_WAREHOUSE:XDWL11002001} # 付款申请推送配置(A103-3 付款申请保存) payApply: @@ -326,6 +331,8 @@ thirdParty: require-contract-synced: ${HCS_CREDIT_REQUIRE_CONTRACT_SYNCED:true} # 结算付款特批(C106-3/4) spec-pay: + # 付款特批走合同合规开关(false 时不再发起 C106-3/4、不挂起"特批审核"、不拦金蝶推送;提交时特批附件必传逻辑保留) + enabled: ${HCS_SPEC_PAY_ENABLED:false} # 单据状态(示例 10) doc-status: ${HCS_SPEC_PAY_DOC_STATUS:10} # 付款用途/性质/类型字典值(§9-U3 待联调后配置) diff --git a/doc/nacos/blade-prod.yaml b/doc/nacos/blade-prod.yaml index 9eae1638..60322695 100644 --- a/doc/nacos/blade-prod.yaml +++ b/doc/nacos/blade-prod.yaml @@ -122,6 +122,9 @@ thirdParty: username: ${BFI_USERNAME:} accountId: ${BFI_ACCOUNT_ID:} language: ${BFI_LANGUAGE:zh_CN} + # Feign 超时(秒):金蝶保存类接口偶发超 60 秒,超时后远端可能已生成单据,重推会自动勾稽防重 + connectTimeoutSeconds: ${BFI_CONNECT_TIMEOUT_SECONDS:10} + readTimeoutSeconds: ${BFI_READ_TIMEOUT_SECONDS:120} # 数据来源系统编码(推送金蝶公共参数:客商、付款申请等统一使用),默认 WLTMS(运输TMS) srcSys: ${BFI_SRC_SYS:WLTMS} # 金蝶黑名单查询(A801-5 客商提审拦截):禁止交易类(A)始终拦截; diff --git a/doc/nacos/third-party-api.yaml b/doc/nacos/third-party-api.yaml index 10b832e0..864baf09 100644 --- a/doc/nacos/third-party-api.yaml +++ b/doc/nacos/third-party-api.yaml @@ -52,6 +52,9 @@ thirdParty: accountId: ${BFI_ACCOUNT_ID:} # 语言字串,默认 zh_CN language: ${BFI_LANGUAGE:zh_CN} + # Feign 超时(秒):金蝶保存类接口偶发超 60 秒,超时后远端可能已生成单据,重推会自动勾稽防重 + connectTimeoutSeconds: ${BFI_CONNECT_TIMEOUT_SECONDS:10} + readTimeoutSeconds: ${BFI_READ_TIMEOUT_SECONDS:120} # BFI 客户端本地 Mock 开关(联调自测用,生产必须 false): # true 时 IBfiClient 有桩用桩({mockDir}/{接口方法名}.json)、无桩透传真实 Feign 接口,调用侧零改动 @@ -67,7 +70,7 @@ thirdParty: blockRiskWarning: ${BFI_BLACKLIST_BLOCK_RISK_WARNING:false} # 应付/应收/收票单分录物料映射(A102-2/A302-2/A303-2 共用,未配置使用代码默认值): # 运输类按项目业务模式映射——国内运输按运单运输方式映射、国际运输映射国际货代服务; - # 装卸大类及手动添加的非运费杂费映射装卸物料 + # 装卸大类及手动添加的非运费杂费映射装卸物料;仓储大类(档案编码 A05)及手动添加仓储费用映射仓储物料 # 注意:Map 的中文 key 必须用 ["中文"] 方括号写法,否则 Spring Boot 绑定失败、服务启动报错 material: # 国内运输:运单运输方式 -> 金蝶物料编码(公路运输/铁路运输/水路运输/航空运输四种) @@ -81,6 +84,8 @@ thirdParty: international: ${BFI_MATERIAL_INTERNATIONAL:Item-00001081} # 装卸大类、手动添加非运费杂费物料编码(装卸) loading: ${BFI_MATERIAL_LOADING:XDWL11002004} + # 仓储大类、手动添加仓储费用物料编码(仓储) + warehouse: ${BFI_MATERIAL_WAREHOUSE:XDWL11002001} # 付款申请推送配置(A103-3 付款申请保存) payApply: # 单据类型编码:ap_payapply_BT_S 采购付款申请、ap_payapply_oth_BT_S 其他付款申请 @@ -109,6 +114,9 @@ thirdParty: syncEnabled: ${BFI_RECEIVED_INVOICE_RESULT_SYNC_ENABLED:false} # 调度 cron,默认每10分钟(:08 起步,错峰避让其他金蝶任务) cron: ${BFI_RECEIVED_INVOICE_RESULT_CRON:0 8/10 * * * ?} + # A302-6 指定应付自动补偿开关:pushed 且已认领的收票单自动重发指定应付; + # 默认关闭(安全兜底),不自动推送指定应付,由人工入口(发票登记推送/认领发票重推按钮)触发 + assignCompensationEnabled: ${BFI_RECEIVED_INVOICE_RESULT_ASSIGN_COMPENSATION_ENABLED:false} # 收款认领推送金蝶定时任务(A301-3 收款认领结果推送) receiptClaimPush: # 推送任务开关,默认关闭(联调稳定后开启) @@ -269,6 +277,8 @@ thirdParty: require-contract-synced: ${HCS_CREDIT_REQUIRE_CONTRACT_SYNCED:true} # 结算付款特批(C106-3/4) spec-pay: + # 付款特批走合同合规开关(false 时不再发起 C106-3/4、不挂起"特批审核"、不拦金蝶推送;提交时特批附件必传逻辑保留) + enabled: ${HCS_SPEC_PAY_ENABLED:false} # 单据状态(示例 10) doc-status: ${HCS_SPEC_PAY_DOC_STATUS:10} # 付款用途/性质/类型字典值(§9-U3 待联调后配置) diff --git a/doc/sql/transport/lqj/20261102/07_blade_invoice_receipt_supplier_code_20261104.sql b/doc/sql/transport/lqj/20261102/07_blade_invoice_receipt_supplier_code_20261104.sql new file mode 100644 index 00000000..5f0fc641 --- /dev/null +++ b/doc/sql/transport/lqj/20261102/07_blade_invoice_receipt_supplier_code_20261104.sql @@ -0,0 +1,12 @@ +-- 发票收票单新增往来户供应商编码: +-- blade_invoice_receipt.supplier_code:金蝶 A901-1 供应商查询返回 number,发票销售方往来户编码, +-- 保存时尽力回填、推送前强校验。 +-- 幂等可重复执行:列存在才跳过。 +SET @ddl = (SELECT IF( + (SELECT COUNT(*) FROM information_schema.COLUMNS + WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'blade_invoice_receipt' AND COLUMN_NAME = 'supplier_code') > 0, + 'SELECT 1', + 'ALTER TABLE `blade_invoice_receipt` ADD COLUMN `supplier_code` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT ''往来户金蝶供应商代码(发票销售方,A901-1,保存时尽力回填、推送前强校验)'' AFTER `payee_name`')); +PREPARE stmt FROM @ddl; +EXECUTE stmt; +DEALLOCATE PREPARE stmt; diff --git a/doc/sql/transport/lqj/20261102/08_blade_payment_application_record_kingdee_bill_id_20261104.sql b/doc/sql/transport/lqj/20261102/08_blade_payment_application_record_kingdee_bill_id_20261104.sql new file mode 100644 index 00000000..dcb44d27 --- /dev/null +++ b/doc/sql/transport/lqj/20261102/08_blade_payment_application_record_kingdee_bill_id_20261104.sql @@ -0,0 +1,12 @@ +-- 付款申请操作记录新增金蝶付款处理单id: +-- blade_payment_application_record.kingdee_bill_id:A103-4 回写的星瀚付款处理单内部主键(cas_paybill.id), +-- 应付单 preentry 预付抵扣取数用。 +-- 幂等可重复执行:列存在才跳过。 +SET @ddl = (SELECT IF( + (SELECT COUNT(*) FROM information_schema.COLUMNS + WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'blade_payment_application_record' AND COLUMN_NAME = 'kingdee_bill_id') > 0, + 'SELECT 1', + 'ALTER TABLE `blade_payment_application_record` ADD COLUMN `kingdee_bill_id` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT ''金蝶付款处理单id(cas_paybill.id,A103-4回写,应付单preentry预付抵扣取数用)'' AFTER `kingdee_bill_no`')); +PREPARE stmt FROM @ddl; +EXECUTE stmt; +DEALLOCATE PREPARE stmt;