< Summary

Information
Class: OzonOrdersWeb.WebServices.Transactions.Grid.Load.TransactionGridLoadRequest
Assembly: OzonOrdersWeb
File(s): /home/runner/work/Studio2Prod/Studio2Prod/OzonOrdersWeb/WebServices/Transactions/Grid/Load/TransactionGridLoadService.cs
Line coverage
0%
Covered lines: 0
Uncovered lines: 6
Coverable lines: 6
Total lines: 609
Line coverage: 0%
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%210%
get_Ids()100%210%
get_Principal()100%210%
get_SourceUrl()100%210%
get_ClientIp()100%210%
get_UserAgent()100%210%

File(s)

/home/runner/work/Studio2Prod/Studio2Prod/OzonOrdersWeb/WebServices/Transactions/Grid/Load/TransactionGridLoadService.cs

#LineLine coverage
 1using System.Security.Claims;
 2using Microsoft.EntityFrameworkCore;
 3using OzonDomains;
 4using OzonDomains.Models;
 5using OzonOrdersWeb.Areas.Studio2.Controllers.Base;
 6using OzonOrdersWeb.WebServices.Currency;
 7using OzonOrdersWeb.WebServices.Orders.Grid;
 8using OzonOrdersWeb.WebServices.Transactions.Grid.Contracts;
 9using OzonOrdersWeb.WebServices.Transactions.Grid.Definitions;
 10using OzonRepositories.Context;
 11using Services.ApiServices;
 12using Services.ApiServices._1CApi;
 13using Services.BackgroundJobsServices;
 14using Services.ApiServices.OzonApi;
 15using Services.ApiSupplierSettingsServices;
 16using Services.Authorization.Services;
 17using Services.CacheServices.Cache.UserCacheService;
 18using Services.DataServices;
 19using Services.StockSyncServices;
 20using Services.TransactionUtilsServices;
 21
 22namespace OzonOrdersWeb.WebServices.Transactions.Grid.Load;
 23
 024public sealed record TransactionGridLoadRequest(
 025    IReadOnlyList<int> Ids,
 026    ClaimsPrincipal Principal,
 027    string? SourceUrl,
 028    string? ClientIp,
 029    string? UserAgent);
 30
 31public interface ITransactionGridLoadService
 32{
 33    Task<TransactionGridPageDto> LoadAsync(TransactionDefinition definition, TransactionGridLoadRequest request, Cancell
 34}
 35
 36/// <summary>
 37/// Страница проведения: строки через конвейер OrdersGrid (RLS, гейтинг колонок), редактируемая часть из заказов
 38/// БД, справочники по определению. Повторяет GET классических страниц (<c>CreateTransactionBase</c>), но отдаёт JSON.
 39/// </summary>
 40public sealed class TransactionGridLoadService : ITransactionGridLoadService
 41{
 42    private readonly ApplicationDbContext _context;
 43    private readonly IOrdersGridRowHydrator _hydrator;
 44    private readonly IOrdersGridScopedQuery _scoped;
 45    private readonly OrdersDataServices _orders;
 46    private readonly AppStatusDataServices _appStatuses;
 47    private readonly SupplierDataServices _suppliers;
 48    private readonly WarehouseDataServices _warehouses;
 49    private readonly ExpenseTypeDataServices _expenseTypes;
 50    private readonly StatusTransitionDataServices _transitions;
 51    private readonly ICurrencyRatesViewBagService _rates;
 52    private readonly IUserCacheService _userCache;
 53    private readonly IScopeFilterValuesService? _scopeValues;
 54    private readonly IDataScopeService? _dataScope;
 55    private readonly IWarehouseStockLookupService? _stockLookup;
 56    private readonly ApiSupplierWarehouseService? _apiSupplierWarehouses;
 57    private readonly OzonOrderCancellationService? _cancellation;
 58    private readonly OneCDataManager? _oneC;
 59    private readonly ITransactionTimeTrackingService? _tracking;
 60    private readonly WarehouseSelectionResolver? _warehouseResolver;
 61    private readonly OneCReceiptWarehouseHistoryResolver? _receiptHistory;
 62    private readonly ILogger<TransactionGridLoadService> _logger;
 63
 64    public TransactionGridLoadService(
 65        ApplicationDbContext context,
 66        IOrdersGridRowHydrator hydrator,
 67        IOrdersGridScopedQuery scoped,
 68        OrdersDataServices orders,
 69        AppStatusDataServices appStatuses,
 70        SupplierDataServices suppliers,
 71        WarehouseDataServices warehouses,
 72        ExpenseTypeDataServices expenseTypes,
 73        StatusTransitionDataServices transitions,
 74        ICurrencyRatesViewBagService rates,
 75        IUserCacheService userCache,
 76        ILogger<TransactionGridLoadService> logger,
 77        IScopeFilterValuesService? scopeValues = null,
 78        IDataScopeService? dataScope = null,
 79        IWarehouseStockLookupService? stockLookup = null,
 80        ApiSupplierWarehouseService? apiSupplierWarehouses = null,
 81        OzonOrderCancellationService? cancellation = null,
 82        OneCDataManager? oneC = null,
 83        ITransactionTimeTrackingService? tracking = null,
 84        WarehouseSelectionResolver? warehouseResolver = null,
 85        OneCReceiptWarehouseHistoryResolver? receiptHistory = null)
 86    {
 87        _context = context;
 88        _hydrator = hydrator;
 89        _scoped = scoped;
 90        _orders = orders;
 91        _appStatuses = appStatuses;
 92        _suppliers = suppliers;
 93        _warehouses = warehouses;
 94        _expenseTypes = expenseTypes;
 95        _transitions = transitions;
 96        _rates = rates;
 97        _userCache = userCache;
 98        _logger = logger;
 99        _scopeValues = scopeValues;
 100        _dataScope = dataScope;
 101        _stockLookup = stockLookup;
 102        _apiSupplierWarehouses = apiSupplierWarehouses;
 103        _cancellation = cancellation;
 104        _oneC = oneC;
 105        _tracking = tracking;
 106        _warehouseResolver = warehouseResolver;
 107        _receiptHistory = receiptHistory;
 108    }
 109
 110    public async Task<TransactionGridPageDto> LoadAsync(TransactionDefinition d, TransactionGridLoadRequest request, Can
 111    {
 112        var principal = request.Principal;
 113        var userId = principal.FindFirstValue(ClaimTypes.NameIdentifier) ?? string.Empty;
 114        var isAdmin = principal.IsInRole("Admin");
 115        var warnings = new List<string>();
 116        var skipped = new List<int>();
 117
 118        // 1. Ids → data scope → RLS → строки грида (в порядке ссылки).
 119        var ids = request.Ids.Where(i => i > 0).Distinct().ToList();
 120        var scopedIds = await ApplyDataScopeAsync(ids, userId, ct);
 121        var rows = await _hydrator.HydrateAsync(scopedIds, principal, ct);
 122        var rowById = rows.ToDictionary(r => r.Id);
 123        skipped.AddRange(ids.Where(id => !rowById.ContainsKey(id)));
 124        if (skipped.Count > 0)
 125        {
 126            warnings.Add(skipped.Count == 1 ? $"Заказ #{skipped[0]} недоступен и пропущен." : $"Недоступных заказов проп
 127        }
 128
 129        // 2. Заказы БД (без трекинга) — для редактируемых полей и предупреждений.
 130        var orders = rowById.Count > 0 ? await _orders.GetOrdersNoTracking(rowById.Keys.ToArray()) : new List<Order>();
 131        var orderById = orders.ToDictionary(o => o.Id);
 132        orders = ids.Where(orderById.ContainsKey).Select(id => orderById[id]).ToList();
 133
 134        // 3. Статусы: переходы из БД или дефолты определения.
 135        var (statuses, targetStatusId, transitions) = await BuildStatusesAsync(d, userId, ct);
 136
 137        // 4. Правки по строкам.
 138        var c0 = d.ClearPurchasePriceOnLoad && _stockLookup != null && orders.Count > 0
 139            ? await SafeC0Async(orders, ct)
 140            : new Dictionary<int, decimal>();
 141        // В выпадающем списке показываем обе стороны переходов и дополнительные статусы.
 142        // Та же совокупность считается допустимой для строки, иначе интерфейс предлагал статус, который тут же блокиров
 143        var selectableStatusIds = statuses.Select(s => s.Id).ToHashSet();
 144        var allowedNames = d.AllowedSourceStatuses.ToHashSet(StringComparer.OrdinalIgnoreCase);
 145        var statusOnly = d.StatusOnlyWhenAlreadyIn.ToHashSet(StringComparer.OrdinalIgnoreCase);
 146        var disallowMarket = d.DisallowMarketplaceStatuses.ToHashSet(StringComparer.OrdinalIgnoreCase);
 147        var fromToTarget = transitions.GroupBy(t => t.FromStatusId).ToDictionary(g => g.Key, g => g.First().ToStatusId);
 148
 149        var edits = new List<TransactionGridRowDto>(orders.Count);
 150        foreach (var o in orders)
 151        {
 152            var edit = new TransactionGridRowEdit
 153            {
 154                Id = o.Id,
 155                SupplierId = o.SupplierId,
 156                ShipmentWarehouseId = o.ShipmentWarehouseId,
 157                Zasyl = TransactionWarehouseHelper.GetZasylValue(o),
 158                OriginalPurchasePrice = o.PurchasePrice,
 159                // Данные заказа — стартовое состояние транзакции. C0 остаётся только подсказкой,
 160                // но не должен стирать уже заполненную цену закупки при переходе из OrdersGrid.
 161                PurchasePrice = o.PurchasePrice,
 162                WarehouseStockC0 = c0.TryGetValue(o.Id, out var c0Value) ? c0Value : null,
 163                Price = o.Price,
 164                CurrentPriceWithDiscount = o.ProductInfo?.CurrentPriceWithDiscount,
 165                Quantity = o.Quantity,
 166                OriginalQuantity = o.Quantity,
 167                TrueQuantity = o.TrueQuantity,
 168                ProductKey = o.ProductKey,
 169                OrderNumberToSupplier = o.OrderNumberToSupplier,
 170                Weight = o.ProductInfo?.Weight,
 171                Manufacturer = o.Manufacturer?.Name,
 172                ExtraExpenses = (o.ExtraExpenses ?? new List<OrderExtraExpense>())
 173                    .Select(e => new TransactionGridExtraExpenseEdit { ExpenseTypeId = e.ExpenseTypeId, Amount = e.Amoun
 174                    .ToList()
 175            };
 176
 177            var statusName = o.AppStatus?.Name ?? string.Empty;
 178            if (d.PriceEditingOnly)
 179            {
 180                edit.AppStatusId = o.AppStatusId;
 181            }
 182            else if (statusOnly.Contains(statusName))
 183            {
 184                edit.StatusOnly = true;
 185                edit.AppStatusId = targetStatusId ?? o.AppStatusId;
 186            }
 187            // Для транзакций с явными переходами сохраняем их семантику: исходный статус
 188            // сопоставляется с целевым. Для status-only сценариев список содержит только
 189            // доступные для выбора целевые статусы, поэтому исходный разрешённый статус
 190            // (например «Не указан») должен сразу получать TargetStatusName.
 191            else if (transitions.Count > 0 && selectableStatusIds.Count > 0)
 192            {
 193                if (selectableStatusIds.Contains(o.AppStatusId ?? 0))
 194                {
 195                    edit.AppStatusId = fromToTarget.TryGetValue(o.AppStatusId ?? 0, out var to) ? to : targetStatusId;
 196                }
 197                else
 198                {
 199                    edit.AppStatusId = o.AppStatusId;
 200                    edit.Warnings.Add($"Статус «{statusName}» не входит в допустимые для транзакции.");
 201                }
 202            }
 203            else if (allowedNames.Count > 0 && !allowedNames.Contains(statusName))
 204            {
 205                edit.AppStatusId = o.AppStatusId;
 206                edit.Warnings.Add($"Статус «{statusName}», а должен быть: {string.Join(" / ", allowedNames)}.");
 207            }
 208            else if (targetStatusId.HasValue)
 209            {
 210                edit.AppStatusId = targetStatusId;
 211            }
 212            else
 213            {
 214                edit.AppStatusId = targetStatusId ?? o.AppStatusId;
 215            }
 216
 217            if (!string.IsNullOrEmpty(o.Status) && disallowMarket.Contains(o.Status))
 218            {
 219                edit.Warnings.Add($"Статус маркетплейса «{o.Status}» — проведение недоступно.");
 220            }
 221            else if (!d.PriceEditingOnly && d.TargetStatusName != null && !TransactionConfig.StandardOrderFilter(o))
 222            {
 223                edit.Warnings.Add("Заказ отменён маркетплейсом.");
 224            }
 225
 226            edits.Add(new TransactionGridRowDto { Base = rowById[o.Id], Edit = edit });
 227        }
 228
 229        if (d.RequireSingleSupplierAndWarehouse && orders.Count > 1)
 230        {
 231            var inns = orders.Select(o => o.Supplier?.INNCode?.Trim()).Where(i => !string.IsNullOrEmpty(i)).Distinct().T
 232            if (inns.Count > 1)
 233            {
 234                warnings.Add("Заказы от разных поставщиков (по ИНН) — приходная накладная требует одного поставщика.");
 235            }
 236
 237            var whs = orders.Select(TransactionWarehouseHelper.GetEffectiveWarehouseName).Where(w => !string.IsNullOrWhi
 238            if (whs.Count > 1)
 239            {
 240                warnings.Add($"Заказы с разных складов: {string.Join(", ", whs)}.");
 241            }
 242        }
 243
 244        // 5. Справочники.
 245        var lookups = await BuildLookupsAsync(d, orders, statuses, userId, isAdmin, ct);
 246
 247        // 5a. Подсказки складов 1С по режиму определения (как на GET классических страниц).
 248        await ApplyWarehouseSuggestionsAsync(d, orders, edits, lookups, warnings, isAdmin);
 249
 250        // 6. Учёт времени.
 251        string? trackingKey = null;
 252        if (_tracking != null && orders.Count > 0)
 253        {
 254            try
 255            {
 256                var cached = await _userCache.GetCachedUserAsync(principal);
 257                trackingKey = await _tracking.StartSessionAsync(
 258                    principal.Identity?.Name ?? string.Empty, cached?.AccessProfileId, d.Type,
 259                    orders.Select(o => o.Id), request.SourceUrl, request.ClientIp, request.UserAgent);
 260            }
 261            catch (OperationCanceledException) when (ct.IsCancellationRequested)
 262            {
 263                throw;
 264            }
 265            catch (OperationCanceledException ex)
 266            {
 267                _logger.LogInformation("TransactionsGrid: старт сессии учёта времени отменён ({Message})", ex.Message);
 268            }
 269            catch (Exception ex)
 270            {
 271                _logger.LogWarning(ex, "TransactionsGrid: не удалось начать сессию учёта времени");
 272            }
 273        }
 274
 275        return new TransactionGridPageDto
 276        {
 277            Definition = TransactionDefinitionDto.From(d, targetStatusId),
 278            Rows = edits,
 279            Lookups = lookups,
 280            Warnings = warnings,
 281            SkippedIds = skipped,
 282            TrackingKey = trackingKey,
 283            IsAdmin = isAdmin
 284        };
 285    }
 286
 287    /// <summary>
 288    /// GlobalPair (WarehouseTransfer): отправитель/получатели по WarehouseSelectionResolver, не-админу отправитель фикс
 289    /// PerOrderRecipient (CustomerReturn): получатель по заказу. PerOrderBoth (ReturnToSupplier): отправитель по маппин
 290    /// возврата, получатель — по последнему проведённому приходу 1С для артикула.
 291    /// </summary>
 292    private async Task ApplyWarehouseSuggestionsAsync(TransactionDefinition d, List<Order> orders, List<TransactionGridR
 293    {
 294        if (orders.Count == 0 || _warehouseResolver == null || d.OneCMode == OneCMode.None)
 295        {
 296            return;
 297        }
 298
 299        try
 300        {
 301            switch (d.WarehouseMode)
 302            {
 303                case WarehouseMode.GlobalPair:
 304                {
 305                    var sender = await _warehouseResolver.ResolveSenderWarehouseAsync(orders);
 306                    var recipient = await _warehouseResolver.ResolveRecipientWarehousesAsync(orders);
 307                    lookups.SuggestedSenderWarehouse = sender.SenderWarehouseName;
 308                    lookups.SuggestedRecipientWarehouses.AddRange(recipient.PreferredRecipientWarehouses);
 309                    if (!isAdmin && sender.HasErrors)
 310                    {
 311                        warnings.AddRange(sender.Errors);
 312                    }
 313
 314                    warnings.AddRange(recipient.Warnings);
 315                    break;
 316                }
 317                case WarehouseMode.PerOrderRecipient:
 318                {
 319                    var res = await _warehouseResolver.ResolveCustomerReturnRecipientByOrderAsync(orders);
 320                    foreach (var row in rows)
 321                    {
 322                        if (res.RecipientByOrderId.TryGetValue(row.Edit.Id, out var wh))
 323                        {
 324                            row.Edit.RecipientWarehouse = wh;
 325                            row.Edit.RecipientSuggestions.Add(new TransactionGridWarehouseSuggestion(wh, null, res.Recip
 326                        }
 327                    }
 328
 329                    if (res.HasBlockingErrors)
 330                    {
 331                        warnings.AddRange(res.BlockingErrors);
 332                    }
 333
 334                    break;
 335                }
 336                case WarehouseMode.PerOrderBoth:
 337                {
 338                    var senders = await _warehouseResolver.ResolveCustomerReturnRecipientByOrderAsync(orders);
 339                    foreach (var row in rows)
 340                    {
 341                        if (senders.RecipientByOrderId.TryGetValue(row.Edit.Id, out var wh))
 342                        {
 343                            row.Edit.SenderWarehouse = wh;
 344                        }
 345                    }
 346
 347                    if (_receiptHistory != null && lookups.OneCWarehouses.Count > 0)
 348                    {
 349                        var history = await _receiptHistory.ResolveByArticlesAsync(orders.Select(o => o.Article), lookup
 350                        foreach (var row in rows)
 351                        {
 352                            var article = orders.First(o => o.Id == row.Edit.Id).Article?.Trim();
 353                            if (string.IsNullOrEmpty(article) || !history.TryGetValue(article, out var list) || list.Cou
 354                            {
 355                                continue;
 356                            }
 357
 358                            row.Edit.RecipientWarehouse = list[0].Warehouse;
 359                            row.Edit.RecipientSuggestions.AddRange(list.Select(x => new TransactionGridWarehouseSuggesti
 360                        }
 361                    }
 362
 363                    break;
 364                }
 365            }
 366        }
 367        catch (OperationCanceledException ex)
 368        {
 369            _logger.LogInformation("TransactionsGrid: подсказки складов отменены ({Message})", ex.Message);
 370        }
 371        catch (Exception ex)
 372        {
 373            _logger.LogWarning(ex, "TransactionsGrid: подсказки складов недоступны");
 374            warnings.Add("Подсказки складов 1С недоступны — выберите склады вручную.");
 375        }
 376    }
 377
 378    private async Task<List<int>> ApplyDataScopeAsync(List<int> ids, string userId, CancellationToken ct)
 379    {
 380        if (ids.Count == 0)
 381        {
 382            return ids;
 383        }
 384
 385        IQueryable<Order> q = _context.Orders.AsNoTracking().Where(o => ids.Contains(o.Id));
 386        if (_dataScope != null && !string.IsNullOrWhiteSpace(userId))
 387        {
 388            q = await _dataScope.ApplyDataScopeAsync(q, userId, "Order", null, ct);
 389        }
 390
 391        var allowed = (await q.Select(o => o.Id).ToListAsync(ct)).ToHashSet();
 392        return ids.Where(allowed.Contains).ToList();
 393    }
 394
 395    /// <summary>Список статусов для выпадающего списка строки — как <c>CreateTransactionBase</c>: переходы ⊕ дополнител
 396    private async Task<(List<StatusOption> Statuses, int? TargetStatusId, List<StatusTransition> Transitions)> BuildStat
 397    {
 398        var transitions = d.UseStatusTransitions ? await _transitions.GetByTransactionType(d.Type) : new List<StatusTran
 399        var list = new List<AppStatus>();
 400        AppStatus? target = null;
 401
 402        if (transitions.Count > 0)
 403        {
 404            list = transitions.Select(t => t.ToStatus).Concat(transitions.Select(t => t.FromStatus))
 405                .Where(s => s != null).Select(s => s!).GroupBy(s => s.Id).Select(g => g.First()).OrderBy(s => s.Name).To
 406            target = transitions.Select(t => t.ToStatus).FirstOrDefault(s => s != null);
 407        }
 408        else if (!string.IsNullOrEmpty(d.TargetStatusName))
 409        {
 410            target = await _appStatuses.GetAppStatusAsync(new AppStatus { Name = d.TargetStatusName });
 411            if (target != null)
 412            {
 413                list.Add(target);
 414            }
 415        }
 416        else if (!d.PriceEditingOnly)
 417        {
 418            list = (await _appStatuses.GetAppStatuses()).OrderBy(s => s.Name).ToList();
 419        }
 420
 421        foreach (var extra in d.ExtraStatusNames)
 422        {
 423            var s = await _appStatuses.GetAppStatusAsync(new AppStatus { Name = extra });
 424            if (s != null && list.All(x => x.Id != s.Id))
 425            {
 426                list.Add(s);
 427            }
 428        }
 429
 430        list = list.GroupBy(s => s.Id).Select(g => g.First()).ToList();
 431        list = await FilterByOrderScopeAsync(list, s => s.Id, "AppStatusId", userId, ct);
 432
 433        var options = list.Select(s => new StatusOption(s.Id, s.Name ?? string.Empty, s.GetStatusColor(), s.IsFinal)).To
 434        return (options, target?.Id, transitions);
 435    }
 436
 437    private async Task<TransactionGridLookups> BuildLookupsAsync(TransactionDefinition d, List<Order> orders, List<Statu
 438    {
 439        var rateTasks = _rates.StartFetch();
 440        var suppliers = await FilterByOrderScopeAsync(await _suppliers.GetSuppliers(), s => s.Id, "SupplierId", userId, 
 441        var warehouses = await FilterByOrderScopeAsync(await _warehouses.GetWarehouses(), w => w.Id, "ShipmentWarehouseI
 442        var expenseTypes = (await _expenseTypes.GetExpenseTypes()).Where(e => e.IsActive).OrderBy(e => e.Name).ToList();
 443
 444        var oneCWarehouseNames = new List<string>();
 445        var needsOneC = d.OneCMode != OneCMode.None || d.EditableFields.Contains("zasyl");
 446        if (needsOneC && _oneC != null)
 447        {
 448            try
 449            {
 450                oneCWarehouseNames = (await _oneC.GetWarehouseListAsync()).Select(w => w.Name).Where(n => !string.IsNull
 451            }
 452            catch (OperationCanceledException) when (ct.IsCancellationRequested)
 453            {
 454                throw;
 455            }
 456            catch (OperationCanceledException ex)
 457            {
 458                _logger.LogInformation("TransactionsGrid: получение списка складов 1С отменено ({Message})", ex.Message)
 459            }
 460            catch (Exception ex)
 461            {
 462                _logger.LogWarning(ex, "TransactionsGrid: список складов 1С недоступен");
 463            }
 464        }
 465
 466        var zasylValues = await FilterWarehouseNamesByOrderScopeAsync(oneCWarehouseNames, userId, ct);
 467        var bitrixWarehouses = d.OneCMode != OneCMode.None
 468            ? await FilterBitrixWarehouseNamesByScopeAsync(oneCWarehouseNames, userId, ct)
 469            : new List<string>();
 470
 471        var cancelReasons = new Dictionary<int, OzonOrderCancelReasonsInfo>();
 472        if (d.OzonCancelPanel && _cancellation != null && orders.Count > 0)
 473        {
 474            try
 475            {
 476                cancelReasons = await _cancellation.LoadCancelReasonsAsync(orders);
 477            }
 478            catch (OperationCanceledException) when (ct.IsCancellationRequested)
 479            {
 480                throw;
 481            }
 482            catch (OperationCanceledException ex)
 483            {
 484                _logger.LogInformation("TransactionsGrid: загрузка причин отмены Ozon отменена ({Message})", ex.Message)
 485            }
 486            catch (Exception ex)
 487            {
 488                _logger.LogWarning(ex, "TransactionsGrid: причины отмены Ozon недоступны");
 489            }
 490        }
 491
 492        var allowedWarehouses = new Dictionary<string, List<int>>();
 493        if (d.SupplierOffers && _apiSupplierWarehouses != null)
 494        {
 495            allowedWarehouses = await _apiSupplierWarehouses.GetAllAllowedWarehouseIdsByProviderAsync(ct);
 496        }
 497
 498        var rates = await rateTasks.ToSnapshotAsync();
 499
 500        // ИНН для договоров 1С: приход — поставщик заказов, расход — клиент; сами договоры грузятся лениво (Lookups?kin
 501        string? treatiesInn = null;
 502        if (d.ReceiptDocumentFields)
 503        {
 504            treatiesInn = orders.Select(o => o.Supplier?.INNCode?.Trim()).FirstOrDefault(i => !string.IsNullOrEmpty(i));
 505        }
 506        else if (d.ExpenseDocumentFields)
 507        {
 508            treatiesInn = orders.Select(o => o.OzonClient?.INNCode?.Trim()).FirstOrDefault(i => !string.IsNullOrEmpty(i)
 509        }
 510
 511        return new TransactionGridLookups
 512        {
 513            Statuses = statuses,
 514            Suppliers = suppliers.OrderBy(s => s.Name).Select(s => new SupplierOption(s.Id, s.Name ?? string.Empty, s.Si
 515            Warehouses = warehouses.OrderBy(w => w.Name).Select(w => new IdName(w.Id, w.Name ?? string.Empty)).ToList(),
 516            ExpenseTypes = expenseTypes.Select(e => new IdName(e.Id, e.Name ?? string.Empty)).ToList(),
 517            ZasylValues = zasylValues,
 518            CancelReasonsByOrder = cancelReasons,
 519            ApiSupplierAllowedWarehouses = allowedWarehouses,
 520            RateUsd = rates.RateUsd,
 521            RateEur = rates.RateEur,
 522            RateByn = rates.RateByn,
 523            OneCWarehouses = bitrixWarehouses,
 524            ForceSenderWarehouse = d.WarehouseMode == WarehouseMode.GlobalPair && !isAdmin,
 525            TreatiesInn = treatiesInn
 526        };
 527    }
 528
 529    private async Task<Dictionary<int, decimal>> SafeC0Async(List<Order> orders, CancellationToken ct)
 530    {
 531        try
 532        {
 533            return await _stockLookup!.BuildC0MapAsync(orders, ct);
 534        }
 535        catch (OperationCanceledException) when (ct.IsCancellationRequested)
 536        {
 537            throw;
 538        }
 539        catch (OperationCanceledException ex)
 540        {
 541            _logger.LogInformation("TransactionsGrid: операция получения остатков C0 отменена ({Message})", ex.Message);
 542            return new Dictionary<int, decimal>();
 543        }
 544        catch (Exception ex)
 545        {
 546            _logger.LogWarning(ex, "TransactionsGrid: остатки C0 недоступны");
 547            return new Dictionary<int, decimal>();
 548        }
 549    }
 550
 551    private async Task<List<T>> FilterByOrderScopeAsync<T>(List<T> source, Func<T, int> idSelector, string scopeField, s
 552    {
 553        if (source.Count == 0 || _scopeValues == null || string.IsNullOrWhiteSpace(userId))
 554        {
 555            return source;
 556        }
 557
 558        var allowed = await _scopeValues.GetAllowedValuesAsync(userId, "Order", ct);
 559        if (!allowed.TryGetValue(scopeField, out var raw) || raw.Count == 0)
 560        {
 561            return source;
 562        }
 563
 564        var ids = raw.Select(v => int.TryParse(v, out var id) ? id : (int?)null).Where(v => v.HasValue).Select(v => v!.V
 565        return ids.Count == 0 ? source : source.Where(x => ids.Contains(idSelector(x))).ToList();
 566    }
 567
 568    private async Task<List<string>> FilterWarehouseNamesByOrderScopeAsync(IEnumerable<string> names, string userId, Can
 569    {
 570        var normalized = names.Where(n => !string.IsNullOrWhiteSpace(n)).Distinct(StringComparer.OrdinalIgnoreCase).Orde
 571        if (normalized.Count == 0 || _scopeValues == null || string.IsNullOrWhiteSpace(userId))
 572        {
 573            return normalized;
 574        }
 575
 576        var allowed = await _scopeValues.GetAllowedValuesAsync(userId, "Order", ct);
 577        if (!allowed.TryGetValue("ShipmentWarehouseId", out var raw) || raw.Count == 0)
 578        {
 579            return normalized;
 580        }
 581
 582        var ids = raw.Select(v => int.TryParse(v, out var id) ? id : (int?)null).Where(v => v.HasValue).Select(v => v!.V
 583        if (ids.Count == 0)
 584        {
 585            return normalized;
 586        }
 587
 588        var allowedNames = (await _warehouses.GetWarehouses()).Where(w => ids.Contains(w.Id)).Select(w => w.Name)
 589            .Where(n => !string.IsNullOrWhiteSpace(n)).Select(n => n!).ToHashSet(StringComparer.OrdinalIgnoreCase);
 590        return normalized.Where(allowedNames.Contains).OrderBy(n => n).ToList();
 591    }
 592
 593    private async Task<List<string>> FilterBitrixWarehouseNamesByScopeAsync(IEnumerable<string> names, string userId, Ca
 594    {
 595        var normalized = names.Where(n => !string.IsNullOrWhiteSpace(n)).Distinct(StringComparer.OrdinalIgnoreCase).Orde
 596        if (_scopeValues == null || string.IsNullOrWhiteSpace(userId))
 597        {
 598            return normalized;
 599        }
 600
 601        var allowed = await _scopeValues.GetAllowedValuesAsync(userId, "BitrixWarehouse", ct);
 602        if (!allowed.TryGetValue("Name", out var allowedNames) || allowedNames.Count == 0)
 603        {
 604            return normalized;
 605        }
 606
 607        return normalized.Where(allowedNames.Contains).OrderBy(n => n).ToList();
 608    }
 609}