feat(swap): 更新互换估值功能并优化数据模型

- 添加互换估值V1配置常量以支持新版本
- 将PeriodAmount和DividendAmount字段改为可空decimal类型
- 更新字段注释说明预付金利率和利息的计算方式
- 修改净额结算金额定义,排除期初和追加预付金本金
- 在报表服务中添加安全求和操作防止空值异常
- 增加多个汇总字段包括股息、保证金利息等统计
- 重构预付金处理逻辑,改用加权平均计算方式
- 新增加权保证金利率和利息计算辅助方法
- 调整前端表格列顺序,优化交易编号显示位置
- 更新数值格式化器以支持空值处理和精度控制
- 添加文档列顺序恢复功能确保表格一致性
This commit is contained in:
张名锐
2026-07-14 09:46:54 +08:00
parent a7c4344436
commit 4449bd5824
10 changed files with 324 additions and 179 deletions
@@ -28,6 +28,13 @@
/// </summary>
public string MarginDetail { get; set; }
public int ClientId { get; set; }
/// <summary>
/// 每日估值报告页面选择的簿记账户。为空时按客户维度生成全量报告;
/// 有值时仅筛选互换估值页的交易所属账户。
/// </summary>
public int? BookId { get; set; }
public DateTime From { get; set; }
public DateTime To { get; set; }
public double PayableMargin { get; set; }
@@ -4,6 +4,7 @@ using OfficeOpenXml.Style;
using Org.BouncyCastle.Ocsp;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
@@ -14,6 +15,7 @@ using YLErp.BLL;
using YLErp.BLL.EodSettlement;
using YLErp.Configuration;
using YLErp.Core.Helpers;
using YLErp.DBModels;
using YLErp.Enums;
using YLErp.Helpers;
using YLErp.Model;
@@ -60,7 +62,16 @@ namespace YLErp.Modules.ReportModule.SettlementReportModule
#endregion
if (emailData.SendContent.Contains("互换估值"))
{
var eodReq = new ClientSwapPositionRequest { ClientId = emailData.ClientId, ValueDate = emailData.To, ValueDateFrom = emailData.From,page=1, rows=10000,StructureType= "普通债券类收益互换" };
var eodReq = new ClientSwapPositionRequest
{
ClientId = emailData.ClientId,
BookId = emailData.BookId,
ValueDate = emailData.To,
ValueDateFrom = emailData.From,
page = 1,
rows = 10000,
StructureType = "普通债券类收益互换"
};
report.EodSwapPositions = swapEodPositionService.SearchEodPositionList(eodReq).rows.ToList();
}
if (emailData.SendContent.Contains("互换持仓明细"))
@@ -208,7 +219,7 @@ namespace YLErp.Modules.ReportModule.SettlementReportModule
Directory.CreateDirectory(targetPath);
}
var clientName = report.client.Name;
var fileName = report.ReportFrom == DateTime.MinValue ? $"证券_估值表_{report.ReportEnd:yyyyMMdd}_{clientName}" : $"证券_估值表_{report.ReportFrom:yyyyMMdd}_{report.ReportEnd:yyyyMMdd}_{clientName}";
var fileName = $"{clientName}_每日估值报告_{report.ReportEnd:yyyyMMdd}";
var targetFileName = Path.Combine(targetPath, $"{fileName}.xlsx");
var excelDeclareModel = new ExcelDeclareModel()
@@ -238,14 +249,22 @@ namespace YLErp.Modules.ReportModule.SettlementReportModule
{
modelDict.Add("互换估值", new
{
// 明细列表供模板渲染;以下 *Sum 字段用于“互换估值”页签合计行。
EodSwapPositions = report.EodSwapPositions,
// 持仓规模、期间收益和利息/分红类金额合计。
PosiNotionalValueSum= report.EodSwapPositions.Sum(x => x.position.PosiNotionalValue),
PosiQuantitySum= report.EodSwapPositions.Sum(x => x.position.PosiQuantity),
PeriodAmountSum= report.EodSwapPositions.Sum(x => x.PeriodAmount),
PeriodAmountSum= report.EodSwapPositions.Sum(x => x.PeriodAmount) ?? 0m,
DividendAmountSum = report.EodSwapPositions.Sum(x => x.DividendAmount) ?? 0m,
InterestAmountSum= report.EodSwapPositions.Sum(x => x.InterestAmount),
PosiFeePendingSum = report.EodSwapPositions.Sum(x => x.position.PosiFeePending),
PosiProfitSum= report.EodSwapPositions.Sum(x => x.position.PosiProfitSum),
// 保证金相关收益和保证金占用金额合计。
MarginInterestAmountSum = report.EodSwapPositions.Sum(x => x.MarginInterestAmount),
OpenMarginAmountSum = report.EodSwapPositions.Sum(x => x.OpenMarginAmount),
AdditionalMarginAmountSum = report.EodSwapPositions.Sum(x => x.AdditionalMarginAmount),
// 净结算金额为日终估值口径;TRS价值在净结算金额基础上叠加期初/追加保证金。
NetSettmentAmountSum= report.EodSwapPositions.Sum(x => x.NetSettmentAmount),
TrsValueSum = report.EodSwapPositions.Sum(x => x.TrsValue),
});
}
if (report.SwapPositions != null)
@@ -287,7 +306,10 @@ namespace YLErp.Modules.ReportModule.SettlementReportModule
string sourceFileName = Path.Combine(sourcePath, $"结算报告模板.xlsx");
ExcelPackage.LicenseContext = LicenseContext.NonCommercial;
var pdffile = ExcelTemplate.GeneratePDFFromExeclTemplate(sourcePath, sourceFileName, modelDict, targetPath, targetFileName
, shouldDeleteSheet: true, needToPdf: false);
, shouldDeleteSheet: true, needToPdf: false, callback: sheets =>
{
FormatSwapValuationDisplayCells(sheets, report.EodSwapPositions);
});
if (needToPdf)
{
var targetPdfFileName = FileHelper.ReplaceExtension(targetFileName, ".pdf");
@@ -297,6 +319,53 @@ namespace YLErp.Modules.ReportModule.SettlementReportModule
return Path.Combine(targetPath, targetFileName);
}
/// <summary>
/// 部分 Office 版本会将可选小数格式(例如 <c>#,##0.##</c>)错误显示为 <c>20,000.</c>。
/// 互换估值中的这些列是展示字段,不参与 Excel 公式计算,因此在模板替换完成后写为已格式化文本,
/// 既保留去尾零口径,也避免留下孤立的小数点。
/// </summary>
private static void FormatSwapValuationDisplayCells(IEnumerable<ExcelWorksheet> sheets, IEnumerable<EodSwapPositionResponse> positions)
{
var worksheet = sheets.FirstOrDefault(x => x.Name == "互换估值");
var positionList = positions?.ToList() ?? new List<EodSwapPositionResponse>();
if (worksheet == null || !positionList.Any())
{
return;
}
const int dataStartRow = 2;
for (var index = 0; index < positionList.Count; index++)
{
var row = dataStartRow + index;
var item = positionList[index];
SetTrimmedExcelText(worksheet.Cells[row, 8], item.position.PosiNotionalValue, 2);
SetTrimmedExcelText(worksheet.Cells[row, 9], item.position.PosiQuantity, 9);
SetTrimmedExcelText(worksheet.Cells[row, 10], item.PeriodAmount, 2);
SetTrimmedExcelText(worksheet.Cells[row, 11], item.DividendAmount, 2);
SetTrimmedExcelText(worksheet.Cells[row, 13], item.InitYtm, 4, percent: true);
}
var totalRow = dataStartRow + positionList.Count;
SetTrimmedExcelText(worksheet.Cells[totalRow, 8], positionList.Sum(x => x.position.PosiNotionalValue), 2);
SetTrimmedExcelText(worksheet.Cells[totalRow, 10], positionList.Sum(x => x.PeriodAmount) ?? 0m, 2);
SetTrimmedExcelText(worksheet.Cells[totalRow, 11], positionList.Sum(x => x.DividendAmount) ?? 0m, 2);
}
private static void SetTrimmedExcelText(ExcelRange cell, decimal? value, int decimalPlaces, bool percent = false)
{
if (!value.HasValue)
{
cell.Value = null;
return;
}
var displayValue = percent ? value.Value * 100m : value.Value;
var format = "#,##0." + new string('#', decimalPlaces);
var text = displayValue.ToString(format, CultureInfo.InvariantCulture).TrimEnd('.');
cell.Value = percent ? text + "%" : text;
cell.Style.Numberformat.Format = "@";
}
/// <summary>
/// 财务状况
/// </summary>