< Summary

Information
Class: OzonOrdersWeb.Areas.Studio2.Controllers.OneCStockController
Assembly: OzonOrdersWeb
File(s): /home/runner/work/Studio2Prod/Studio2Prod/OzonOrdersWeb/Areas/Studio2/Controllers/OneCStockController.cs
Line coverage
0%
Covered lines: 0
Uncovered lines: 118
Coverable lines: 118
Total lines: 232
Line coverage: 0%
Branch coverage
0%
Covered branches: 0
Total branches: 48
Branch coverage: 0%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%210%
Index()100%210%
Data()0%4260%
ExportExcel()0%620%
BuildQuery(...)0%1056320%

File(s)

/home/runner/work/Studio2Prod/Studio2Prod/OzonOrdersWeb/Areas/Studio2/Controllers/OneCStockController.cs

#LineLine coverage
 1using Microsoft.AspNetCore.Authorization;
 2using Microsoft.AspNetCore.Mvc;
 3using OzonOrdersWeb.Areas.Studio2.ViewModels.OneCStock;
 4using OzonOrdersWeb.Authorization;
 5using OzonRepositories.Data;
 6using Services.ApiServices._1CApi.OData;
 7using Services.ApiServices._1CApi.Stock;
 8
 9namespace OzonOrdersWeb.Areas.Studio2.Controllers;
 10
 11/// <summary>
 12/// Раздел «Остатки 1С по складам»: номенклатура на выбранном складе с фильтром по сроку поступления.
 13/// Данные читаются из 1С онлайн, поэтому страница отдаётся сразу, а таблица приходит отдельным AJAX.
 14/// </summary>
 15[Area("Studio2")]
 16[Authorize(Roles = "User,Admin")]
 17[Route("Studio2/OneCStock")]
 18[Route("OneCStock")]
 19public sealed class OneCStockController : Controller
 20{
 21    private const int DefaultPageSize = 25;
 22    private const int MinPageSize = 10;
 23    private const int MaxPageSize = 100;
 24    private const int MaxOlderThanDays = 3650;
 25    private const int MaxRangeDays = 1095;
 26
 27    private readonly IOneCWarehouseStockService _stockService;
 28    private readonly IOneCWarehouseStockExcelExporter _excelExporter;
 29    private readonly ProductRepository _productRepository;
 30    private readonly IOneCODataReferenceService _referenceService;
 31
 032    public OneCStockController(
 033        IOneCWarehouseStockService stockService,
 034        IOneCWarehouseStockExcelExporter excelExporter,
 035        ProductRepository productRepository,
 036        IOneCODataReferenceService referenceService)
 37    {
 038        _stockService = stockService;
 039        _excelExporter = excelExporter;
 040        _productRepository = productRepository;
 041        _referenceService = referenceService;
 042    }
 43
 44    [HttpGet("")]
 45    public async Task<IActionResult> Index(Guid? warehouse, CancellationToken cancellationToken = default)
 46    {
 047        var warehousesTask = _stockService.GetWarehousesAsync(cancellationToken);
 048        var suppliersTask = _referenceService.GetCounterpartiesAsync();
 049        await Task.WhenAll(warehousesTask, suppliersTask);
 050        var warehouses = await warehousesTask;
 051        var suppliers = await suppliersTask;
 52
 053        return View(new OneCStockIndexViewModel
 054        {
 055            Warehouses = warehouses,
 056            Suppliers = suppliers
 057                .Where(supplier => !string.IsNullOrWhiteSpace(supplier.Description))
 058                .OrderBy(supplier => supplier.Description, StringComparer.CurrentCulture)
 059                .ToList(),
 060            SelectedWarehouse = warehouse,
 061            OneCUnavailable = warehouses.Count == 0
 062        });
 063    }
 64
 65    /// <summary>
 66    /// Данные таблицы. Помечен <see cref="ServiceEndpointAttribute"/>: иначе авто-регистрация страниц
 67    /// заведёт псевдостраницу Studio2.OneCStock.Data и не-Admin получит 403 на запрос таблицы.
 68    /// </summary>
 69    [ServiceEndpoint]
 70    [HttpGet("data")]
 71    public async Task<IActionResult> Data(
 72        Guid warehouse,
 73        string mode = "none",
 74        int? olderThanDays = null,
 75        DateTime? receiptFrom = null,
 76        DateTime? receiptTo = null,
 77        string? article = null,
 78        string? producer = null,
 79        string documentType = "all",
 80        Guid? supplier = null,
 81        decimal minQty = 0m,
 82        string sort = "article",
 83        bool desc = false,
 84        int page = 1,
 85        int pageSize = DefaultPageSize,
 86        bool refresh = false,
 87        CancellationToken cancellationToken = default)
 88    {
 089        var query = BuildQuery(warehouse, mode, olderThanDays, receiptFrom, receiptTo,
 090            article, producer, documentType, supplier, minQty, sort, desc, page, pageSize, refresh);
 91
 092        var result = await _stockService.QueryAsync(query, cancellationToken);
 093        var imageUrls = result.Failed
 094            ? new Dictionary<string, string>()
 095            : await _productRepository.GetImageUrlsByArticlesAsync(
 096                result.Rows.Select(row => row.Article ?? string.Empty));
 97
 098        return Ok(new
 099        {
 0100            ok = !result.Failed,
 0101            error = result.Failed ? result.Warning : null,
 0102            warning = result.Failed ? null : result.Warning,
 0103            warehouseName = result.WarehouseName,
 0104            totalCount = result.TotalCount,
 0105            totalPositions = result.TotalPositions,
 0106            totalQuantity = result.TotalQuantityFiltered,
 0107            page = result.Page,
 0108            pageSize = result.PageSize,
 0109            snapshotAtUtc = result.SnapshotAtUtc,
 0110            items = result.Rows.Select(row => new
 0111            {
 0112                article = row.Article,
 0113                name = row.Name,
 0114                producer = row.Producer,
 0115                imageUrl = row.Article != null && imageUrls.TryGetValue(row.Article, out var imageUrl)
 0116                    ? imageUrl
 0117                    : null,
 0118                quantity = row.Quantity,
 0119                lastReceiptDate = row.LastReceiptDate,
 0120                daysOnStock = row.DaysOnStock,
 0121                lastReceiptDoc = row.LastReceiptDocNumber,
 0122                lastReceiptKind = row.LastReceiptDocKind,
 0123                lastReceiptSupplier = row.LastReceiptSupplier,
 0124                ageLabel = row.AgeBucket.HasValue
 0125                    ? OneCWarehouseStockQueryBuilder.BucketLabel(row.AgeBucket.Value)
 0126                    : null
 0127            })
 0128        });
 0129    }
 130
 131    [HttpGet("export/excel")]
 132    public async Task<IActionResult> ExportExcel(
 133        Guid warehouse,
 134        string mode = "none",
 135        int? olderThanDays = null,
 136        DateTime? receiptFrom = null,
 137        DateTime? receiptTo = null,
 138        string? article = null,
 139        string? producer = null,
 140        string documentType = "all",
 141        Guid? supplier = null,
 142        decimal minQty = 0m,
 143        string sort = "article",
 144        bool desc = false,
 145        bool refresh = false,
 146        CancellationToken cancellationToken = default)
 147    {
 0148        var query = BuildQuery(warehouse, mode, olderThanDays, receiptFrom, receiptTo,
 0149            article, producer, documentType, supplier, minQty, sort, desc, page: 1, pageSize: MaxPageSize, refresh);
 0150        var export = await _excelExporter.ExportAsync(query, cancellationToken);
 0151        if (!export.Succeeded)
 0152            return BadRequest(new { error = export.Error });
 153
 0154        var fileName = $"onec-stock-{DateTime.Now:yyyyMMdd-HHmm}.xlsx";
 0155        return File(export.Bytes!, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", fileName);
 0156    }
 157
 158    /// <summary>Приводит параметры запроса к безопасным границам — контроллер отвечает за валидацию входа.</summary>
 159    private static OneCWarehouseStockQuery BuildQuery(
 160        Guid warehouse,
 161        string mode,
 162        int? olderThanDays,
 163        DateTime? receiptFrom,
 164        DateTime? receiptTo,
 165        string? article,
 166        string? producer,
 167        string documentType,
 168        Guid? supplier,
 169        decimal minQty,
 170        string sort,
 171        bool desc,
 172        int page,
 173        int pageSize,
 174        bool refresh)
 175    {
 0176        var filterMode = mode switch
 0177        {
 0178            "older" => StockAgeFilterMode.OlderThanDays,
 0179            "range" => StockAgeFilterMode.ReceiptBetween,
 0180            _ => StockAgeFilterMode.None
 0181        };
 182
 0183        var days = olderThanDays.HasValue
 0184            ? Math.Clamp(olderThanDays.Value, 1, MaxOlderThanDays)
 0185            : (int?)null;
 186
 0187        var to = receiptTo;
 0188        var from = receiptFrom;
 0189        if (filterMode == StockAgeFilterMode.ReceiptBetween)
 190        {
 0191            to ??= DateTime.Today;
 0192            from ??= to.Value.AddMonths(-1);
 193            // Слишком широкий диапазон превращает обороты в многостраничную выгрузку — режем сверху.
 0194            if ((to.Value - from.Value).TotalDays > MaxRangeDays)
 0195                from = to.Value.AddDays(-MaxRangeDays);
 196        }
 197
 0198        var normalizedSort = sort switch
 0199        {
 0200            "qty" or "producer" or "age" => sort,
 0201            _ => "article"
 0202        };
 203
 0204        var normalizedDocumentType = documentType switch
 0205        {
 0206            "receipt" => ReceiptDocumentFilter.ReceiptInvoice,
 0207            "transfer" => ReceiptDocumentFilter.StockTransfer,
 0208            _ => ReceiptDocumentFilter.All
 0209        };
 210
 211        // У перемещения нет реквизита поставщика — не передаём несогласованную комбинацию в сервис.
 0212        if (normalizedDocumentType == ReceiptDocumentFilter.StockTransfer)
 0213            supplier = null;
 214
 0215        return new OneCWarehouseStockQuery(
 0216            warehouse,
 0217            filterMode,
 0218            days,
 0219            from,
 0220            to,
 0221            string.IsNullOrWhiteSpace(article) ? null : article.Trim(),
 0222            string.IsNullOrWhiteSpace(producer) ? null : producer.Trim(),
 0223            minQty < 0 ? 0m : minQty,
 0224            normalizedSort,
 0225            desc,
 0226            Math.Max(1, page),
 0227            Math.Clamp(pageSize, MinPageSize, MaxPageSize),
 0228            refresh,
 0229            normalizedDocumentType,
 0230            supplier);
 231    }
 232}