diff --git a/AsbCloudInfrastructure/Services/ProcessMaps/Files/DrillingProcessMapTemplate.xlsx b/AsbCloudInfrastructure/Services/ProcessMaps/Files/DrillingProcessMapTemplate.xlsx new file mode 100644 index 00000000..be2c62e9 Binary files /dev/null and b/AsbCloudInfrastructure/Services/ProcessMaps/Files/DrillingProcessMapTemplate.xlsx differ diff --git a/AsbCloudInfrastructure/Services/ProcessMaps/WellDrillingProcessMap/WellDrillingProcessMapImportService.cs b/AsbCloudInfrastructure/Services/ProcessMaps/WellDrillingProcessMap/WellDrillingProcessMapImportService.cs new file mode 100644 index 00000000..576e3ba2 --- /dev/null +++ b/AsbCloudInfrastructure/Services/ProcessMaps/WellDrillingProcessMap/WellDrillingProcessMapImportService.cs @@ -0,0 +1,358 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Threading; +using System.Threading.Tasks; +using AsbCloudApp.Data; +using AsbCloudApp.Data.ProcessMaps; +using AsbCloudApp.Exceptions; +using AsbCloudApp.Repositories; +using AsbCloudApp.Services; +using AsbCloudApp.Services.ProcessMaps; +using ClosedXML.Excel; + +namespace AsbCloudInfrastructure.Services.ProcessMaps.WellDrillingProcessMap; + +/* + * password for ProcessMapImportTemplate.xlsx is ASB2020! + */ +public class WellDrillingProcessMapImportService : IProcessMapImportService +{ + private readonly IWellDrillingProcessMapRepository wellDrillingProcessMapRepository; + private readonly ICrudRepository wellSectionTypeRepository; + private readonly IWellService wellService; + + private const string sheetNamePlan = "План"; + + private const int headerRowsCount = 2; + + private const int columnWellSectionType = 1; + private const int columnMode = 2; + private const int columnDepthStart = 3; + private const int columnDepthEnd = 4; + private const int columnPressurePlan = 5; + private const int columnPressureLimitMax = 6; + private const int columnAxialLoadPlan = 7; + private const int columnAxialLoadLimitMax = 8; + private const int columnTopDriveTorquePlan = 9; + private const int columnTopDriveTorqueLimitMax = 10; + private const int columnTopDriveSpeedPlan = 11; + private const int columnTopDriveSpeedLimitMax = 12; + private const int columnFlowPlan = 13; + private const int columnFlowLimitMax = 14; + private const int columnRopPlan = 15; + private const int columnUsageSaub = 16; + private const int columnUsageSpin = 17; + + private WellSectionTypeDto[] sections = null!; + + public WellDrillingProcessMapImportService(IWellDrillingProcessMapRepository wellDrillingProcessMapRepository, + ICrudRepository wellSectionTypeRepository, + IWellService wellService) + { + this.wellDrillingProcessMapRepository = wellDrillingProcessMapRepository; + this.wellSectionTypeRepository = wellSectionTypeRepository; + this.wellService = wellService; + } + + public async Task ImportAsync(int idWell, int idUser, bool deleteProcessMapPlansBeforeImport, Stream stream, + CancellationToken cancellationToken) + { + sections = (await wellSectionTypeRepository.GetAllAsync(cancellationToken)).ToArray(); + + using var workBook = new XLWorkbook(stream); + + var wellDrillingProcessMaps = ParseWorkBook(workBook); + + if (deleteProcessMapPlansBeforeImport) + await wellDrillingProcessMapRepository.RemoveByWellAsync(idWell, cancellationToken); + + foreach (var wellDrillingProcessMap in wellDrillingProcessMaps) + { + wellDrillingProcessMap.IdWell = idWell; + wellDrillingProcessMap.IdUser = idUser; + } + + await wellDrillingProcessMapRepository.InsertRangeAsync(wellDrillingProcessMaps, cancellationToken); + } + + public async Task<(string Name, Stream File)> ExportAsync(int idWell, CancellationToken cancellationToken) + { + var well = await wellService.GetOrDefaultAsync(idWell, cancellationToken) + ?? throw new ArgumentInvalidException(nameof(idWell), $"Скважины с {idWell} не существует"); + + sections = (await wellSectionTypeRepository.GetAllAsync(cancellationToken)).ToArray(); + + var processMapPlans = (await wellDrillingProcessMapRepository.GetByIdWellAsync(idWell, + cancellationToken)).ToArray(); + + var file = await GenerateExcelFileStreamAsync(processMapPlans, cancellationToken); + + var fileName = $"РТК-план бурение по скважине {well.Caption} куст {well.Cluster}.xlsx"; + + return (fileName, file); + } + + public async Task<(string Name, Stream File)> GetExcelTemplateStreamAsync(CancellationToken cancellationToken) + { + var resourceName = Assembly.GetExecutingAssembly() + .GetManifestResourceNames() + .FirstOrDefault(n => n.EndsWith("DrillingProcessMapTemplate.xlsx"))!; + + using var stream = Assembly.GetExecutingAssembly() + .GetManifestResourceStream(resourceName)!; + + var memoryStream = new MemoryStream(); + await stream.CopyToAsync(memoryStream, cancellationToken); + memoryStream.Position = 0; + + var name = "ЕЦП_шаблон_файла_РТК_бурение.xlsx"; + + return (name, memoryStream); + } + + private void AddToWorkbook(XLWorkbook workbook, IEnumerable wellDrillingProcessMaps) + { + var sheet = workbook.Worksheets.FirstOrDefault(ws => ws.Name == sheetNamePlan) + ?? throw new FileFormatException($"Книга excel не содержит листа {sheetNamePlan}."); + + AddToSheet(sheet, wellDrillingProcessMaps.ToArray()); + } + + private void AddToSheet(IXLWorksheet sheet, IList wellDrillingProcessMaps) + { + if (!wellDrillingProcessMaps.Any()) + return; + + for (int i = 0; i < wellDrillingProcessMaps.Count; i++) + { + var row = sheet.Row(1 + i + headerRowsCount); + AddToRow(row, wellDrillingProcessMaps[i]); + } + } + + private void AddToRow(IXLRow row, WellDrillingProcessMapDto wellDrillingProcessMap) + { + row.Cell(columnWellSectionType).Value = sections.First(x => x.Id == wellDrillingProcessMap.IdWellSectionType).Caption; + row.Cell(columnMode).Value = GetModeCaption(wellDrillingProcessMap.IdMode); + row.Cell(columnDepthStart).Value = wellDrillingProcessMap.DepthStart; + row.Cell(columnDepthEnd).Value = wellDrillingProcessMap.DepthEnd; + row.Cell(columnPressurePlan).Value = wellDrillingProcessMap.Pressure.Plan; + row.Cell(columnPressureLimitMax).Value = wellDrillingProcessMap.Pressure.LimitMax; + row.Cell(columnAxialLoadPlan).Value = wellDrillingProcessMap.AxialLoad.Plan; + row.Cell(columnAxialLoadLimitMax).Value = wellDrillingProcessMap.AxialLoad.LimitMax; + row.Cell(columnTopDriveTorquePlan).Value = wellDrillingProcessMap.TopDriveTorque.Plan; + row.Cell(columnTopDriveTorqueLimitMax).Value = wellDrillingProcessMap.TopDriveTorque.LimitMax; + row.Cell(columnTopDriveSpeedPlan).Value = wellDrillingProcessMap.TopDriveSpeed.Plan; + row.Cell(columnTopDriveSpeedLimitMax).Value = wellDrillingProcessMap.TopDriveSpeed.LimitMax; + row.Cell(columnFlowPlan).Value = wellDrillingProcessMap.Flow.Plan; + row.Cell(columnFlowLimitMax).Value = wellDrillingProcessMap.Flow.LimitMax; + row.Cell(columnRopPlan).Value = wellDrillingProcessMap.RopPlan; + row.Cell(columnUsageSaub).Value = wellDrillingProcessMap.UsageSaub; + row.Cell(columnUsageSpin).Value = wellDrillingProcessMap.UsageSpin; + } + + private IEnumerable ParseWorkBook(IXLWorkbook workbook) + { + var sheet = workbook.Worksheets.FirstOrDefault(ws => ws.Name == sheetNamePlan) + ?? throw new FileFormatException($"Книга excel не содержит листа {sheetNamePlan}."); + + return ParseSheet(sheet); + } + + private IEnumerable ParseSheet(IXLWorksheet sheet) + { + const int columnsCount = 17; + + if (sheet.RangeUsed().RangeAddress.LastAddress.ColumnNumber < columnsCount) + throw new FileFormatException($"Лист {sheet.Name} содержит меньшее количество столбцов."); + + var rowsCount = sheet.RowsUsed().Count() - headerRowsCount; + + if (rowsCount <= 0) + return Array.Empty(); + + var processMapPlans = new WellDrillingProcessMapDto[rowsCount]; + + var parseErrors = new List(); + + for (int i = 0; i < processMapPlans.Length; i++) + { + var row = sheet.Row(1 + i + headerRowsCount); + + try + { + processMapPlans[i] = ParseRow(row); + } + catch (FileFormatException ex) + { + parseErrors.Add(ex.Message); + } + } + + if (parseErrors.Any()) + throw new FileFormatException(string.Join("\r\n", parseErrors)); + + return processMapPlans; + } + + private WellDrillingProcessMapDto ParseRow(IXLRow row) + { + var wellSectionTypeCaption = row.Cell(columnWellSectionType).GetCellValue()?.Trim().ToLower(); + var modeName = row.Cell(columnMode).GetCellValue()?.Trim().ToLower(); + var depthStart = row.Cell(columnDepthStart).GetCellValue(); + var depthEnd = row.Cell(columnDepthEnd).GetCellValue(); + var pressurePlan = row.Cell(columnPressurePlan).GetCellValue(); + var pressureLimitMax = row.Cell(columnPressureLimitMax).GetCellValue(); + var axialLoadPlan = row.Cell(columnAxialLoadPlan).GetCellValue(); + var axialLoadLimitMax = row.Cell(columnAxialLoadLimitMax).GetCellValue(); + var topDriveTorquePlan = row.Cell(columnTopDriveTorquePlan).GetCellValue(); + var topDriveTorqueLimitMax = row.Cell(columnTopDriveTorqueLimitMax).GetCellValue(); + var topDriveSpeedPlan = row.Cell(columnTopDriveSpeedPlan).GetCellValue(); + var topDriveSpeedLimitMax = row.Cell(columnTopDriveSpeedLimitMax).GetCellValue(); + var flowPlan = row.Cell(columnFlowPlan).GetCellValue(); + var flowLimitMax = row.Cell(columnFlowLimitMax).GetCellValue(); + var ropPlan = row.Cell(columnRopPlan).GetCellValue(); + var usageSaub = row.Cell(columnUsageSaub).GetCellValue(); + var usageSpin = row.Cell(columnUsageSpin).GetCellValue(); + + var wellSection = sections.FirstOrDefault(s => s.Caption.Trim().ToLower() == wellSectionTypeCaption) + ?? throw new FileFormatException( + $"Лист {row.Worksheet.Name}. В строке {row.RowNumber()} указана некорректная секция"); + + var idMode = GetIdMode(modeName) + ?? throw new FileFormatException( + $"Лист {row.Worksheet.Name}. В строке {row.RowNumber()} указан некорректный режим"); + + if (depthStart is < 0 or > 99999.9) + throw new FileFormatException( + $"Лист {row.Worksheet.Name}. В строке {row.RowNumber()} указана некорректная стартовая глубина"); + + if (depthEnd is < 0 or > 99999.9) + throw new FileFormatException( + $"Лист {row.Worksheet.Name}. В строке {row.RowNumber()} указана некорректная конечная глубина"); + + if (pressurePlan is < 0 or > 50000) + throw new FileFormatException( + $"Лист {row.Worksheet.Name}. В строке {row.RowNumber()} указано некорректное плановое значение перепада давления"); + + if (pressureLimitMax is < 0 or > 50000) + throw new FileFormatException( + $"Лист {row.Worksheet.Name}. В строке {row.RowNumber()} указано некорректное ограничение перепада давления"); + + if (axialLoadPlan is < 0 or > 50000) + throw new FileFormatException( + $"Лист {row.Worksheet.Name}. В строке {row.RowNumber()} указано некорректное плановое значение нагрузки"); + + if (axialLoadLimitMax is < 0 or > 50000) + throw new FileFormatException( + $"Лист {row.Worksheet.Name}. В строке {row.RowNumber()} указано некорректное ограничение нагрузки"); + + if (topDriveTorquePlan is < 0 or > 50000) + throw new FileFormatException( + $"Лист {row.Worksheet.Name}. В строке {row.RowNumber()} указано некорректное плановое значение момента на ВСП"); + + if (topDriveTorqueLimitMax is < 0 or > 50000) + throw new FileFormatException( + $"Лист {row.Worksheet.Name}. В строке {row.RowNumber()} указано некорректное ограничение момента на ВСП"); + + if (topDriveSpeedPlan is < 0 or > 50000) + throw new FileFormatException( + $"Лист {row.Worksheet.Name}. В строке {row.RowNumber()} указано некорректное плановое значение оборотов на ВСП"); + + if (topDriveSpeedLimitMax is < 0 or > 50000) + throw new FileFormatException( + $"Лист {row.Worksheet.Name}. В строке {row.RowNumber()} указано некорректное ограничения оборота на ВСП"); + + if (flowPlan is < 0 or > 50000) + throw new FileFormatException( + $"Лист {row.Worksheet.Name}. В строке {row.RowNumber()} указано некорректное плановое значение расхода"); + + if (flowLimitMax is < 0 or > 50000) + throw new FileFormatException( + $"Лист {row.Worksheet.Name}. В строке {row.RowNumber()} указано некорректное ограничение расхода"); + + if (ropPlan is < 0 or > 99999.9) + throw new FileFormatException( + $"Лист {row.Worksheet.Name}. В строке {row.RowNumber()} указано некорректное плановое значение механической скорости"); + + if (usageSaub is < 0 or > 100) + throw new FileFormatException( + $"Лист {row.Worksheet.Name}. В строке {row.RowNumber()} указан некорректный плановый процент использования АКБ"); + + if (usageSpin is < 0 or > 100) + throw new FileFormatException( + $"Лист {row.Worksheet.Name}. В строке {row.RowNumber()} указан некорректные плановый процент использования spin master"); + + return new() + { + IdWellSectionType = wellSection.Id, + IdMode = idMode, + DepthStart = depthStart, + LastUpdate = DateTime.UtcNow, + DepthEnd = depthEnd, + Pressure = new() + { + Plan = pressurePlan, + LimitMax = pressureLimitMax + }, + AxialLoad = new() + { + Plan = axialLoadPlan, + LimitMax = axialLoadLimitMax + }, + TopDriveTorque = new() + { + Plan = topDriveTorquePlan, + LimitMax = topDriveTorqueLimitMax + }, + TopDriveSpeed = new() + { + Plan = topDriveSpeedPlan, + LimitMax = topDriveSpeedLimitMax + }, + Flow = new() + { + Plan = flowPlan, + LimitMax = flowLimitMax + }, + RopPlan = ropPlan, + UsageSaub = usageSaub, + UsageSpin = usageSpin + }; + } + + private async Task GenerateExcelFileStreamAsync(WellDrillingProcessMapDto[] wellDrillingProcessMaps, + CancellationToken cancellationToken) + { + using var excelTemplateStream = (await GetExcelTemplateStreamAsync(cancellationToken)).File; + + using var workbook = new XLWorkbook(excelTemplateStream, XLEventTracking.Disabled); + + AddToWorkbook(workbook, wellDrillingProcessMaps); + + MemoryStream memoryStream = new(); + workbook.SaveAs(memoryStream, new SaveOptions { }); + memoryStream.Seek(0, SeekOrigin.Begin); + return memoryStream; + } + + private static int? GetIdMode(string modeName) => + modeName switch + { + "ручной" => 0, + "ротор" => 1, + "слайд" => 2, + _ => null + }; + + private static string GetModeCaption(int idMode) + => idMode switch + { + 1 => "Ротор", + 2 => "Слайд", + _ => "Ручной", + }; +} \ No newline at end of file diff --git a/AsbCloudInfrastructure/XLExtentions.cs b/AsbCloudInfrastructure/XLExtentions.cs index 24043605..c1a463d0 100644 --- a/AsbCloudInfrastructure/XLExtentions.cs +++ b/AsbCloudInfrastructure/XLExtentions.cs @@ -122,7 +122,42 @@ internal static class XLExtentions return cell; } - internal static IXLStyle SetAllBorders(this IXLStyle style, XLBorderStyleValues borderStyle = XLBorderStyleValues.Thin) + public static IXLCell SetVal(this IXLCell cell, double? value, string format = "0.00") + { + cell.Value = (value is not null && double.IsFinite(value.Value)) ? value : null; + cell.DataType = XLDataType.Number; + cell.Style.NumberFormat.Format = format; + return cell; + } + + public static IXLCell SetVal(this IXLCell cell, string value, bool adaptRowHeight = false) + { + cell.Value = value; + if (adaptRowHeight) + { + var colWidth = cell.WorksheetColumn().Width; + var maxCharsToWrap = colWidth / (0.1d * cell.Style.Font.FontSize); + if (value.Length > maxCharsToWrap) + { + var row = cell.WorksheetRow(); + var baseHeight = row.Height; + row.Height = 0.5d * baseHeight * Math.Ceiling(1d + value.Length / maxCharsToWrap); + } + } + + return cell; + } + + public static IXLCell SetVal(this IXLCell cell, DateTime value, string dateFormat = "DD.MM.YYYY HH:MM:SS") + { + cell.Value = value; + cell.DataType = XLDataType.DateTime; + cell.Style.DateFormat.Format = dateFormat; + + return cell; + } + + internal static IXLStyle SetAllBorders(this IXLStyle style, XLBorderStyleValues borderStyle = XLBorderStyleValues.Thin) { style.Border.RightBorder = borderStyle; style.Border.LeftBorder = borderStyle;