From 7b1f69a410ef00b6f1b8df27b459d690936a610f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=94=A6=E9=BA=9F=20=E7=8E=8B?= Date: Fri, 21 Aug 2026 10:33:29 +0800 Subject: [PATCH] =?UTF-8?q?#EQD-6948=20=E5=9B=BD=E8=81=94=E6=B0=91?= =?UTF-8?q?=E7=94=9F-=E5=AE=9E=E7=8E=B0=E4=BF=9D=E8=AF=81=E9=87=91?= =?UTF-8?q?=E8=A7=84=E5=88=99=EF=BC=882=EF=BC=89=E8=BF=BD=E4=BF=9D?= =?UTF-8?q?=E9=87=91=E9=A2=9D=E7=9A=84=E4=BA=A7=E7=94=9F=E4=B8=8E=E6=94=B6?= =?UTF-8?q?=E7=9B=98=E8=AE=A1=E7=AE=97=20=E6=8E=88=E4=BF=A1=E5=92=8C?= =?UTF-8?q?=E7=8E=B0=E9=87=91tag?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../YLErp.Core/DBModels/ClientCreditInout.cs | 81 ++++++++ .../YLErp.Core/DBModels/Consts/ConsFundTag.cs | 28 +++ Framework/YLErp.Core/DBModels/CreditTable.cs | 13 +- Framework/YLErp.Core/DBModels/SwapPosition.cs | 9 + YLErpDAL/BLL/EodSettlement/RealtimePnlCalc.cs | 44 +++- YLErpDAL/DataBase/YLContext.cs | 1 + .../EodClientBalanceCalcV2.cs | 7 +- .../MarginTemplateV2RateHelper.cs | 4 +- .../SwapModule/ClientCreditInoutService.cs | 123 +++++++++++ .../Modules/SwapModule/Margin/FundTagCalc.cs | 176 ++++++++++++++++ .../Modules/SwapModule/SwapDealService.cs | 51 +++-- .../SwapModule/SwapEodPositionService.cs | 40 +++- .../Modules/SwapModule/SwapFundTagService.cs | 191 ++++++++++++++++++ .../Modules/SwapModule/SwapTradeService.cs | 40 +++- .../TradeModule/ClientCashInCashOutService.cs | 1 + .../DealModule/TradeConfirmService.cs | 59 ++++-- .../DealModule/TradeOpenService.cs | 2 +- YLErpWeb/App_Data/Config/clientEditConfig.js | 7 +- YLErpWeb/Controllers/creditController.cs | 12 ++ YLErpWeb/Views/SwapTrade2/TradeEdit.cshtml | 8 + YLErpWeb/Views/SwapTrade2/TradeView.cshtml | 5 + YLErpWeb/Views/credit/creditEdit.cshtml | 21 +- YLErpWeb/Views/credit/creditView.cshtml | 3 + .../wwwroot/Scripts/app/credit/creditList.js | 10 + .../Scripts/app/swaptrade/swapTradeEdit.js | 1 + .../Scripts/app/swaptrade/swapTradeList.js | 3 +- .../Scripts/app/trade/swapTradeList.js | 3 +- 27 files changed, 874 insertions(+), 69 deletions(-) create mode 100644 Framework/YLErp.Core/DBModels/ClientCreditInout.cs create mode 100644 Framework/YLErp.Core/DBModels/Consts/ConsFundTag.cs create mode 100644 YLErpDAL/Modules/SwapModule/ClientCreditInoutService.cs create mode 100644 YLErpDAL/Modules/SwapModule/Margin/FundTagCalc.cs create mode 100644 YLErpDAL/Modules/SwapModule/SwapFundTagService.cs diff --git a/Framework/YLErp.Core/DBModels/ClientCreditInout.cs b/Framework/YLErp.Core/DBModels/ClientCreditInout.cs new file mode 100644 index 00000000..74fe7917 --- /dev/null +++ b/Framework/YLErp.Core/DBModels/ClientCreditInout.cs @@ -0,0 +1,81 @@ +using System.ComponentModel.DataAnnotations.Schema; + +namespace YLErp.DBModels +{ + /// + /// 客户授信出入表(R4 决策④):记录授信占用/释放/调整的全量变化。 + /// 金额符号与资金流水同号(2026-08-20 业务确认):入金为负、出金为正; + /// 已使用授信 = SUM(amount) 直接求和,可用授信 = 有效授信 − 已使用授信(入金使可用授信上升、出金使其收缩)。 + /// change_type(占用/释放/调整)仅作分类审计;业务字段名一律小写下划线(兼容 PostgreSQL)。 + /// + [Table("client_credit_inout")] + public class client_credit_inout : DBModelBaseV2 + { + /// + /// 变更类型:占用(簿记授信部分) + /// + public const int ChangeTypeOccupy = 1; + /// + /// 变更类型:释放(平仓/到期按原标签返还) + /// + public const int ChangeTypeRelease = 2; + /// + /// 变更类型:人工调整(授信审批额度变化/特批后补记) + /// + public const int ChangeTypeAdjust = 3; + + /// + /// 客户 + /// + public int client_id { get; set; } + + /// + /// 关联预付金腿(swap_position.id,授信占用/释放绑定到那条腿;交易级占用/释放可为空) + /// + public long? position_id { get; set; } + + /// + /// 关联交易(与 position_id 并存,冗余便于按交易查询;授信调整类可为空) + /// + public int? trade_id { get; set; } + + /// + /// 变更类型 1=占用 2=释放 3=人工调整 + /// + public int change_type { get; set; } + + /// + /// 变更金额:与资金流水同号(入金为负、出金为正);入金使可用授信上升、出金使其收缩 + /// + public double amount { get; set; } + + /// + /// 变更后已使用授信(冗余快照,便于核对与报表) + /// + public double used_after { get; set; } + + /// + /// 发生日期 + /// + public DateTime happen_date { get; set; } + + /// + /// 备注(如"簿记拆单授信部分""平仓释放") + /// + public string remark { get; set; } + + // 基类操作人列为 PascalCase(其他存量表共用),本表全列小写下划线——override 并映射小写列名。 + // 新建表约定:所有列一律小写下划线,基类继承列按此方式覆写。 + /// 操作人ID(列 opt_id) + [Column("opt_id")] + public override int OptId { get; set; } + + /// 操作人名称(列 opt_name) + [Column("opt_name")] + public override string OptName { get; set; } + + /// 操作时间(列 opt_time) + [Column("opt_time")] + public override DateTime OptTime { get; set; } + } +} diff --git a/Framework/YLErp.Core/DBModels/Consts/ConsFundTag.cs b/Framework/YLErp.Core/DBModels/Consts/ConsFundTag.cs new file mode 100644 index 00000000..2c1169fb --- /dev/null +++ b/Framework/YLErp.Core/DBModels/Consts/ConsFundTag.cs @@ -0,0 +1,28 @@ +namespace YLErp.DBModels +{ + /// + /// 预付金腿资金标签常量(R4 授信/现金标签体系)。 + /// 授信不进资金:授信占用/释放通过 swap_position.FundTag + 授信出入表(client_credit_inout)体现, + /// 资金流水(ClientCashInCashOut)只记录现金部分,不加标签。 + /// + public static class ConsFundTag + { + /// + /// 授信(该腿预付金占用授信额度,不产生资金流水,变化记入授信出入表) + /// + public const string Credit = "Credit"; + + /// + /// 现金(该腿预付金正常产生资金流水) + /// + public const string Cash = "Cash"; + + /// + /// 存量腿无标签时视同现金 + /// + public static string EffectiveTag(string fundTag) + { + return fundTag == Credit ? Credit : Cash; + } + } +} diff --git a/Framework/YLErp.Core/DBModels/CreditTable.cs b/Framework/YLErp.Core/DBModels/CreditTable.cs index fa899773..ec42e885 100644 --- a/Framework/YLErp.Core/DBModels/CreditTable.cs +++ b/Framework/YLErp.Core/DBModels/CreditTable.cs @@ -26,17 +26,26 @@ namespace YLErp.DBModels public double? Credit { get; set; } /// - /// 原始授信值 + /// 原始授信值(数据库列 original_credit,小写) /// [DisplayName("原始授信值")] + [Column("original_credit")] public double? OriginalCredit { get; set; } /// - /// 最大授信可用比例(0-1,NULL按1) + /// 最大授信可用比例(0-1,NULL按1)(数据库列 max_credit_use_ratio,小写) /// [DisplayName("最大授信可用比例")] + [Column("max_credit_use_ratio")] public double? MaxCreditUseRatio { get; set; } + /// + /// 已使用授信(R4,非持久化:授信出入表 SUM(占用)−SUM(释放)+SUM(调整),列表展示用) + /// + [DisplayName("已使用授信")] + [NotMapped] + public double? UsedCredit { get; set; } + /// /// PFE授信 /// diff --git a/Framework/YLErp.Core/DBModels/SwapPosition.cs b/Framework/YLErp.Core/DBModels/SwapPosition.cs index a5d66d81..b6716c23 100644 --- a/Framework/YLErp.Core/DBModels/SwapPosition.cs +++ b/Framework/YLErp.Core/DBModels/SwapPosition.cs @@ -255,6 +255,15 @@ namespace YLErp.DBModels /// public string category_tag { get; set; } /// + /// 资金标签(R4,单列,数据库列名 fund_tag):Credit=授信 / Cash=现金 / NULL=未选(默认现金,存量视同现金)。 + /// 簿记录入时存用户逐腿选择;确认成交时系统在同列定稿——整腿授信→Credit、整腿现金→Cash、 + /// 授信额度不足跨界时拆单(原腿 Cash + 新拆授信腿 Credit);特批全部定稿为 Cash。 + /// 授信腿不产生资金流水,变化记入授信出入表(client_credit_inout)。 + /// + [DisplayName("资金标签")] + [Column("fund_tag")] + public string FundTag { get; set; } + /// /// 互换观察日集合 /// [NotMapped] diff --git a/YLErpDAL/BLL/EodSettlement/RealtimePnlCalc.cs b/YLErpDAL/BLL/EodSettlement/RealtimePnlCalc.cs index d8347cbc..596641e6 100644 --- a/YLErpDAL/BLL/EodSettlement/RealtimePnlCalc.cs +++ b/YLErpDAL/BLL/EodSettlement/RealtimePnlCalc.cs @@ -1,4 +1,4 @@ -using CsvHelper; +using CsvHelper; using DocumentFormat.OpenXml.Bibliography; using DocumentFormat.OpenXml.Drawing; using DocumentFormat.OpenXml.Drawing.Charts; @@ -2327,12 +2327,44 @@ namespace YLErp.BLL.Eod } if (trade.ExerciseDate.Value.Date >= valuedateBLL.ValueDate.Date) { - var margin = trade.InitialMargin ?? 0; - var endMoney = margin + tradePrice; - AvailableAmount = Math.Max(clientBalance.AvailableAmount + clientBalance.TotalCredit, 0); - if (AvailableAmount < margin + tradePrice) + // R4 簿记资金校验口径(2026-08-21 业务强调"走了资金的就不能占用授信"): + // 按腿的资金走向分流——走现金的部分(未选/选现金腿 + 成交金额)只认现金结存; + // 选授信的腿认 剩余可用授信(有效授信−已使用授信,授信出入表 Σ(amount)), + // 授信不够覆盖的部分回落现金,同样只认现金结存。杜绝"现金腿拿授信垫付校验→现金透支"。 + var marginModes = new[] { (int)InterestModeEnum.追加预付金, (int)InterestModeEnum.初始预付金 }; + var legs = trade.swap_positions?.Where(x => marginModes.Contains(x.InterestMode)).ToList(); + if (legs == null || legs.Count == 0) { - errorMsg = $"当前交易应付预付金:{trade.InitialMargin ?? 0:#,##0.000},应付成交金额:{tradePrice:#,##0.000},总应付资金:{endMoney:#,##0.000}。当前剩余资金:{clientBalance.AmountFund:F3},冻结资金:{clientBalance.AllFreezeBalance():F3},抵押品价值:{clientBalance.GuaranteesTotalAmount:F3},授信额度:{clientBalance.TotalCredit:F3},可用总额度:{AvailableAmount:F3}。不足以支付上述金额,交易费用:{tradePrice ?? 0:F3}"; + legs = db.swap_position.Where(x => x.SwapTradeId == trade.id && x.IsInitial && !x.Invalid + && marginModes.Contains(x.InterestMode)).ToList(); + } + //客户应付为正:收取方向(dir=1)腿 fix 为正应付额;支付方向为客户收钱不参与 + double creditPayable = 0, cashPayable = 0; + foreach (var leg in legs) + { + var payable = Convert.ToDouble(leg.InterestPrincipalFix) * (leg.InterestDirection == 1 ? 1 : -1); + if (payable <= 0) + { + continue; + } + if (leg.FundTag == YLErp.DBModels.ConsFundTag.Credit) + { + creditPayable += payable; + } + else + { + cashPayable += payable; + } + } + var usedCredit = Modules.SwapModule.ClientCreditInoutService.GetUsedCredit(clientId, db); + var creditCap = Math.Max(clientBalance.TotalCredit - usedCredit, 0); + var creditCovered = Math.Min(creditPayable, creditCap); + //授信覆盖不足的回落现金部分 + 走现金部分 + 成交金额,合计必须 ≤ 现金结存 + var cashNeed = tradePrice + cashPayable + (creditPayable - creditCovered); + if (cashNeed > clientBalance.AmountFund) + { + var totalPayable = tradePrice + cashPayable + creditPayable; + errorMsg = $"当前交易应付总额:{totalPayable:#,##0.000}(走现金:{cashPayable + tradePrice:#,##0.000},选授信:{creditPayable:#,##0.000})。当前现金结存:{clientBalance.AmountFund:F3}(走现金部分只认现金结存),授信额度:{clientBalance.TotalCredit:F3},已使用授信:{usedCredit:F3},剩余授信:{creditCap:F3}(授信仅覆盖选授信部分,不足回落现金)。现金不足以覆盖应付的现金部分。"; return false; } } diff --git a/YLErpDAL/DataBase/YLContext.cs b/YLErpDAL/DataBase/YLContext.cs index 78936a0c..427ced1e 100644 --- a/YLErpDAL/DataBase/YLContext.cs +++ b/YLErpDAL/DataBase/YLContext.cs @@ -346,6 +346,7 @@ namespace YLErp.BLL public DbSet swap_flow_merge { get; set; } public DbSet swap_position { get; set; } + public DbSet client_credit_inout { get; set; } public DbSet trade_extend { get; set; } public DbSet trade_initial_margin { get; set; } public DbSet swap_flow_event { get; set; } diff --git a/YLErpDAL/Modules/EodModule/SettlementModule/EodClientBalanceCalcV2.cs b/YLErpDAL/Modules/EodModule/SettlementModule/EodClientBalanceCalcV2.cs index 1ae7941e..0411ad6d 100644 --- a/YLErpDAL/Modules/EodModule/SettlementModule/EodClientBalanceCalcV2.cs +++ b/YLErpDAL/Modules/EodModule/SettlementModule/EodClientBalanceCalcV2.cs @@ -795,7 +795,12 @@ namespace YLErp.Modules.EodModule.SettlementModule var clientSpan = client.ClientSpan; if (clientSpan != null) { - PayableMargin = clientSpan.WorstCastClientPayable ?? 0d; + // 合约维度盯市(MarginWatchRule==0)客户成交时已产生出入金,保证金不再计入占用(PayableMargin),避免双重体现; + // 客户维度盯市(null/1)维持现状,计入占用。存量客户为 null,行为不变。 + if (client.MarginWatchRule != 0) + { + PayableMargin = clientSpan.WorstCastClientPayable ?? 0d; + } DeltaMargin = clientSpan.DeltaMargin ?? 0d; SwapPayableMargin = clientSpan.SwapWorstCastClientPayable ?? 0d; TwoSideMargin = clientSpan.TwoSideMargin ?? 0d; diff --git a/YLErpDAL/Modules/MarginModule/MarginTemplateV2RateHelper.cs b/YLErpDAL/Modules/MarginModule/MarginTemplateV2RateHelper.cs index 6d9cad90..b19c878e 100644 --- a/YLErpDAL/Modules/MarginModule/MarginTemplateV2RateHelper.cs +++ b/YLErpDAL/Modules/MarginModule/MarginTemplateV2RateHelper.cs @@ -150,10 +150,10 @@ namespace YLErp.Modules.MarginModule } /// - /// 标的细分分类判定钩子(转债ETF/科创债ETF/中债指数等)。 + /// 标的细分分类判定钩子(转债ETF/科创债ETF/中债指数等),public 供单元测试回归默认行为。 /// 本期默认返回 null → 走通配行兜底,行为与现状一致;后续需求按业务给的判定规则(代码段/标的维护字段)实现。 /// - private static string GetUnderlyingCategory(string underlyingCode, string underlyingInstrumentType) + public static string GetUnderlyingCategory(string underlyingCode, string underlyingInstrumentType) { return null; } diff --git a/YLErpDAL/Modules/SwapModule/ClientCreditInoutService.cs b/YLErpDAL/Modules/SwapModule/ClientCreditInoutService.cs new file mode 100644 index 00000000..02db251a --- /dev/null +++ b/YLErpDAL/Modules/SwapModule/ClientCreditInoutService.cs @@ -0,0 +1,123 @@ +using YLErp.BLL; +using YLErp.DBModels; + +namespace YLErp.Modules.SwapModule +{ + /// + /// 客户授信出入服务(R4 授信/现金标签体系,决策④)。 + /// 授信出入表的写入口集中在 标签赋值(占用)与 平仓/到期返还(释放)两个链路内,禁止散落调用。 + /// 金额符号口径(2026-08-20 业务确认):与资金流水同号——入金为负、出金为正; + /// 已使用授信 = SUM(amount) 直接求和(入金使已使用授信下降、可用授信=有效授信−已使用授信 上升;出金反之)。 + /// change_type(占用/释放/调整)仅作分类审计,不参与求和方向。 + /// + public class ClientCreditInoutService : YLBaseService + { + public ClientCreditInoutService(OptUserInfo userInfo) : base(userInfo) + { + } + + public ClientCreditInoutService(YLBaseService baseService) : base(baseService) + { + } + + /// + /// 客户已使用授信 = SUM(amount)(金额与资金流水同号:入金负、出金正)。 + /// 可用授信 = 有效授信 − 本值;入金使可用授信增长、出金使其收缩。 + /// 供交易确认校验(RealtimePnlCalc.TradeCanBeConfirm)等静态上下文直接调用。 + /// + public static double GetUsedCredit(int clientId, YLContext db) + { + var records = db.client_credit_inout.Where(x => x.client_id == clientId).ToList(); + return Math.Round(records.Sum(x => x.amount), 2, MidpointRounding.AwayFromZero); + } + + /// + /// 客户当前已使用授信(实例方法,走服务 DbContext) + /// + public double GetUsedCredit(int clientId) + { + return GetUsedCredit(clientId, DbContext); + } + + /// + /// 写入一条授信变化记录(占用/释放/调整统一入口),并冗余记录变更后已使用授信。 + /// amount 带符号:入金(客户付)为负、出金(客户收)为正,与资金流水 Money 同号。 + /// + public client_credit_inout Record(int clientId, long? positionId, int? tradeId, int changeType, + double amount, DateTime happenDate, string remark) + { + if (amount == 0) + { + return null; + } + var amountRounded = Math.Round(amount, 2, MidpointRounding.AwayFromZero); + //变更后已使用授信快照:直接求和口径(符号已含方向) + var usedAfter = Math.Round(GetUsedCredit(clientId) + amountRounded, 2, MidpointRounding.AwayFromZero); + var record = new client_credit_inout + { + client_id = clientId, + position_id = positionId, + trade_id = tradeId, + change_type = changeType, + amount = amountRounded, + used_after = usedAfter, + happen_date = happenDate, + remark = remark, + OptId = UserId, + OptName = UserName, + OptTime = DateTime.Now + }; + DbContext.client_credit_inout.Add(record); + DbContext.SaveChanges(); + return record; + } + + /// + /// 占用:预付金腿标 Credit 的簿记入金(含拆单的授信部分;交易级占用 positionId 为空)。 + /// amount 传负数(入金方向,与资金流水同号)。 + /// + public client_credit_inout Occupy(int clientId, long? positionId, int tradeId, double amount, DateTime happenDate, string remark) + { + return Record(clientId, positionId, tradeId, client_credit_inout.ChangeTypeOccupy, amount, happenDate, remark); + } + + /// + /// 释放:平仓/到期按原标签返还授信部分(按腿的 position_id 匹配原占用记录)。 + /// amount 传正数(出金方向,与资金流水同号)。 + /// + public client_credit_inout Release(int clientId, long? positionId, int tradeId, double amount, DateTime happenDate, string remark) + { + return Record(clientId, positionId, tradeId, client_credit_inout.ChangeTypeRelease, amount, happenDate, remark); + } + + /// + /// 删除某交易的全部授信出入记录:与资金记录同生命周期—— + /// 交易回退到开仓(DeleteTradeCashInCashOut)、修改清除(ClearSwapPositions)、删除交易时同步清理, + /// 重新确认/重补时按最新标签重写,避免占用悬挂。 + /// 注意:阶段四追加保证金占用落地后,此处需区分保留追加部分。 + /// + public void RemoveByTrade(int tradeId) + { + var records = DbContext.client_credit_inout.Where(x => x.trade_id == tradeId).ToList(); + DbContext.client_credit_inout.RemoveRange(records); + } + + /// + /// 按客户批量查询已使用授信(客户列表/详情"已使用授信"展示,数据源即本表)。 + /// + public static Dictionary GetUsedCreditByClients(List clientIds, YLContext db) + { + var result = new Dictionary(); + if (clientIds == null || clientIds.Count == 0) + { + return result; + } + var records = db.client_credit_inout.Where(x => clientIds.Contains(x.client_id)).ToList(); + foreach (var group in records.GroupBy(x => x.client_id)) + { + result[group.Key] = Math.Round(group.Sum(x => x.amount), 2, MidpointRounding.AwayFromZero); + } + return result; + } + } +} diff --git a/YLErpDAL/Modules/SwapModule/Margin/FundTagCalc.cs b/YLErpDAL/Modules/SwapModule/Margin/FundTagCalc.cs new file mode 100644 index 00000000..62a206f5 --- /dev/null +++ b/YLErpDAL/Modules/SwapModule/Margin/FundTagCalc.cs @@ -0,0 +1,176 @@ +using YLErp.DBModels; + +namespace YLErp.Modules.SwapModule.Margin; + +/// +/// 授信/现金资金标签分配的纯函数(从服务层剥离,便于单元测试)。 +/// 对应实现方案阶段二 §2.3 标签赋值四种情形 与 §2.4 平仓按原标签返还。 +/// 资金标签为预付金腿上的单列(swap_position.fund_tag,逐腿录入选择、确认时定稿),非交易级。 +/// +public static class FundTagCalc +{ + /// + /// 按腿分配授信额度(§2.3 四种情形的逐腿版本): + /// 选授信的腿按顺序消耗剩余额度(有效授信−已使用授信),额度耗尽的授信腿拆单(剩余授信+现金差额), + /// 之后的授信腿全额现金;未选/选现金的腿直接现金;特批(情形4)全部现金不占授信; + /// 客户净收取(负金额)的腿不参与授信分配。 + /// legs 需按预期占用顺序传入(HappenDate、id)。 + /// + public static List AllocateByLegPreference(List legs, double creditAvailable, bool ignoreMoneyCheck) + { + var plans = new List(); + var remaining = Math.Round(Math.Max(creditAvailable, 0), 2, MidpointRounding.AwayFromZero); + foreach (var leg in legs) + { + var amount = Math.Round(leg.Amount, 2, MidpointRounding.AwayFromZero); + var credit = 0.0; + if (!ignoreMoneyCheck && leg.PreferCredit && amount > 0) + { + credit = Math.Min(amount, remaining); + remaining = Math.Round(remaining - credit, 2, MidpointRounding.AwayFromZero); + } + plans.Add(new LegFundPlan + { + Leg = leg.Leg, + Amount = amount, + CreditAmount = credit, + CashAmount = Math.Round(amount - credit, 2, MidpointRounding.AwayFromZero) + }); + } + return plans; + } + + /// + /// 平仓/到期返还金额按被平仓腿的 FundTag 分流(§2.4): + /// Credit 腿的返还本金与返息不产生资金流水(本金写授信出入表"释放",出金方向记正数),Cash/无标签(存量)走现金。 + /// 返回 现金部分返还本金、现金部分返息、以及按腿的授信释放明细。 + /// + public static UnwindTagSplit SplitUnwindByTag(List settlements) + { + var result = new UnwindTagSplit(); + foreach (var s in settlements) + { + var margin = Convert.ToDouble(s.MarginAmount); + var rebate = Convert.ToDouble(s.RebateAmount); + if (s.Tag == ConsFundTag.Credit) + { + result.CreditMargin += margin; + result.CreditRebate += rebate; + if (s.MarginAmount != 0) + { + result.Releases.Add(new TagRelease + { + PositionId = s.PositionId, + Amount = Math.Round(margin, 2, MidpointRounding.AwayFromZero) + }); + } + } + else + { + result.CashMargin += margin; + result.CashRebate += rebate; + } + } + result.CashMargin = Math.Round(result.CashMargin, 2, MidpointRounding.AwayFromZero); + result.CreditMargin = Math.Round(result.CreditMargin, 2, MidpointRounding.AwayFromZero); + result.CashRebate = Math.Round(result.CashRebate, 2, MidpointRounding.AwayFromZero); + result.CreditRebate = Math.Round(result.CreditRebate, 2, MidpointRounding.AwayFromZero); + return result; + } +} + +/// +/// 参与标签分配的预付金腿及其簿记金额(客户应付为正)、资金来源选择 +/// +public class LegAmount +{ + public swap_position Leg { get; set; } + public double Amount { get; set; } + /// 腿上是否选了授信(swap_position.fund_tag=='Credit') + public bool PreferCredit { get; set; } +} + +/// +/// 单腿分配结果:CreditAmount 与 CashAmount 皆大于 0 时该腿需拆为两条(拆单) +/// +public class LegFundPlan +{ + public swap_position Leg { get; set; } + /// 腿原簿记金额 + public double Amount { get; set; } + /// 授信部分金额 + public double CreditAmount { get; set; } + /// 现金部分金额 + public double CashAmount { get; set; } + /// 拆单时新拆出的授信腿(占用记录绑定到它) + public swap_position CreditLeg { get; set; } + public bool NeedSplit => CreditAmount > 0 && CashAmount > 0; +} + +/// +/// 平仓/到期结算中的单条保证金腿结算额(客户应收返还为正) +/// +public class MarginLegSettlement +{ + public long PositionId { get; set; } + /// 该腿资金标签(EffectiveTag 后:Credit 或 Cash) + public string Tag { get; set; } + /// 返还本金(swap_flow_event.InterestPrincipal × 方向比) + public decimal MarginAmount { get; set; } + /// 预付金返息(InterestClosePnL) + public decimal RebateAmount { get; set; } +} + +/// +/// 平仓利息事件逐腿构造结算额(纯函数,便于单元测试): +/// 预付金腿(InterestMode=5/6、无标的代码)按标签表查标签(无标签存量按现金), +/// 返还本金=InterestPrincipal×方向比(InterestDirection==1 取 -1,与 MarginCalc.AccumulateSettlement 一致),返息=InterestClosePnL。 +/// +public static class MarginSettlementBuilder +{ + private static readonly int[] MarginModes = { (int)InterestModeEnum.追加预付金, (int)InterestModeEnum.初始预付金 }; + + public static List Build(IDictionary positionTags, IEnumerable interestEvents) + { + var settlements = new List(); + foreach (var x in interestEvents ?? Array.Empty()) + { + if (!string.IsNullOrEmpty(x.UnderlyingCode) || !MarginModes.Contains(x.InterestMode)) + { + continue; + } + var interestRatio = x.InterestDirection == 1 ? -1m : 1m; + settlements.Add(new MarginLegSettlement + { + PositionId = x.PositionId, + Tag = positionTags != null && positionTags.TryGetValue(x.PositionId, out var tag) ? tag : ConsFundTag.Cash, + MarginAmount = x.InterestPrincipal * interestRatio, + RebateAmount = x.InterestClosePnL + }); + } + return settlements; + } +} + +public class TagRelease +{ + public long PositionId { get; set; } + public double Amount { get; set; } +} + +/// +/// 按标签分流后的平仓结算金额 +/// +public class UnwindTagSplit +{ + /// 现金部分返还本金(产生 应付预付金 资金流水) + public double CashMargin { get; set; } + /// 授信部分返还本金(写授信出入表"释放",不产生资金流水) + public double CreditMargin { get; set; } + /// 现金部分预付金返息(产生 预付金返息 资金流水) + public double CashRebate { get; set; } + /// 授信部分预付金返息(授信不进资金,不产生资金流水) + public double CreditRebate { get; set; } + /// 按腿的授信释放明细(position_id 匹配原占用记录) + public List Releases { get; set; } = new List(); +} diff --git a/YLErpDAL/Modules/SwapModule/SwapDealService.cs b/YLErpDAL/Modules/SwapModule/SwapDealService.cs index 229f4c1b..b5c9e6b8 100644 --- a/YLErpDAL/Modules/SwapModule/SwapDealService.cs +++ b/YLErpDAL/Modules/SwapModule/SwapDealService.cs @@ -1095,26 +1095,39 @@ namespace YLErp.Modules.SwapModule } /// /// 写入保证金的资金记录:应付预付金(SwapMarginAmount)和预付金返息(SwapMarginRebatePnl)。 - /// 依赖实例方法 AddClientCash/AddClientCashInCashOut,暂留此处。 - /// - private void RecordMarginCashFlow(trade td, UnwindData unwindData) - => RecordMarginCashFlow(td, unwindData.ValueDate, - unwindData.SwapMarginAmount, unwindData.SwapMarginRebatePnl, - AddClientCashInCashOut); - - /// - /// 写入保证金资金记录的通用重载,接受资金写入委托。 - /// AddClientCash(virtual,测试可stub) 和 AddClientCashInCashOut(非virtual,直接写库) - /// 都可通过此重载统一。 + /// R4 §2.4 平仓/到期按被平仓腿 FundTag 原路返还: + /// Credit 腿的返还本金与返息不产生资金流水(本金写授信出入表"释放",按 position_id 匹配原占用), + /// Cash/无标签(存量)部分正常产生资金流水。 /// private void RecordMarginCashFlow(trade td, DateTime valueDate, + List interestEvents, decimal marginAmount, decimal marginRebate, Func writeCash) { - if (marginAmount != 0) - writeCash(td, Convert.ToDouble(marginAmount), ClientCashInCashOut.系统操作_应付预付金, valueDate); - if (marginRebate != 0) - writeCash(td, Convert.ToDouble(-marginRebate), ClientCashInCashOut.系统操作_预付金返息, valueDate); + var split = interestEvents != null && interestEvents.Any(x => string.IsNullOrEmpty(x.UnderlyingCode)) + ? ReleaseMarginByFundTag(td, valueDate, interestEvents, marginAmount, marginRebate) + //无预付金腿结算事件(如金额手工归一化/无腿场景)——退化原逻辑,全额现金 + : new UnwindTagSplit { CashMargin = Convert.ToDouble(marginAmount), CashRebate = Convert.ToDouble(marginRebate) }; + if (split.CashMargin != 0) + writeCash(td, split.CashMargin, ClientCashInCashOut.系统操作_应付预付金, valueDate); + if (split.CashRebate != 0) + writeCash(td, -split.CashRebate, ClientCashInCashOut.系统操作_预付金返息, valueDate); + } + + /// + /// 按标签分流并写授信释放记录(virtual,测试可 stub 为全现金,见 TestableSwapDealService)。 + /// + protected virtual UnwindTagSplit ReleaseMarginByFundTag(trade td, DateTime valueDate, List interestEvents, + decimal marginAmount, decimal marginRebate) + { + var settlements = new SwapFundTagService(this).GetSettlements(interestEvents); + var split = new SwapFundTagService(this).ReleaseMarginByTag(td, valueDate, settlements); + //结算事件缺失(异常数据)时保底按传入总额走现金,不丢资金记录 + if (settlements.Count == 0) + { + return new UnwindTagSplit { CashMargin = Convert.ToDouble(marginAmount), CashRebate = Convert.ToDouble(marginRebate) }; + } + return split; } /// @@ -1528,7 +1541,7 @@ namespace YLErp.Modules.SwapModule ExecuteInTransaction(() => { int clientCashId = AddClientCash(td, Convert.ToDouble(-unwindData.SwapRealizedPnL), ClientCashInCashOut.系统操作_平仓费, unwindData.ValueDate); - RecordMarginCashFlow(td, unwindData.ValueDate, unwindData.SwapMarginAmount, 0m, AddClientCash); + RecordMarginCashFlow(td, unwindData.ValueDate, unwindData.FlowEvents, unwindData.SwapMarginAmount, 0m, AddClientCash); DealFloatPosition(unwindData); var flowList = new List(unwindData.FlowEvents); var eventId = SaveSwapDeal(unwindData, (int)SwapEventTypeEnum.平仓, clientCashId, "系统操作_平仓"); @@ -1697,7 +1710,7 @@ namespace YLErp.Modules.SwapModule private void DealUnwind(UnwindData unwindData, trade td, string actionMsg = "系统操作_自动平仓") { int clientCashId = AddClientCashInCashOut(td, Convert.ToDouble(-unwindData.SwapRealizedPnL), ClientCashInCashOut.系统操作_平仓费, unwindData.ValueDate); - RecordMarginCashFlow(td, unwindData.ValueDate, unwindData.SwapMarginAmount, 0m, AddClientCashInCashOut); + RecordMarginCashFlow(td, unwindData.ValueDate, unwindData.FlowEvents, unwindData.SwapMarginAmount, 0m, AddClientCashInCashOut); var flowList = new List(unwindData.FlowEvents); var eventId = SaveSwapDeal(unwindData, (int)SwapEventTypeEnum.平仓, clientCashId, actionMsg); if (unwindData.CloseMethod == (int)CloseMethodEnum.全部平仓) @@ -1768,7 +1781,7 @@ namespace YLErp.Modules.SwapModule ExecuteInTransaction(() => { int clientCashId = AddClientCash(td, Convert.ToDouble(-unwindData.SwapRealizedPnL), ClientCashInCashOut.系统操作_互换, unwindData.ValueDate); - RecordMarginCashFlow(td, unwindData.ValueDate, 0m, unwindData.SwapMarginRebatePnl, AddClientCash); + RecordMarginCashFlow(td, unwindData.ValueDate, unwindData.FlowEvents, 0m, unwindData.SwapMarginRebatePnl, AddClientCash); foreach (var item in unwindData.FlowEvents) { item.OptLog = "手工操作"; @@ -1837,7 +1850,7 @@ namespace YLErp.Modules.SwapModule int clientCashId = AddClientCash(td, Convert.ToDouble(-swapEvent.unwindData.SwapRealizedPnL), action, swapEvent.unwindData.ValueDate); if (eventType == (int)SwapEventTypeEnum.平仓) { - RecordMarginCashFlow(td, swapEvent.unwindData.ValueDate, swapEvent.unwindData.SwapMarginAmount, 0m, AddClientCash); + RecordMarginCashFlow(td, swapEvent.unwindData.ValueDate, swapEvent.unwindData.FlowEvents, swapEvent.unwindData.SwapMarginAmount, 0m, AddClientCash); } swapEvent.ClientCashId = clientCashId; td.UnWindDate = swapEvent.unwindData.UnwindDate; diff --git a/YLErpDAL/Modules/SwapModule/SwapEodPositionService.cs b/YLErpDAL/Modules/SwapModule/SwapEodPositionService.cs index f2f06971..472fe3fa 100644 --- a/YLErpDAL/Modules/SwapModule/SwapEodPositionService.cs +++ b/YLErpDAL/Modules/SwapModule/SwapEodPositionService.cs @@ -741,10 +741,16 @@ namespace YLErp.Modules.SwapModule } // 预付金腿:单独插入一条资金记录(系统操作_预付金返息) + // R4 §2.4:返息按腿 FundTag 分流——授信部分不进资金(授信不产生流水), + // 只对现金部分(含无标签存量)产生返息资金记录 if (unwindData.SwapMarginRebatePnl != 0) { - clientCashId = AddClientCashInCashOut(td, Convert.ToDouble(-unwindData.SwapMarginRebatePnl), ClientCashInCashOut.系统操作_预付金返息, unwindData.ValueDate); - clientCashIds.Add(clientCashId); + var cashRebate = GetAutoSwapCashRebate(td, flowEvents, unwindData.SwapMarginRebatePnl); + if (cashRebate != 0) + { + clientCashId = AddClientCashInCashOut(td, Convert.ToDouble(-cashRebate), ClientCashInCashOut.系统操作_预付金返息, unwindData.ValueDate); + clientCashIds.Add(clientCashId); + } } unwindData.SwapCloseAmount = unwindData.SwapRealizedPnL;//需要算上预付金利息 和 分红; 只是不算预付金返还 // 分红:使用派息支付日偏移记录资金记录 @@ -784,6 +790,36 @@ namespace YLErp.Modules.SwapModule } return swapEvent.id; } + + /// + /// R4 §2.4:自动互换预付金返息按腿 FundTag 分流,返回现金部分返息。 + /// 授信腿返息不进资金(授信不产生流水);无标签存量/无预付金腿事件时全额现金。 + /// virtual 供纯内存测试 stub 为全额现金(见 TestableSwapEodPositionService)。 + /// + protected virtual decimal GetAutoSwapCashRebate(trade td, List flowEvents, decimal totalRebate) + { + if (flowEvents == null || flowEvents.Count == 0) + { + return totalRebate; + } + var premiumModes = MarginModes.ForLinq; + var legs = flowEvents.Where(x => string.IsNullOrEmpty(x.UnderlyingCode) && premiumModes.Contains(x.InterestMode)).ToList(); + if (legs.Count == 0) + { + return totalRebate; + } + //GetSettlements 与 legs 同谓词同序过滤,settlements[i] 与 legs[i] 一一对应 + var settlements = new SwapFundTagService(this).GetSettlements(legs); + for (var i = 0; i < legs.Count; i++) + { + //对齐 DealAutoInterests 返息符号口径:InterestClosePnL × −ReceivePay(方向); + //自动互换只结返息,保证金本金不在此返还(不写释放记录) + settlements[i].MarginAmount = 0m; + settlements[i].RebateAmount = legs[i].InterestClosePnL * -DirectionRatio.ReceivePay(legs[i].InterestDirection); + } + var split = FundTagCalc.SplitUnwindByTag(settlements); + return Math.Round(totalRebate - Convert.ToDecimal(split.CreditRebate), 2, MidpointRounding.AwayFromZero); + } /// /// 互换更新实时持仓信息 /// diff --git a/YLErpDAL/Modules/SwapModule/SwapFundTagService.cs b/YLErpDAL/Modules/SwapModule/SwapFundTagService.cs new file mode 100644 index 00000000..73f05a1a --- /dev/null +++ b/YLErpDAL/Modules/SwapModule/SwapFundTagService.cs @@ -0,0 +1,191 @@ +using YLErp.BLL; +using YLErp.DBModels; +using YLErp.Modules.SwapModule.Margin; +using YLErp.Modules.TradeModule; + +namespace YLErp.Modules.SwapModule +{ + /// + /// 互换预付金 授信/现金 标签服务(实现方案阶段二 §2.3/§2.4)。 + /// 标签赋值与返还两个写入口集中在本服务,授信出入表(ClientCreditInoutService)的占用/释放由此统一触发。 + /// 口径:授信值取 credit.Credit 合计(已审批+日期有效+含母公司,阶段一已折算),已使用授信取授信出入表; + /// 授信不进资金——授信部分不产生资金流水。 + /// 资金标签是预付金腿上的单列(swap_position.fund_tag,逐腿):录入时存用户选择(授信/现金/未选默认现金),确认成交时系统在同列定稿。 + /// + public class SwapFundTagService : YLBaseService + { + public SwapFundTagService(OptUserInfo userInfo) : base(userInfo) + { + } + + public SwapFundTagService(YLBaseService baseService) : base(baseService) + { + } + + /// + /// 有效授信合计:客户本人+母公司、已审批、日期有效(口径与 RealtimePnlCalc 取授信一致)。 + /// Credit 列在保存时已折算(OriginalCredit×MaxCreditUseRatio),此处直接取用。 + /// + public double GetEffectiveCredit(int clientId, DateTime valueDate) + { + var client = DataCacheProvider.GetClientDataSource().GetData(clientId); + var clientIds = new List { clientId }; + if (client != null && client.ParentId > 0) + { + clientIds.Add(client.ParentId); + } + var credits = DbContext.credit.Where(t => + clientIds.Contains(t.ClientId ?? 0) && t.ProcessStatus == "已审批" + && (!t.CreditDeadLine.HasValue || t.CreditDeadLine >= valueDate) + && (!t.CreditStartDate.HasValue || t.CreditStartDate <= valueDate)).ToList(); + return credits.Sum(t => t.Credit ?? 0); + } + + /// + /// 剩余可用授信 = 有效授信 − 已使用授信。 + /// 已使用授信 = 授信出入表 Σ(amount)(入金负/出金正)——入金使可用授信上升、出金使其收缩(业务口径)。 + /// + public double GetAvailableCredit(int clientId, DateTime valueDate) + { + return GetEffectiveCredit(clientId, valueDate) - ClientCreditInoutService.GetUsedCredit(clientId, DbContext); + } + + /// + /// 簿记确认时对预付金腿定稿资金标签并产生资金记录(§2.3 四种情形,逐腿)。 + /// fund_tag 单列:录入时存用户选择(Credit/Cash/NULL),本方法读取选择后在同列定稿—— + /// 特批全现金;选授信按剩余额度分配(跨界腿拆单为 授信+现金 两条),未选/现金直接现金。 + /// 授信腿只写授信出入表占用(入金方向记负数,绑定腿 position_id,冗余 trade_id),不产生资金流水; + /// 现金腿走 SaveSwapTradeClientCash 幂等 upsert 产生 应付预付金 记录。 + /// marginLegs 需为已过滤(IsDeductPrincipal 等)的预付金腿(InterestMode=5/6)。 + /// + public void ApplyMarginFundTags(trade td, List marginLegs, ClientCashInCashOutService cashService, bool ignoreMoneyCheck) + { + var creditService = new ClientCreditInoutService(this); + //重确认/重补场景自愈:清掉本交易旧占用记录后按腿上当前选择与最新额度重写 + //(阶段四追加保证金占用落地后需区分保留追加部分) + creditService.RemoveByTrade(td.id); + + var valueDate = td.TradeDate ?? DateTime.Now; + var creditAvailable = GetAvailableCredit(td.ClientId, valueDate); + //客户应付为正:资金记录符号口径为 Money<0=客户付钱,即 收取方向(dir=1)腿 fix 为正应付额—— + //正是占用授信的场景;支付方向(dir=2)为客户收钱,不占用授信,直接现金。 + var allocateLegs = marginLegs + .Select(x => new LegAmount + { + Leg = x, + Amount = Convert.ToDouble(x.InterestPrincipalFix * (x.InterestDirection == 1 ? 1 : -1)), + PreferCredit = x.FundTag == ConsFundTag.Credit + }) + .Where(x => x.Amount > 0) + .OrderBy(x => x.Leg.HappenDate ?? DateTime.MaxValue) + .ThenBy(x => x.Leg.id) + .ToList(); + var plans = FundTagCalc.AllocateByLegPreference(allocateLegs, creditAvailable, ignoreMoneyCheck); + + //先落库拆分的新腿(需要 id 才能绑定占用记录) + foreach (var plan in plans.Where(p => p.NeedSplit)) + { + plan.CreditLeg = SplitLeg(td, plan); + } + //标签定稿(覆盖录入选择):拆单的两条腿在 SplitLeg 内已分别标 Cash/Credit; + //整腿授信→Credit、整腿现金/负应付(客户净收取)腿→Cash + foreach (var leg in marginLegs) + { + var plan = plans.FirstOrDefault(p => p.Leg.id == leg.id); + if (plan == null || plan.NeedSplit) + { + if (plan == null) + { + leg.FundTag = ConsFundTag.Cash; + } + continue; + } + leg.FundTag = plan.CreditAmount > 0 ? ConsFundTag.Credit : ConsFundTag.Cash; + } + DbContext.SaveChanges(); + + foreach (var leg in marginLegs) + { + var plan = plans.FirstOrDefault(p => p.Leg.id == leg.id); + var happenDate = leg.HappenDate ?? td.TradeDate ?? DateTime.Now; + if (plan != null && plan.CreditAmount > 0) + { + //整腿授信 或 拆单后的授信部分:不产生资金流水,只写占用(拆单绑新拆出的授信腿)。 + //入金方向记负数(业务口径:出入表金额与资金流水同号,入金负/出金正;入金使可用授信上升) + creditService.Occupy(td.ClientId, plan.CreditLeg?.id ?? leg.id, td.id, -plan.CreditAmount, happenDate, + plan.NeedSplit ? "簿记拆单授信部分" : "簿记授信占用"); + } + //资金记录沿用既有符号口径(客户付钱为负 = -应付额):授信部分不产生流水,现金部分按差额产生 + var recordAmount = plan != null + ? -plan.CashAmount + : Convert.ToDouble(leg.InterestPrincipalFix * (leg.InterestDirection == 1 ? -1 : 1)); + if (recordAmount != 0) + { + cashService.SaveSwapTradeClientCash(td, recordAmount, happenDate, leg.id, ClientCashInCashOut.系统操作_应付预付金); + } + } + } + + /// + /// 拆单:把跨界腿拆为 授信+现金 两条。原腿保留现金部分并标 Cash(资金来源同步改现金,与最终标签一致), + /// 克隆一条授信腿(InterestPrincipalFix 按授信金额折算)标 Credit,返回新腿。 + /// 拆出的腿为普通初始腿,后续编辑/回退/平仓链路按既有腿处理。 + /// + private swap_position SplitLeg(trade td, LegFundPlan plan) + { + var leg = plan.Leg; + //应付额 = fix × (dir==1 ? 1 : -1),反推 fix 用同一比例(±1 自反) + var payableRatio = leg.InterestDirection == 1 ? 1 : -1; + leg.InterestPrincipalFix = Math.Round(Convert.ToDecimal(plan.CashAmount) * payableRatio, ConsGlobal.MoneyRound, MidpointRounding.AwayFromZero); + leg.FundTag = ConsFundTag.Cash; + + var creditLeg = leg.Clone(); + creditLeg.id = 0; + creditLeg.PositionId = 0; + creditLeg.InterestPrincipalFix = Math.Round(Convert.ToDecimal(plan.CreditAmount) * payableRatio, ConsGlobal.MoneyRound, MidpointRounding.AwayFromZero); + creditLeg.FundTag = ConsFundTag.Credit; + creditLeg.OptId = UserId; + creditLeg.OptName = UserName; + creditLeg.OptTime = DateTime.Now; + DbContext.swap_position.Add(creditLeg); + DbContext.SaveChanges(); + creditLeg.PosiNumber = $"{td.TradeNumber}-{creditLeg.id}"; + return creditLeg; + } + + /// + /// 平仓/到期返还按被平仓腿 FundTag 分流(§2.4): + /// Credit 腿返还本金写授信出入表"释放"(按 position_id 匹配原占用)、本金与返息均不产生资金流水; + /// Cash/无标签(存量)部分由调用方按返回的现金金额正常产生返还流水。 + /// settlements 由平仓利息事件(swap_flow_event)逐腿构造。 + /// + public UnwindTagSplit ReleaseMarginByTag(trade td, DateTime valueDate, List settlements) + { + var split = FundTagCalc.SplitUnwindByTag(settlements); + if (split.Releases.Count > 0) + { + var creditService = new ClientCreditInoutService(this); + foreach (var release in split.Releases) + { + creditService.Release(td.ClientId, release.PositionId, td.id, release.Amount, valueDate, "平仓/到期释放"); + } + } + return split; + } + + /// + /// 由平仓利息事件构造逐腿结算额(供 ReleaseMarginByTag 消费): + /// 预付金腿按 PositionId 查腿标签(无标签存量按现金),纯函数构造见 MarginSettlementBuilder.Build。 + /// + public List GetSettlements(IEnumerable interestEvents) + { + var marginModes = new[] { (int)InterestModeEnum.追加预付金, (int)InterestModeEnum.初始预付金 }; + var events = (interestEvents ?? Enumerable.Empty()) + .Where(x => string.IsNullOrEmpty(x.UnderlyingCode) && marginModes.Contains(x.InterestMode)).ToList(); + var positionIds = events.Select(x => x.PositionId).Where(x => x > 0).Distinct().ToList(); + var tags = DbContext.swap_position.Where(p => positionIds.Contains(p.id)) + .ToDictionary(p => p.id, p => ConsFundTag.EffectiveTag(p.FundTag)); + return MarginSettlementBuilder.Build(tags, events); + } + } +} diff --git a/YLErpDAL/Modules/SwapModule/SwapTradeService.cs b/YLErpDAL/Modules/SwapModule/SwapTradeService.cs index 60a031cd..14e01e22 100644 --- a/YLErpDAL/Modules/SwapModule/SwapTradeService.cs +++ b/YLErpDAL/Modules/SwapModule/SwapTradeService.cs @@ -16,6 +16,7 @@ using System.Data; using System.Linq.Expressions; using System.Text; using YLErp.BLL; +using YLErp.BLL.Eod; using YLErp.Configuration; using YLErp.Configuration.Enums; using YLErp.CustomizedBizLogic; @@ -86,8 +87,16 @@ namespace YLErp.Modules.SwapModule { var um = checkUnderlying(req); trade dbTrade = new trade(); - //交易保存处理 + //交易保存处理(PrepareInitialMargin 在此把 trade_Initial_Margin 折算进 req.InitialMargin, + //资金校验须在其后取值,否则互换表单不填平铺 InitialMargin 时校验会按 0 放行) var tradeNumberGenerated = PrepareTrade(req, TradeSourceEnum.系统交易, um); + // R4 簿记资金校验:可用资金(现金结存+授信−已使用授信,口径见 RealtimePnlCalc.TradeCanBeConfirm) + // 需覆盖 应付预付金+成交金额,不足拦截抛错;特批放行发生在确认成交环节(ignoreMoneyCheck)。 + // ExerciseDate 为空(异常数据)时跳过该校验,避免 TradeCanBeConfirm 内部解引用抛错。 + if (req.ExerciseDate.HasValue && !RealtimePnlCalc.TradeCanBeConfirm(req.ClientId, req, out var fundErrorMsg)) + { + throw new ServiceException(fundErrorMsg); + } var trans = DbContext.Database.BeginTransaction(); try @@ -215,6 +224,9 @@ namespace YLErp.Modules.SwapModule var delCashInCashOutArr = DbContext.ClientCashInCashOut.Where(predicate_cashIncashOut); DbContext.ClientCashInCashOut.RemoveRange(delCashInCashOutArr); + // R4:授信出入记录与资金记录同生命周期,随资金记录一并清理(回退到开仓/删除交易), + // 重新确认时按最新标签与额度重写,避免授信占用悬挂 + new ClientCreditInoutService(this).RemoveByTrade(tradeId); } /// /// 单标的初始化实时持仓 @@ -1462,6 +1474,8 @@ namespace YLErp.Modules.SwapModule position.interest_rest_days = swap.interest_rest_days; position.interest_rule = swap.interest_rule; position.category_tag = string.IsNullOrEmpty(swap.category_tag) ? "互换利率" : swap.category_tag; + // R4:资金标签(fund_tag 单列)随录入保存用户逐腿选择,确认成交时系统在同列定稿 + position.FundTag = string.IsNullOrWhiteSpace(swap.FundTag) ? null : swap.FundTag; position.InitYtm = swap.InitYtm; if (position.InitYtm != null && position.InitYtm > 0) { @@ -1496,6 +1510,8 @@ namespace YLErp.Modules.SwapModule var eodSwapPositions = DbContext.eod_swap_position.Where(x => x.SwapTradeId == tradeId); var eodSwaps = DbContext.eod_swap.Where(x => x.SwapTradeId == tradeId); var clientcashinouts = DbContext.ClientCashInCashOut.Where(x => x.TradeId == tradeId && x.Action == ClientCashInCashOut.系统操作_应付预付金); + // R4:修改清除资金记录时同步清理授信出入记录,重确认时按最新标签重写 + new ClientCreditInoutService(this).RemoveByTrade(tradeId); swapFlowEvents.ForEach(x => { x.DataState = (int)SwapFlowDateStateEnum.废弃; @@ -1629,14 +1645,19 @@ namespace YLErp.Modules.SwapModule } /// - /// 重新补录预付金记录 + /// 重新补录预付金记录(回退到开仓后重补,与 SwapTradeConfirm 同口径) /// /// /// /// private void ResetMarginAmount(trade td, DateTime happenDate, List swapPositions) { - new ClientCashInCashOutService(this).SaveSwapTradeClientCash(td, td.TradePrice ?? 0, happenDate, 0); + var cashSvc = new ClientCashInCashOutService(this); + cashSvc.SaveSwapTradeClientCash(td, td.TradePrice ?? 0, happenDate, 0); + // R4:重补同样走标签分配(授信占用/拆单/现金流水); + // 特批标志在回退重补场景不可得,按当前剩余额度重新分配(回退即重新簿记) + var fundTagSvc = new SwapFundTagService(this); + var generateMarginLegs = new List(); foreach (var marginPositions in swapPositions.Where(x => x.HappenDate != null && x.IsInitial && x.InterestMode != (int)InterestModeEnum.追加预付金).GroupBy(g => g.HappenDate)) { var marginHappenDate = marginPositions.Key; @@ -1653,18 +1674,21 @@ namespace YLErp.Modules.SwapModule } if (generateMargin) { - var marginAmount = marginPosition.InterestPrincipalFix * (marginPosition.InterestDirection == 1 ? -1 : 1); - new ClientCashInCashOutService(this).SaveSwapTradeClientCash(td, Convert.ToDouble(marginAmount), marginHappenDate.HasValue ? marginHappenDate.Value : td.TradeDate.Value, marginPosition.id, ClientCashInCashOut.系统操作_应付预付金); + generateMarginLegs.Add(marginPosition); } } } + fundTagSvc.ApplyMarginFundTags(td, generateMarginLegs, cashSvc, false); + //标签定稿(含可能的拆单)后重克隆实时持仓:TradeBack 的克隆先于定稿生成, + //重克隆使实时腿继承定稿标签、新拆出的授信腿也获得克隆(平仓返还分流查的是实时腿标签) + InitialPosition(td); // 合约维度盯市+无预付金腿:重建交易级(positionId=0)初始预付金记录(与 SwapTradeConfirm 一致,回退重补场景)。 - // 有预付金腿的互换由上面 foreach 按腿重建,不在此重复生成。 + // 有预付金腿的互换由上面按腿重建,不在此重复生成。 + // R4:资金来源是预付金腿上的录入项,无腿即无从选择授信,本交易级记录恒为现金。 var resetWatchClient = DataCacheProvider.GetClientDataSource().GetData(td.ClientId); var hasPrepayLeg = swapPositions != null && swapPositions.Any(x => x.InterestMode == (int)InterestModeEnum.初始预付金 || x.InterestMode == (int)InterestModeEnum.追加预付金); if (resetWatchClient != null && resetWatchClient.MarginWatchRule == 0 && !hasPrepayLeg) { - var cashSvc = new ClientCashInCashOutService(this); var initMargin = cashSvc.GetInitialMarginAmount(td); if (initMargin > 0) { @@ -1706,6 +1730,8 @@ namespace YLErp.Modules.SwapModule interest.id = 0; interest.PositionId = item.id; interest.IsInitial = false; + // R4:回到开仓保留 fund_tag(用户录入的选择),重新确认时按当前额度在同列定稿; + // 授信出入记录已随资金记录清理,重确认时重写 td.TradeAmount = td.TradeAmount + Convert.ToDouble(interest.PosiQuantity); DbContext.swap_position.Add(interest); } diff --git a/YLErpDAL/Modules/TradeModule/ClientCashInCashOutService.cs b/YLErpDAL/Modules/TradeModule/ClientCashInCashOutService.cs index 2819dc05..24885bff 100644 --- a/YLErpDAL/Modules/TradeModule/ClientCashInCashOutService.cs +++ b/YLErpDAL/Modules/TradeModule/ClientCashInCashOutService.cs @@ -189,6 +189,7 @@ namespace YLErp.Modules.TradeModule // 追保规则-合约维度盯市:非互换交易(期权等)成交确认时,按计算保证金生成初始预付金资金记录(入金)。 // 客户维度盯市(null/1)不生成出入金,保证金只进 clientbalancedaily.PayableMargin(占用),维持现状。 + // R4:资金来源是互换预付金腿上的录入项,期权等无腿交易无从选择授信,恒为现金(行为不变)。 var marginClient = DataCacheProvider.GetClientDataSource().GetData(newtrade.ClientId); if (marginClient != null && marginClient.MarginWatchRule == 0) { diff --git a/YLErpDAL/Modules/TradeModule/DealModule/TradeConfirmService.cs b/YLErpDAL/Modules/TradeModule/DealModule/TradeConfirmService.cs index a10d96ac..32c1fcd5 100644 --- a/YLErpDAL/Modules/TradeModule/DealModule/TradeConfirmService.cs +++ b/YLErpDAL/Modules/TradeModule/DealModule/TradeConfirmService.cs @@ -280,7 +280,7 @@ namespace YLErp.Modules.TradeModule.DealModule DbContext.processtradelog.Add(p); new SalesCommissionDataService(OptUser).SetCommissionVaild(td.id); EditReportStatus(td.id); - SwapTradeConfirm(td, history,false,optlog:"手工操作"); + SwapTradeConfirm(td, history,false,optlog:"手工操作", ignoreMoneyCheck: ignoreMoneyCheck); } else if (passStatus == -3) { @@ -313,7 +313,7 @@ namespace YLErp.Modules.TradeModule.DealModule DbContext.processtradelog.Add(p); new SalesCommissionDataService(OptUser).SetCommissionVaild(td.id); EditReportStatus(td.id); - SwapTradeConfirm(td, history, false, optlog: "手工操作"); + SwapTradeConfirm(td, history, false, optlog: "手工操作", ignoreMoneyCheck: ignoreMoneyCheck); } new ClientCashInCashOutService(this).SaveClientCashInCashOut(td); @@ -352,7 +352,8 @@ namespace YLErp.Modules.TradeModule.DealModule /// 新版互换确认 /// /// - public void SwapTradeConfirm(trade td,string history,bool addlog,DateTime? openDate=null,string optlog="") + /// 特批(忽略资金检查)推进:预付金腿全部标现金,不占授信(R4 情形4) + public void SwapTradeConfirm(trade td,string history,bool addlog,DateTime? openDate=null,string optlog="", bool ignoreMoneyCheck = false) { if (td.TradeType != "收益互换") { @@ -363,12 +364,9 @@ namespace YLErp.Modules.TradeModule.DealModule var marginModes = new int[]{ (int)InterestModeEnum.追加预付金, (int)InterestModeEnum.初始预付金 }; var positions= DbContext.swap_position.Where(x => x.SwapTradeId == td.id && x.IsInitial && !x.Invalid ).ToList(); td.swap_positions = positions.Where(x =>marginModes.Contains(x.InterestMode)).ToList(); - var posiPositions= positions.Where(x => x.PosiDirection>0).ToList(); + var posiPositions= positions.Where(x=>x.PosiDirection>0).ToList(); SwapTradeService swapTradeService = new SwapTradeService(this); var swapEventService = new SwapEventService(this); - swapTradeService.InitialPosition(td); - swapTradeService.AddPositionEvent(td, optlog); - //new SwapEodPositionService(this).InitSaveEodSwapPosition(td); var happenDate = td.TradeDate.Value; if (td.trade_extend != null && !td.trade_extend.ExtendObj.NeedOpenFee) { @@ -376,26 +374,43 @@ namespace YLErp.Modules.TradeModule.DealModule } var cashService = new ClientCashInCashOutService(this); cashService.SaveSwapTradeClientCash(td, td.TradePrice ?? 0, happenDate,0); - foreach (var marginPositions in td.swap_positions.GroupBy(g=>g.HappenDate)) + // R4 授信/现金标签:预付金腿定稿资金标签(选授信按剩余授信分配,不足跨界腿拆单), + // 授信部分不产生资金流水(只写授信出入表占用),现金部分产生 应付预付金 记录;特批全现金。 + // 必须在 InitialPosition/AddPositionEvent 之前执行:实时持仓克隆与初始事件要继承"定稿后"的标签, + // 拆单新拆出的授信腿也要被克隆、建事件(否则平仓返还分流会查到克隆腿上的旧标签/漏腿)。 + var generateMarginLegs = new List(); + foreach (var marginPosition in td.swap_positions) { - var marginHappenDate = marginPositions.Key; - foreach (var marginPosition in marginPositions) + marginPosition.Obervation = DbContext.trade_obervation.FirstOrDefault(x => x.PositionId == marginPosition.id); + bool generateMargin = true; + if (marginPosition.Obervation != null) { - marginPosition.Obervation = DbContext.trade_obervation.FirstOrDefault(x => x.PositionId == marginPosition.id); - bool generateMargin = true; - if (marginPosition.Obervation != null) + if (!marginPosition.Obervation.IsDeductPrincipal) { - if (!marginPosition.Obervation.IsDeductPrincipal) - { - generateMargin = false; - } - } - if (generateMargin) - { - var marginAmount = marginPosition.InterestPrincipalFix * (marginPosition.InterestDirection == 1 ? -1 : 1); - cashService.SaveSwapTradeClientCash(td,Convert.ToDouble( marginAmount), marginHappenDate.HasValue ? marginHappenDate.Value : td.TradeDate.Value, marginPosition.id, ClientCashInCashOut.系统操作_应付预付金); + generateMargin = false; } } + if (generateMargin) + { + generateMarginLegs.Add(marginPosition); + } + } + new SwapFundTagService(this).ApplyMarginFundTags(td, generateMarginLegs, cashService, ignoreMoneyCheck); + //标签定稿(含拆单)完成后,再克隆实时持仓、生成初始事件——克隆继承定稿标签 + swapTradeService.InitialPosition(td); + swapTradeService.AddPositionEvent(td, optlog); + // 追保规则-合约维度盯市:互换无预付金腿时(td.swap_positions 在上方已过滤为只剩5/6预付金腿,为空即无预付金腿), + // 按计算保证金补一条交易级(positionId=0)初始预付金资金记录(入金),与按腿生成的记录去重。 + // 客户维度盯市(null/1)不生成出入金,维持现状;有预付金腿的互换沿用上方既有记录,不在此重复生成。 + // R4:资金来源是预付金腿上的录入项,无腿即无从选择授信,本交易级记录恒为现金。 + var watchClient = DataCacheProvider.GetClientDataSource().GetData(td.ClientId); + if (watchClient != null && watchClient.MarginWatchRule == 0 && !td.swap_positions.Any()) + { + var initMargin = cashService.GetInitialMarginAmount(td); + if (initMargin > 0) + { + cashService.SaveSwapTradeClientCash(td, initMargin, td.TradeDate.Value, 0, ClientCashInCashOut.系统操作_应付预付金); + } } var swap_event= swapEventService.UpdateSwapEvent(); swapEventService.AddSwapEventDate(openDate.Value, td.id, (int)SwapEventTypeEnum.确认交易, "", 0, true, history); diff --git a/YLErpDAL/Modules/TradeModule/DealModule/TradeOpenService.cs b/YLErpDAL/Modules/TradeModule/DealModule/TradeOpenService.cs index 3755dc1b..ab7654d3 100644 --- a/YLErpDAL/Modules/TradeModule/DealModule/TradeOpenService.cs +++ b/YLErpDAL/Modules/TradeModule/DealModule/TradeOpenService.cs @@ -542,7 +542,7 @@ namespace YLErp.Modules.TradeModule.DealModule { SwapTradeService swapTradeService = new SwapTradeService(this); new SwapEventService(UserInfo).AddSwapEventDate(DateTime.Now.Date, td.id, (int)SwapEventTypeEnum.审批通过, "", 0, true, req.comments); - new TradeConfirmService(UserInfo).SwapTradeConfirm(td, "确认交易",false, optlog: "手工操作"); + new TradeConfirmService(UserInfo).SwapTradeConfirm(td, "确认交易",false, optlog: "手工操作", ignoreMoneyCheck: req.ignoreMoneyCheck); } //组合交易(非互换) if (td.IsGroup == 1) diff --git a/YLErpWeb/App_Data/Config/clientEditConfig.js b/YLErpWeb/App_Data/Config/clientEditConfig.js index cdc0f5d2..c66811da 100644 --- a/YLErpWeb/App_Data/Config/clientEditConfig.js +++ b/YLErpWeb/App_Data/Config/clientEditConfig.js @@ -16,7 +16,7 @@ const editConfig = { name: "Name", label: "客户名称", type: "text", required: true }, { name: "MainProtocolCode", label: "主协议编号", type: "text" }, { name: "CustomerManagerId", label: "对冲交易询价对象", type: "select-m" }, - + { name: "MarginWatchRule", label: "追保规则", required: true, type: "select" }, { type: "new-col" }, { name: "Abbreviation", label: "客户简称", type: "text" }, @@ -62,11 +62,13 @@ const editConfig = IsAssessmentResultChange: [{ text: "否", value: 0 }, { text: "是", value: 1 }], IsEvaluate: [{ text: "否", value: 0 }, { text: "是", value: 1 }], EvaluateOfValidity: [{ text: "----", value: "" }, "3个月", "6个月", "12个月", "长期"], + MarginWatchRule: [{ text: "合约维度盯市", value: 0 }, { text: "客户维度盯市", value: 1 }], }, defaults: { ClientRight: "4", SwapTradeType: 0, - DerivativesInvestmentVarieties: "2" + DerivativesInvestmentVarieties: "2", + AccessRule: "2", MarginOptionType: "0", MarginWatchRule: "1", }, openList: [ { name: 'id', label: '', hidden: true, optionHide: true, sortable: false, align: '', width: '', }, @@ -97,5 +99,6 @@ const editConfig = { name: 'SupProtocolCode', label: '补充协议编号', hidden: false, sortable: false, align: 'left', width: '150' }, { name: 'ProcessStatus', label: '开户状态', hidden: false, sortable: false, align: 'left', width: '90' }, { name: "Manager", label: "管理人名称", hidden: false, sortable: false, align: 'left', width: '100' }, + { name: 'MarginWatchRule', label: '追保规则', hidden: false, sortable: false, align: 'left', width: '90' }, ] } \ No newline at end of file diff --git a/YLErpWeb/Controllers/creditController.cs b/YLErpWeb/Controllers/creditController.cs index 905de565..9a4dbee8 100644 --- a/YLErpWeb/Controllers/creditController.cs +++ b/YLErpWeb/Controllers/creditController.cs @@ -80,6 +80,13 @@ namespace YLErp.Web.Controllers { c.VarietyName = GetWhiteListVarietyNames(c); } + // R4:已使用授信(授信出入表 SUM(占用)−SUM(释放)+SUM(调整),按客户汇总) + var usedCredits = Modules.SwapModule.ClientCreditInoutService.GetUsedCreditByClients( + sList.rows.Where(x => x.ClientId > 0).Select(x => x.ClientId ?? 0).Distinct().ToList(), yldb); + foreach (var c in sList.rows) + { + c.UsedCredit = usedCredits.TryGetValue(c.ClientId ?? 0, out var used) ? used : 0; + } return Json(sList); } @@ -94,6 +101,11 @@ namespace YLErp.Web.Controllers { var intid = DataProtectHelper.DecryptInt(enid); var r = yldb.credit.Find(intid); + // R4:已使用授信(授信出入表) + if (r != null && r.ClientId > 0) + { + r.UsedCredit = Modules.SwapModule.ClientCreditInoutService.GetUsedCredit(r.ClientId ?? 0, yldb); + } return View(r); } diff --git a/YLErpWeb/Views/SwapTrade2/TradeEdit.cshtml b/YLErpWeb/Views/SwapTrade2/TradeEdit.cshtml index ebd545df..9c85106c 100644 --- a/YLErpWeb/Views/SwapTrade2/TradeEdit.cshtml +++ b/YLErpWeb/Views/SwapTrade2/TradeEdit.cshtml @@ -302,6 +302,7 @@ 收支方向 资金类别 + 资金来源 发生日期 金额 币种 @@ -323,6 +324,13 @@ + + + diff --git a/YLErpWeb/Views/SwapTrade2/TradeView.cshtml b/YLErpWeb/Views/SwapTrade2/TradeView.cshtml index 1f3efd57..fc33a462 100644 --- a/YLErpWeb/Views/SwapTrade2/TradeView.cshtml +++ b/YLErpWeb/Views/SwapTrade2/TradeView.cshtml @@ -271,6 +271,7 @@ 收支方向 资金类别 + 资金标签 发生日期 金额 币种 @@ -286,6 +287,10 @@ @((SwapDirectionEnum)item.InterestDirection) @((InterestModeEnum)item.InterestMode) + + @*R4 资金标签(fund_tag 单列):录入时为用户选择,确认成交后为系统定稿(授信/现金)*@ + @(item.FundTag == ConsFundTag.Credit ? "授信" : "现金") + @item.HappenDate.OtcFormatDate() @item.Currency diff --git a/YLErpWeb/Views/credit/creditEdit.cshtml b/YLErpWeb/Views/credit/creditEdit.cshtml index 3c4c8f99..9ff43b13 100644 --- a/YLErpWeb/Views/credit/creditEdit.cshtml +++ b/YLErpWeb/Views/credit/creditEdit.cshtml @@ -136,9 +136,16 @@ function checkSubmitData() { var pass = $('#creditEditForm').valid(); - if (page.UseClientStockEqvNotional) { - if ((!$("#Credit").val() && !$("#StockEqvNotional").val())) { - main.alert("名义本金规模和授信额度应至少填一项"); + if (pass) { + if (page.UseClientStockEqvNotional) { + if ((!$("#Credit").val() && !$("#StockEqvNotional").val())) { + main.alert("名义本金规模和授信额度应至少填一项"); + return false; + } + } + var ratio = $("#MaxCreditUseRatio").val(); + if (ratio && (Number(ratio) < 0 || Number(ratio) > 1)) { + main.alert("最大授信可用比例取值范围为0-1"); return false; } } @@ -346,6 +353,11 @@ $("#MarginLimit").val(moneyStr); } + function moneyOnFocusOriginalCredit() { + var moneyStr = $("#OriginalCredit").val().replace(/,/g, ""); + $("#OriginalCredit").val(moneyStr); + } + //转换为大写金额 function toAmountMoney(n) { @@ -406,6 +418,9 @@ @Html.MyDecimalFor(model => model.Credit, new { onfocus = "moneyOnFocus()", onblur = "this.value=cc(this.value);" }, !pageObj.UseClientStockEqvNotional) + @Html.MyDecimalFor(model => model.OriginalCredit, new { onfocus = "moneyOnFocusOriginalCredit()", onblur = "this.value=cc(this.value);" }, required: false) + @Html.MyDecimalFor(model => model.MaxCreditUseRatio, required: false) + 注:填写原始授信值后,授信额度将按"原始授信值×最大授信可用比例(空白按1)"自动计算. @if (PS.Config.Company == CompanyEnum.中金) { @Html.MyDecimalFor(model => model.PFECredit, new { onkeyup = "changePFECredit()", onfocus = "pfeCreditOnFocus()", onblur = "this.value=cc(this.value);" }, true) diff --git a/YLErpWeb/Views/credit/creditView.cshtml b/YLErpWeb/Views/credit/creditView.cshtml index 8b248d88..d693ec53 100644 --- a/YLErpWeb/Views/credit/creditView.cshtml +++ b/YLErpWeb/Views/credit/creditView.cshtml @@ -78,6 +78,9 @@ @Html.MyDisplayFor(m => m.AuditStockEqvNotional) } @Html.MyDisplayFor(m => m.Credit) + @Html.MyDisplayFor(m => m.OriginalCredit) + @Html.MyDisplayFor(m => m.MaxCreditUseRatio) + @Html.MyDisplayFor(m => m.UsedCredit) if (PS.Config.Company == CompanyEnum.中金) { @Html.MyDisplayFor(m => m.PFECredit) diff --git a/YLErpWeb/wwwroot/Scripts/app/credit/creditList.js b/YLErpWeb/wwwroot/Scripts/app/credit/creditList.js index 48a73bb4..c2bc637c 100644 --- a/YLErpWeb/wwwroot/Scripts/app/credit/creditList.js +++ b/YLErpWeb/wwwroot/Scripts/app/credit/creditList.js @@ -108,6 +108,12 @@ const jqgrid1Mgr = (new function () { } colModelGrid = colModelGrid.concat([{ name: 'Credit', label: '授信额度', index: 'Credit', width: 110, align: 'right', formatter: 'number' + }, { + name: 'OriginalCredit', label: '原始授信值', index: 'OriginalCredit', width: 110, align: 'right', formatter: 'number' + }, { + name: 'MaxCreditUseRatio', label: '最大授信可用比例', index: 'MaxCreditUseRatio', width: 110, align: 'right', formatter: 'number' + }, { + name: 'UsedCredit', label: '已使用授信', index: 'UsedCredit', width: 110, align: 'right', formatter: 'number' }, { name: 'AuditCredit', label: '授信审批规模', index: 'AuditCredit', width: 110, align: 'right', formatter: 'number' }, { @@ -145,6 +151,10 @@ const jqgrid1Mgr = (new function () { name: 'AuditStockEqvNotional', label: '名义本金审批规模', index: 'AuditStockEqvNotional', width: 110, align: 'right', formatter: 'number' }, { name: 'Credit', label: '授信额度', index: 'Credit', width: 110, align: 'right', formatter: 'number' + }, { + name: 'OriginalCredit', label: '原始授信值', index: 'OriginalCredit', width: 110, align: 'right', formatter: 'number' + }, { + name: 'MaxCreditUseRatio', label: '最大授信可用比例', index: 'MaxCreditUseRatio', width: 110, align: 'right', formatter: 'number' }, { name: 'PFECredit', label: 'PFE授信', index: 'PFECredit', width: 110, align: 'right', formatter: 'number' }, { diff --git a/YLErpWeb/wwwroot/Scripts/app/swaptrade/swapTradeEdit.js b/YLErpWeb/wwwroot/Scripts/app/swaptrade/swapTradeEdit.js index d73ea0ab..6a4a81ab 100644 --- a/YLErpWeb/wwwroot/Scripts/app/swaptrade/swapTradeEdit.js +++ b/YLErpWeb/wwwroot/Scripts/app/swaptrade/swapTradeEdit.js @@ -1741,6 +1741,7 @@ const vue = new Vue({ IsAnnualized: true,//是否年化, HappenDate: thisObj.trade.TradeDate,//发生日期, Currency: 'CNY',//币种 + FundTag: '',//资金标签(R4):空=默认现金,可选授信,录入存选择、确认时系统定稿 interest_rest_days: 7,//重置频率 interest_rule: null//利率准则 } diff --git a/YLErpWeb/wwwroot/Scripts/app/swaptrade/swapTradeList.js b/YLErpWeb/wwwroot/Scripts/app/swaptrade/swapTradeList.js index 362ab98a..d5e98dde 100644 --- a/YLErpWeb/wwwroot/Scripts/app/swaptrade/swapTradeList.js +++ b/YLErpWeb/wwwroot/Scripts/app/swaptrade/swapTradeList.js @@ -357,7 +357,8 @@ function confirmAllSelect() { ors.push(or); } var pData = { tradeids: ors.join(",") }; - pData.IsSkipCheck = true; + // R4:不再硬编码 IsSkipCheck=true 静默跳过资金校验——普通确认走校验, + // 资金不足时由 LackOfMoney 弹窗走显式特批(additionalProcessing)链路 if (!main.isEmpty(additionalProcessing)) { pData.additionalProcessing = additionalProcessing; } diff --git a/YLErpWeb/wwwroot/Scripts/app/trade/swapTradeList.js b/YLErpWeb/wwwroot/Scripts/app/trade/swapTradeList.js index c15557e4..2e7987fa 100644 --- a/YLErpWeb/wwwroot/Scripts/app/trade/swapTradeList.js +++ b/YLErpWeb/wwwroot/Scripts/app/trade/swapTradeList.js @@ -1057,7 +1057,8 @@ function confirmAllSelect() { ors.push(or); } var pData = { tradeids: ors.join(",") }; - pData.IsSkipCheck = true; + // R4:不再硬编码 IsSkipCheck=true 静默跳过资金校验——普通确认走校验, + // 资金不足时由 LackOfMoney 弹窗走显式特批(additionalProcessing)链路 if (!main.isEmpty(additionalProcessing)) { pData.additionalProcessing = additionalProcessing; }