diff --git a/UnitTestProject/Modules/TradeModule/DocGenerateModule/GuolianSwapConfirmBookScopeTest.cs b/UnitTestProject/Modules/TradeModule/DocGenerateModule/GuolianSwapConfirmBookScopeTest.cs new file mode 100644 index 00000000..5dcd3937 --- /dev/null +++ b/UnitTestProject/Modules/TradeModule/DocGenerateModule/GuolianSwapConfirmBookScopeTest.cs @@ -0,0 +1,106 @@ +using YLErp.Configuration; +using YLErp.DBModels; +using YLErp.DBModels.Consts; +using YLErp.DBModels.Enums; +using YLErp.Modules.TradeModule.DocGenerateModule; + +namespace YLErp.UnitTestProject.Modules.TradeModule.DocGenerateModule +{ + [TestClass] + public class GuolianSwapConfirmBookScopeTest + { + [TestMethod] + public void IsPreApprovalTrade_AllAllowedStatuses_ReturnsTrue() + { + var statuses = new[] + { + ConsTrade.新增待确认, + ConsTrade.修改待确认, + ConsTrade.审批中 + }; + + foreach (var status in statuses) + { + var trade = CreateTrade(status); + + Assert.IsTrue(GuolianSwapConfirmBookScope.IsPreApprovalTrade(trade, CompanyEnum.国联), status); + } + } + + [TestMethod] + public void IsPreApprovalTrade_NonGuolianSwapOrInvalidTrade_ReturnsFalse() + { + var confirmedSwap = CreateTrade(ConsTrade.确认成交); + var optionTrade = CreateTrade(ConsTrade.审批中, "香草期权"); + var invalidSwap = CreateTrade(ConsTrade.审批中); + invalidSwap.ValidState = ConsGlobal.InValid; + + Assert.IsFalse(GuolianSwapConfirmBookScope.IsPreApprovalTrade(confirmedSwap, CompanyEnum.国联)); + Assert.IsFalse(GuolianSwapConfirmBookScope.IsPreApprovalTrade(optionTrade, CompanyEnum.国联)); + Assert.IsFalse(GuolianSwapConfirmBookScope.IsPreApprovalTrade(CreateTrade(ConsTrade.审批中), CompanyEnum.国泰君安)); + Assert.IsFalse(GuolianSwapConfirmBookScope.IsPreApprovalTrade(invalidSwap, CompanyEnum.国联)); + } + + [TestMethod] + public void OtherActiveReferencePredicate_OnlyMatchesOtherActiveTrade() + { + const string contractCode = "GLMS-TEST-20260728-FICC-04-000001"; + var predicate = GuolianSwapConfirmBookCleanupService + .BuildOtherActiveReferencePredicate(contractCode, 4) + .Compile(); + + Assert.IsFalse(predicate(CreateRelation(4, contractCode, ContractTypeEnum.Trade, true))); + Assert.IsTrue(predicate(CreateRelation(7, contractCode, ContractTypeEnum.Trade, true))); + Assert.IsFalse(predicate(CreateRelation(7, contractCode, ContractTypeEnum.Trade, false))); + Assert.IsFalse(predicate(CreateRelation(7, "OTHER", ContractTypeEnum.Trade, true))); + Assert.IsFalse(predicate(CreateRelation(7, contractCode, ContractTypeEnum.Clearing, true))); + } + + [TestMethod] + public void TradeConfirmationRelationPredicate_MatchesActiveAndInactiveRelations() + { + const string contractCode = "GLMS-TEST-20260728-FICC-04-000001"; + var predicate = GuolianSwapConfirmBookCleanupService + .BuildTradeConfirmationRelationPredicate(4) + .Compile(); + + Assert.IsTrue(predicate(CreateRelation(4, contractCode, ContractTypeEnum.Trade, true))); + Assert.IsTrue(predicate(CreateRelation(4, contractCode, ContractTypeEnum.Trade, false))); + Assert.IsFalse(predicate(CreateRelation(7, contractCode, ContractTypeEnum.Trade, true))); + Assert.IsFalse(predicate(CreateRelation(4, contractCode, ContractTypeEnum.Clearing, true))); + } + + [TestMethod] + public void ActiveOaResultPredicate_OnlyMatchesRejectedTradeActiveResult() + { + var predicate = GuolianSwapConfirmBookCleanupService + .BuildActiveOaResultPredicate(4) + .Compile(); + + Assert.IsTrue(predicate(new trade_contract_oa_result { trade_id = 4, is_valid = true })); + Assert.IsFalse(predicate(new trade_contract_oa_result { trade_id = 4, is_valid = false })); + Assert.IsFalse(predicate(new trade_contract_oa_result { trade_id = 7, is_valid = true })); + } + + private static trade CreateTrade(string status, string tradeType = "收益互换") + { + return new trade + { + TradeType = tradeType, + TradeStatus = status, + ValidState = ConsGlobal.Valid + }; + } + + private static trade_contract_r CreateRelation(int tradeId, string contractCode, string type, bool isValid) + { + return new trade_contract_r + { + TradeId = tradeId, + ContractCode = contractCode, + Type = type, + IsValid = isValid + }; + } + } +} diff --git a/YLErpDAL/Modules/SwapModule/SwapTradeService.cs b/YLErpDAL/Modules/SwapModule/SwapTradeService.cs index 3deb098c..3040f746 100644 --- a/YLErpDAL/Modules/SwapModule/SwapTradeService.cs +++ b/YLErpDAL/Modules/SwapModule/SwapTradeService.cs @@ -657,8 +657,15 @@ namespace YLErp.Modules.SwapModule } if (changeConfirmStatus) { - //删除交易确认书 TODO - new TradeDocumentDataService(this).DeleteBooksAfterEditTrade(dbTrade); + if (GuolianSwapConfirmBookScope.IsPreApprovalTrade(dbTrade)) + { + new GuolianSwapConfirmBookCleanupService(this).InvalidateAfterEdit(dbTrade); + } + else + { + //删除交易确认书 TODO + new TradeDocumentDataService(this).DeleteBooksAfterEditTrade(dbTrade); + } } int eventType = isAddNew ? (int)SwapEventTypeEnum.新增交易 : (int)SwapEventTypeEnum.修改交易; new SwapEventService(UserInfo).AddSwapEventDate(dbTrade.TradeDate.Value, dbTrade.id, eventType, "", 0, true, "", true); diff --git a/YLErpDAL/Modules/TradeModule/DealModule/TradeContractGenerateService.cs b/YLErpDAL/Modules/TradeModule/DealModule/TradeContractGenerateService.cs index 877ef0c3..316633e5 100644 --- a/YLErpDAL/Modules/TradeModule/DealModule/TradeContractGenerateService.cs +++ b/YLErpDAL/Modules/TradeModule/DealModule/TradeContractGenerateService.cs @@ -226,9 +226,10 @@ namespace YLErp.Modules.TradeModule.DealModule return true; } - // 过滤掉待确认和审批中的交易,这些状态不允许生成确认书 + // 仅国联收益互换允许在审批前生成确认书,其他交易保持原状态限制。 var invalidStatusList = new List { ConsTrade.新增待确认, ConsTrade.修改待确认, ConsTrade.审批中 }; - var invalidTrades = trades.Where(t => invalidStatusList.Contains(t.TradeStatus)).ToList(); + var invalidTrades = trades.Where(t => invalidStatusList.Contains(t.TradeStatus) + && !GuolianSwapConfirmBookScope.IsPreApprovalTrade(t)).ToList(); if (invalidTrades.Any()) { error.AddRange(invalidTrades.Select(t => $"交易 {t.TradeNumber} 状态为【{t.TradeStatus}】,不能生成确认书")); @@ -237,6 +238,20 @@ namespace YLErp.Modules.TradeModule.DealModule logger.Info($"[GJGenerateConfirmBook] 过滤掉 {invalidTrades.Count} 笔待确认/审批中交易"); } + var preApprovalTrades = trades.Where(GuolianSwapConfirmBookScope.IsPreApprovalTrade).ToList(); + foreach (var trade in preApprovalTrades) + { + var validationErrors = new GuolianSwapConfirmBookValidationService(this).Validate(trade); + if (!validationErrors.Any()) + { + continue; + } + + error.Add($"交易 {trade.TradeNumber} 无法生成确认书:{string.Join(";", validationErrors)}"); + tradeIds = tradeIds.Where(id => id != trade.id).ToList(); + trades = trades.Where(item => item.id != trade.id).ToList(); + } + if (!trades.Any()) { logger.Info("[GJGenerateConfirmBook] 过滤后无有效交易可生成确认书"); diff --git a/YLErpDAL/Modules/TradeModule/DealModule/TradeOpenService.cs b/YLErpDAL/Modules/TradeModule/DealModule/TradeOpenService.cs index 8bfd988e..0ec40dad 100644 --- a/YLErpDAL/Modules/TradeModule/DealModule/TradeOpenService.cs +++ b/YLErpDAL/Modules/TradeModule/DealModule/TradeOpenService.cs @@ -7,6 +7,7 @@ using YLErp.Model.Enum; using YLErp.Modules.ClientModule; using YLErp.Modules.SwapModule; using YLErp.Modules.TradeDalModule; +using YLErp.Modules.TradeModule.DocGenerateModule; namespace YLErp.Modules.TradeModule.DealModule { @@ -295,6 +296,7 @@ namespace YLErp.Modules.TradeModule.DealModule /// private TradeOpenResult TradeReject(TradeOpenReqModel req, trade td) { + var isGuolianPreApprovalTrade = GuolianSwapConfirmBookScope.IsPreApprovalTrade(td); var oldOrderId = td.ProcessOrderId; req.comments = FormatCommentsWithRole(oldOrderId, req.comments); td.ProcessOrderId = Convert.ToInt32(PStatusEnum.reject); @@ -367,6 +369,11 @@ namespace YLErp.Modules.TradeModule.DealModule var tcDetails = DbContext.trade_cash_detail.Where(O => O.TradeCashId == tcId); DbContext.trade_cash_detail.RemoveRange(tcDetails); + if (isGuolianPreApprovalTrade) + { + new GuolianSwapConfirmBookCleanupService(this).CleanRejectedPreApprovalTrade(td); + } + DbContext.SaveChanges(); return new TradeOpenResult(td) { RetCode = TradeOpenRetCode.Success }; diff --git a/YLErpDAL/Modules/TradeModule/DocGenerateModule/BatchConfirmationGenerateService.cs b/YLErpDAL/Modules/TradeModule/DocGenerateModule/BatchConfirmationGenerateService.cs index db8136e4..4ee21d4c 100644 --- a/YLErpDAL/Modules/TradeModule/DocGenerateModule/BatchConfirmationGenerateService.cs +++ b/YLErpDAL/Modules/TradeModule/DocGenerateModule/BatchConfirmationGenerateService.cs @@ -75,7 +75,7 @@ namespace YLErp.Modules.TradeModule.DocGenerateModule throw new ServiceException("没有已确认的记录信息!"); } var tradeDic = allTrades.Where(O => - statusList.Contains(O.TradeStatus) + (statusList.Contains(O.TradeStatus) || GuolianSwapConfirmBookScope.IsPreApprovalTrade(O)) && O.ClientId != 0 && O.ParentTradeId == 0) .ToDictionary(n => n.id, n => new trade_contract_group { trade = n }); diff --git a/YLErpDAL/Modules/TradeModule/DocGenerateModule/GuolianSwapConfirmBookValidationService.cs b/YLErpDAL/Modules/TradeModule/DocGenerateModule/GuolianSwapConfirmBookValidationService.cs new file mode 100644 index 00000000..c23410a2 --- /dev/null +++ b/YLErpDAL/Modules/TradeModule/DocGenerateModule/GuolianSwapConfirmBookValidationService.cs @@ -0,0 +1,192 @@ +using System.Linq.Expressions; +using YLErp.Configuration; +using YLErp.DBModels; +using YLErp.DBModels.Consts; +using YLErp.DBModels.Enums; +using YLErp.Model; + +namespace YLErp.Modules.TradeModule.DocGenerateModule +{ + /// + /// 国联收益互换审批前生成确认书的范围与资料校验。 + /// + public static class GuolianSwapConfirmBookScope + { + private static readonly HashSet PreApprovalStatuses = new() + { + ConsTrade.新增待确认, + ConsTrade.修改待确认, + ConsTrade.审批中 + }; + + public static bool IsPreApprovalTrade(trade trade) + { + return IsPreApprovalTrade(trade, PS.Config.Company); + } + + public static bool IsPreApprovalTrade(trade trade, CompanyEnum company) + { + return IsGuolianSwap(trade, company) + && trade.ValidState != ConsGlobal.InValid + && IsPreApprovalStatus(trade.TradeStatus); + } + + public static bool IsGuolianSwap(trade trade) + { + return IsGuolianSwap(trade, PS.Config.Company); + } + + public static bool IsGuolianSwap(trade trade, CompanyEnum company) + { + return company == CompanyEnum.国联 && trade.TradeType == "收益互换"; + } + + public static bool IsPreApprovalStatus(string tradeStatus) + { + return PreApprovalStatuses.Contains(tradeStatus); + } + + } + + /// + /// 清理国联收益互换审批前生成的确认书,不复用按合同编号批量清理的通用逻辑。 + /// + public class GuolianSwapConfirmBookCleanupService : YLBaseService + { + public GuolianSwapConfirmBookCleanupService(YLBaseService baseService) : base(baseService) + { + } + + public void InvalidateAfterEdit(trade trade) + { + if (!GuolianSwapConfirmBookScope.IsGuolianSwap(trade) + || !GuolianSwapConfirmBookScope.IsPreApprovalStatus(trade.TradeStatus)) + { + return; + } + + InvalidateRelations(trade.id); + } + + public void CleanRejectedPreApprovalTrade(trade trade) + { + if (!GuolianSwapConfirmBookScope.IsGuolianSwap(trade)) + { + return; + } + + GlobalConfig.SealHandler.RemoveQueue(trade.id); + InvalidateOaResults(trade.id); + var contractCodes = InvalidateRelations(trade.id); + foreach (var contractCode in contractCodes) + { + var hasActiveReference = DbContext.trade_contract_r.Any( + BuildOtherActiveReferencePredicate(contractCode, trade.id)); + if (!hasActiveReference) + { + var documents = DbContext.trade_contract_document.Where(x => x.Code == contractCode + && x.Type == ContractTypeEnum.Trade); + DbContext.trade_contract_document.RemoveRange(documents); + } + } + } + + public static Expression> BuildOtherActiveReferencePredicate( + string contractCode, + int rejectedTradeId) + { + return relation => relation.ContractCode == contractCode + && relation.Type == ContractTypeEnum.Trade + && relation.IsValid + && relation.TradeId != rejectedTradeId; + } + + public static Expression> BuildTradeConfirmationRelationPredicate( + int tradeId) + { + return relation => relation.TradeId == tradeId + && relation.Type == ContractTypeEnum.Trade; + } + + public static Expression> BuildActiveOaResultPredicate( + int tradeId) + { + return result => result.trade_id == tradeId && result.is_valid; + } + + private void InvalidateOaResults(int tradeId) + { + var oaResults = DbContext.tradeContractOaResult + .Where(BuildActiveOaResultPredicate(tradeId)) + .ToList(); + oaResults.ForEach(x => x.is_valid = false); + } + + private List InvalidateRelations(int tradeId) + { + var relations = DbContext.trade_contract_r + .Where(BuildTradeConfirmationRelationPredicate(tradeId)) + .ToList(); + relations.Where(x => x.IsValid).ToList().ForEach(x => x.IsValid = false); + return relations.Select(x => x.ContractCode).Distinct().ToList(); + } + } + + /// + /// 仅校验模板无法正常生成或选择时必需的交易数据。 + /// + public class GuolianSwapConfirmBookValidationService : YLBaseService + { + public GuolianSwapConfirmBookValidationService(YLBaseService baseService) : base(baseService) + { + } + + public List Validate(trade trade) + { + var errors = new List(); + var client = DataCacheProvider.GetClientDataSource().GetData(trade.ClientId); + var underlying = DataCacheProvider.GetUnderlyingDataSource().GetData(trade.UnderlyingCode); + + if (client == null) + { + errors.Add("客户信息不存在"); + return errors; + } + + if (underlying == null) + { + errors.Add("标的信息不存在"); + } + + ValidateInitialPosition(trade, errors); + + return errors; + } + + private void ValidateInitialPosition(trade trade, List errors) + { + var position = DbContext.swap_position.FirstOrDefault(x => x.SwapTradeId == trade.id + && x.IsInitial + && !x.Invalid + && (x.PositionType == (int)PositionTypeFlag.Long || x.PositionType == (int)PositionTypeFlag.Short)); + + if (position == null) + { + errors.Add("缺少初始标的持仓"); + return; + } + + if (position.PosiDirection != (int)SwapDirectionEnum.支付 + && position.PosiDirection != (int)SwapDirectionEnum.收取) + { + errors.Add("初始标的持仓收支方向无效"); + } + + if (string.IsNullOrWhiteSpace(position.UnderlyingCode)) + { + errors.Add("初始标的持仓缺少标的代码"); + } + } + + } +} diff --git a/YLErpWeb/wwwroot/Scripts/app/trade/tradeConfirmBook.js b/YLErpWeb/wwwroot/Scripts/app/trade/tradeConfirmBook.js index 46c6e487..0f7652e6 100644 --- a/YLErpWeb/wwwroot/Scripts/app/trade/tradeConfirmBook.js +++ b/YLErpWeb/wwwroot/Scripts/app/trade/tradeConfirmBook.js @@ -926,9 +926,10 @@ function showToolName(cellValue, options, rowObject) { var html = showDownload(cellValue, options, rowObject); var title = page.IsAutoSealAndUploadFiles && page.IsAutoSealAfterGeneratedBook ? "生成并用印" : "生成确认书"; - // 待确认和审批中的交易不允许生成确认书 + // 仅国联收益互换允许在审批前生成确认书,其他交易保持原限制。 var isUnconfirmed = rowObject.TradeStatus === "新增待确认" || rowObject.TradeStatus === "修改待确认" || rowObject.TradeStatus === "审批中"; - var canGenerate = page.canGenerate == true && !isUnconfirmed ? "" : "disabled"; + var isGuolianSwap = page.isGuoLian === true && rowObject.TradeType === "收益互换"; + var canGenerate = page.canGenerate == true && (!isUnconfirmed || isGuolianSwap) ? "" : "disabled"; var isWait = (rowObject.MetaDic.SealResult == "用印等待中"); html += "".template(rowObject.id, rowObject.MetaDic.HasGeneratedConfirmBook == "True", title, isWait, canGenerate); if (rowObject.MetaDic.ContractDocUrl && page.canSendEmail == true) {