< Summary

Information
Class: Services.ApiServices._1CApi.Stock.OneCWarehouseStockExcelExportResult
Assembly: Services
File(s): /home/runner/work/Studio2Prod/Studio2Prod/AppRepository/ApiServices/1CApi/Stock/OneCWarehouseStockExcelExporter.cs
Line coverage
80%
Covered lines: 4
Uncovered lines: 1
Coverable lines: 5
Total lines: 166
Line coverage: 80%
Branch coverage
N/A
Covered branches: 0
Total branches: 0
Branch coverage: N/A
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%11100%
get_Bytes()100%11100%
get_Error()100%210%
get_RowCount()100%11100%
get_Succeeded()100%11100%

File(s)

/home/runner/work/Studio2Prod/Studio2Prod/AppRepository/ApiServices/1CApi/Stock/OneCWarehouseStockExcelExporter.cs

#LineLine coverage
 1using ClosedXML.Excel;
 2
 3namespace Services.ApiServices._1CApi.Stock;
 4
 5public interface IOneCWarehouseStockExcelExporter
 6{
 7    Task<OneCWarehouseStockExcelExportResult> ExportAsync(
 8        OneCWarehouseStockQuery query,
 9        CancellationToken cancellationToken = default);
 10}
 11
 112public sealed record OneCWarehouseStockExcelExportResult(
 213    byte[]? Bytes,
 014    string? Error,
 215    int RowCount)
 16{
 117    public bool Succeeded => Bytes is not null;
 18}
 19
 20/// <summary>Собирает в Excel все страницы одной выборки остатков, сохраняя правила фильтрации страницы.</summary>
 21public sealed class OneCWarehouseStockExcelExporter : IOneCWarehouseStockExcelExporter
 22{
 23    private const int ExportPageSize = 100;
 24    private readonly IOneCWarehouseStockService _stockService;
 25
 26    public OneCWarehouseStockExcelExporter(IOneCWarehouseStockService stockService)
 27        => _stockService = stockService;
 28
 29    public async Task<OneCWarehouseStockExcelExportResult> ExportAsync(
 30        OneCWarehouseStockQuery query,
 31        CancellationToken cancellationToken = default)
 32    {
 33        var rows = new List<OneCWarehouseStockRow>();
 34        OneCWarehouseStockResult? firstPage = null;
 35        var page = 1;
 36
 37        while (true)
 38        {
 39            var pageQuery = query with
 40            {
 41                Page = page,
 42                PageSize = ExportPageSize,
 43                ForceRefresh = page == 1 && query.ForceRefresh
 44            };
 45            var result = await _stockService.QueryAsync(pageQuery, cancellationToken);
 46            if (result.Failed)
 47                return new OneCWarehouseStockExcelExportResult(null, result.Warning ?? "Не удалось загрузить остатки из 
 48
 49            firstPage ??= result;
 50            rows.AddRange(result.Rows);
 51
 52            var totalPages = Math.Max(1, (int)Math.Ceiling(result.TotalCount / (double)ExportPageSize));
 53            if (page >= totalPages)
 54                break;
 55
 56            page++;
 57        }
 58
 59        return new OneCWarehouseStockExcelExportResult(BuildWorkbook(firstPage!, query, rows), null, rows.Count);
 60    }
 61
 62    private static byte[] BuildWorkbook(
 63        OneCWarehouseStockResult summary,
 64        OneCWarehouseStockQuery query,
 65        IReadOnlyList<OneCWarehouseStockRow> rows)
 66    {
 67        using var workbook = new XLWorkbook();
 68        var sheet = workbook.Worksheets.Add("Остатки 1С");
 69        sheet.ShowGridLines = false;
 70
 71        const int columns = 10;
 72        sheet.Range(1, 1, 1, columns).Merge();
 73        sheet.Cell(1, 1).Value = $"Остатки 1С: {summary.WarehouseName}";
 74        sheet.Cell(1, 1).Style.Font.Bold = true;
 75        sheet.Cell(1, 1).Style.Font.FontSize = 14;
 76        sheet.Cell(1, 1).Style.Font.FontColor = XLColor.FromHtml("0F172A");
 77
 78        sheet.Range(2, 1, 2, columns).Merge();
 79        sheet.Cell(2, 1).Value = $"Снимок: {summary.SnapshotAtUtc.ToLocalTime():dd.MM.yyyy HH:mm} · Позиций: {summary.To
 80        sheet.Cell(2, 1).Style.Font.Italic = true;
 81        sheet.Cell(2, 1).Style.Font.FontColor = XLColor.FromHtml("64748B");
 82
 83        sheet.Range(3, 1, 3, columns).Merge();
 84        sheet.Cell(3, 1).Value = DescribeFilters(query);
 85        sheet.Cell(3, 1).Style.Font.FontColor = XLColor.FromHtml("475569");
 86        sheet.Cell(3, 1).Style.Fill.BackgroundColor = XLColor.FromHtml("F8FAFC");
 87
 88        var headers = new[]
 89        {
 90            "Артикул", "Наименование", "Производитель", "Остаток, шт.", "Последний приход",
 91            "Дней на складе", "Категория давности", "Документ", "Тип документа", "Поставщик"
 92        };
 93        for (var index = 0; index < headers.Length; index++)
 94            sheet.Cell(5, index + 1).Value = headers[index];
 95
 96        var header = sheet.Range(5, 1, 5, columns);
 97        header.Style.Font.Bold = true;
 98        header.Style.Font.FontColor = XLColor.FromHtml("1E3A5F");
 99        header.Style.Fill.BackgroundColor = XLColor.FromHtml("DBEAFE");
 100        header.Style.Alignment.Horizontal = XLAlignmentHorizontalValues.Center;
 101        header.Style.Alignment.Vertical = XLAlignmentVerticalValues.Center;
 102        header.Style.Border.BottomBorder = XLBorderStyleValues.Thin;
 103        header.Style.Border.BottomBorderColor = XLColor.FromHtml("93C5FD");
 104
 105        var rowNumber = 6;
 106        foreach (var row in rows)
 107        {
 108            sheet.Cell(rowNumber, 1).Value = row.Article ?? string.Empty;
 109            sheet.Cell(rowNumber, 2).Value = row.Name ?? string.Empty;
 110            sheet.Cell(rowNumber, 3).Value = row.Producer ?? string.Empty;
 111            sheet.Cell(rowNumber, 4).Value = row.Quantity;
 112            sheet.Cell(rowNumber, 5).Value = row.LastReceiptDate;
 113            sheet.Cell(rowNumber, 6).Value = row.DaysOnStock;
 114            sheet.Cell(rowNumber, 7).Value = row.AgeBucket.HasValue
 115                ? OneCWarehouseStockQueryBuilder.BucketLabel(row.AgeBucket.Value)
 116                : string.Empty;
 117            sheet.Cell(rowNumber, 8).Value = row.LastReceiptDocNumber ?? string.Empty;
 118            sheet.Cell(rowNumber, 9).Value = row.LastReceiptDocKind ?? string.Empty;
 119            sheet.Cell(rowNumber, 10).Value = row.LastReceiptSupplier ?? string.Empty;
 120            rowNumber++;
 121        }
 122
 123        if (rows.Count > 0)
 124        {
 125            var dataRange = sheet.Range(6, 1, rowNumber - 1, columns);
 126            dataRange.Style.Alignment.Vertical = XLAlignmentVerticalValues.Center;
 127            dataRange.Style.Border.BottomBorder = XLBorderStyleValues.Hair;
 128            dataRange.Style.Border.BottomBorderColor = XLColor.FromHtml("E2E8F0");
 129            sheet.Range(6, 4, rowNumber - 1, 4).Style.NumberFormat.Format = "#,##0.00";
 130            sheet.Range(6, 5, rowNumber - 1, 5).Style.DateFormat.Format = "dd.MM.yyyy";
 131            sheet.Range(6, 6, rowNumber - 1, 6).Style.NumberFormat.Format = "0";
 132            sheet.Range(5, 1, rowNumber - 1, columns).SetAutoFilter();
 133        }
 134
 135        sheet.Column(1).Width = 20;
 136        sheet.Column(2).Width = 42;
 137        sheet.Column(3).Width = 22;
 138        sheet.Column(4).Width = 15;
 139        sheet.Column(5).Width = 16;
 140        sheet.Column(6).Width = 18;
 141        sheet.Column(7).Width = 24;
 142        sheet.Column(8).Width = 20;
 143        sheet.Column(9).Width = 24;
 144        sheet.Column(10).Width = 28;
 145        sheet.SheetView.FreezeRows(5);
 146
 147        using var stream = new MemoryStream();
 148        workbook.SaveAs(stream);
 149        return stream.ToArray();
 150    }
 151
 152    private static string DescribeFilters(OneCWarehouseStockQuery query)
 153    {
 154        var age = query.Mode switch
 155        {
 156            StockAgeFilterMode.OlderThanDays => $"лежит более {query.OlderThanDays ?? 0} дн.",
 157            StockAgeFilterMode.ReceiptBetween => $"приход с {query.ReceiptFrom:dd.MM.yyyy} по {query.ReceiptTo:dd.MM.yyy
 158            _ => "без ограничения по сроку поступления"
 159        };
 160        var filters = new List<string> { age };
 161        if (!string.IsNullOrWhiteSpace(query.ArticleSearch)) filters.Add($"артикул: {query.ArticleSearch}");
 162        if (!string.IsNullOrWhiteSpace(query.ProducerSearch)) filters.Add($"производитель: {query.ProducerSearch}");
 163        if (query.MinQuantity > 0) filters.Add($"остаток от {query.MinQuantity:N2}");
 164        return "Фильтры: " + string.Join(" · ", filters);
 165    }
 166}