< Summary

Information
Class: OzonOrdersWeb.Areas.Studio2.Controllers.Base.BaseTransactionController
Assembly: OzonOrdersWeb
File(s): /home/runner/work/Studio2Prod/Studio2Prod/OzonOrdersWeb/Areas/Studio2/Controllers/Base/BaseTransactionController.cs
Line coverage
0%
Covered lines: 0
Uncovered lines: 1247
Coverable lines: 1247
Total lines: 1956
Line coverage: 0%
Branch coverage
0%
Covered branches: 0
Total branches: 655
Branch coverage: 0%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

File(s)

/home/runner/work/Studio2Prod/Studio2Prod/OzonOrdersWeb/Areas/Studio2/Controllers/Base/BaseTransactionController.cs

#LineLine coverage
 1using Microsoft.AspNetCore.Authorization;
 2using Microsoft.AspNetCore.Mvc;
 3using Microsoft.Extensions.DependencyInjection;
 4using Microsoft.AspNetCore.Mvc.Rendering;
 5using Microsoft.EntityFrameworkCore;
 6using System.ComponentModel;
 7using System.ComponentModel.DataAnnotations;
 8using Newtonsoft.Json;
 9using OzonDomains;
 10using OzonDomains.Models;
 11using OzonDomains.Models.AccessControl;
 12using OzonOrdersWeb.Areas.Studio2.Controllers;
 13using OzonOrdersWeb.Areas.Studio2.Helpers;
 14using OzonOrdersWeb.ViewModels.OrderViewModels;
 15using OzonOrdersWeb.Areas.Admin.OrderList;
 16using OzonOrdersWeb.Areas.Studio2.Models.Transactions;
 17using OzonRepositories.Context;
 18using OzonRepositories.Context.Identity;
 19using OzonRepositories.Data.Bitrix;
 20using PartsInfo.HttpUtils;
 21using System.Reflection;
 22using System.Security.Claims;
 23using Services.ApiServices.TelegramBotApi;
 24using Services.ApiServices;
 25using Services.ApiServices._1CApi;
 26using Services.ApiServices._1CApi.Models;
 27using Services.ApiServices._1CApi.Stock;
 28using Services.ApiServices.OzonApi;
 29using Services.Authorization.Services;
 30using Services.CacheServices.Cache.OzonOrdersCache;
 31using Services.CacheServices.Cache.UserCacheService;
 32using Services.DataServices;
 33using Services.FiltersServices.SortModels;
 34using OzonOrdersWeb.WebServices.Currency;
 35using Services.SignalRServices;
 36using Services.TransactionUtilsServices;
 37using Services.TransactionCommentAi;
 38using OzonOrdersWeb.TransactionServices;
 39using Services.CacheServices.Cache;
 40using Services.StockSyncServices;
 41using Services.ApiSupplierSettingsServices;
 42
 43namespace OzonOrdersWeb.Areas.Studio2.Controllers.Base
 44{
 45    /// <summary>
 46    /// Абстрактный базовый контроллер для всех транзакционных операций с заказами
 47    /// </summary>
 48    [Authorize(Roles = "User,Admin")]
 49    [Area("Studio2")]
 50    public abstract class BaseTransactionController : Controller
 51    {
 52        protected const string TransactionErrorCodeTempDataKey = "TransactionErrorCode";
 53
 54        protected readonly ApplicationDbContext Context;
 55        protected readonly AppStatusDataServices AppStatusServices;
 56        protected readonly OrdersDataServices OrderServices;
 57        protected readonly TransactionManager TransactionManager;
 58        protected readonly TransactionCache TransactionCache;
 59        protected readonly SupplierDataServices SupplierDataServices;
 60        protected readonly IUserCacheService UserCacheService;
 61        protected readonly ICurrencyRatesViewBagService CurrencyRatesViewBagService;
 62        protected readonly CacheUpdater<Order> CacheUpdater;
 63        protected readonly OrderCache OrderCache;
 64        protected readonly OneCDataManager? OneCDataManager;
 65        protected readonly WarehouseDataServices? WarehouseDataServices;
 66        protected readonly BitrixStockRepository? BitrixStockRepository;
 67        protected readonly ProxyHttpClientService? ProxyHttpClientService;
 68        protected readonly OneCTransferManager? OneCTransferManager;
 69        protected readonly OrderCartServices? OrderCartServices;
 70        protected readonly WarehouseMappingDataServices? WarehouseMappingDataServices;
 71        protected readonly ExpenseTypeDataServices? ExpenseTypeDataServices;
 72        protected readonly OrderExtraExpenseDataServices? OrderExtraExpenseDataServices;
 73        protected readonly StatusTransitionDataServices? StatusTransitionDataServices;
 74        protected readonly IScopeFilterValuesService? ScopeFilterValuesService;
 75        protected readonly IDataScopeService? DataScopeService;
 76        protected readonly OzonOrderCancellationService? OzonCancellationService;
 77
 078        protected BaseTransactionController(
 079            ApplicationDbContext context,
 080            AppStatusDataServices appStatusServices,
 081            OrdersDataServices orderServices,
 082            TransactionManager transactionManager,
 083            TransactionCache transactionCache,
 084            SupplierDataServices supplierDataServices,
 085            IUserCacheService userCacheService,
 086            ICurrencyRatesViewBagService currencyRatesViewBagService,
 087            CacheUpdater<Order> cacheUpdater,
 088            OrderCache orderCache,
 089            OneCDataManager? oneCDataManager = null,
 090            WarehouseDataServices? warehouseDataServices = null,
 091            BitrixStockRepository? bitrixStockRepository = null,
 092            ProxyHttpClientService? proxyHttpClientService = null,
 093            OneCTransferManager? oneCTransferManager = null,
 094            OrderCartServices? orderCartServices = null,
 095            WarehouseMappingDataServices? warehouseMappingDataServices = null,
 096            ExpenseTypeDataServices? expenseTypeDataServices = null,
 097            OrderExtraExpenseDataServices? orderExtraExpenseDataServices = null,
 098            StatusTransitionDataServices? statusTransitionDataServices = null,
 099            IScopeFilterValuesService? scopeFilterValuesService = null,
 0100            IDataScopeService? dataScopeService = null,
 0101            OzonOrderCancellationService? ozonCancellationService = null)
 102        {
 0103            Context = context;
 0104            AppStatusServices = appStatusServices;
 0105            OrderServices = orderServices;
 0106            TransactionManager = transactionManager;
 0107            TransactionCache = transactionCache;
 0108            SupplierDataServices = supplierDataServices;
 0109            UserCacheService = userCacheService;
 0110            CurrencyRatesViewBagService = currencyRatesViewBagService;
 0111            CacheUpdater = cacheUpdater;
 0112            OrderCache = orderCache;
 0113            OneCDataManager = oneCDataManager;
 0114            WarehouseDataServices = warehouseDataServices;
 0115            BitrixStockRepository = bitrixStockRepository;
 0116            ProxyHttpClientService = proxyHttpClientService;
 0117            OneCTransferManager = oneCTransferManager;
 0118            OrderCartServices = orderCartServices;
 0119            WarehouseMappingDataServices = warehouseMappingDataServices;
 0120            ExpenseTypeDataServices = expenseTypeDataServices;
 0121            OrderExtraExpenseDataServices = orderExtraExpenseDataServices;
 0122            StatusTransitionDataServices = statusTransitionDataServices;
 0123            ScopeFilterValuesService = scopeFilterValuesService;
 0124            DataScopeService = dataScopeService;
 0125            OzonCancellationService = ozonCancellationService;
 0126        }
 127
 128        private WebServices.Transactions.TransactionProgressReporter? _progressReporter;
 129        private string? _progressKey;
 130        private bool _progressKeyResolved;
 131
 132        /// <summary>Ключ прогресса, который страница передала вместе с формой.</summary>
 133        private string? ProgressKey
 134        {
 135            get
 136            {
 0137                if (_progressKeyResolved) return _progressKey;
 0138                _progressKeyResolved = true;
 0139                if (Request.HasFormContentType && Request.Form.TryGetValue("progressKey", out var value))
 0140                    _progressKey = value.FirstOrDefault();
 0141                return _progressKey;
 142            }
 143        }
 144
 145        /// <summary>
 146        /// Сообщить странице о реально пройденном этапе проведения: оверлей рисует их по событиям,
 147        /// а не по секундомеру.
 148        /// </summary>
 149        protected Task ReportStageAsync(string code, string title, string? detail = null)
 150        {
 0151            if (string.IsNullOrWhiteSpace(ProgressKey))
 0152                return Task.CompletedTask;
 153
 0154            _progressReporter ??= HttpContext.RequestServices
 0155                .GetService<WebServices.Transactions.TransactionProgressReporter>();
 0156            return _progressReporter?.StageAsync(ProgressKey, code, title, detail) ?? Task.CompletedTask;
 157        }
 158
 159        protected Task ReportProgressFinishedAsync(bool success, string? message = null)
 160        {
 0161            if (string.IsNullOrWhiteSpace(ProgressKey))
 0162                return Task.CompletedTask;
 163
 0164            _progressReporter ??= HttpContext.RequestServices
 0165                .GetService<WebServices.Transactions.TransactionProgressReporter>();
 0166            return _progressReporter?.CompleteAsync(ProgressKey, success, message) ?? Task.CompletedTask;
 167        }
 168
 169        /// <summary>
 170        /// Абстрактный метод для получения конфигурации транзакции
 171        /// </summary>
 172        protected abstract TransactionConfig GetTransactionConfig();
 173
 174        /// <summary>
 175        /// Базовый GET метод для подготовки транзакции
 176        /// </summary>
 177        protected async Task<IActionResult> CreateTransactionBase(string ids, int page)
 178        {
 0179            return await CreateTransactionBase(ids, page, GetTransactionConfig());
 0180        }
 181
 182        /// <summary>
 183        /// Подготовка транзакции с явной конфигурацией (например, массовый сценарий из файла).
 184        /// </summary>
 185        protected async Task<IActionResult> CreateTransactionBase(string ids, int page, TransactionConfig config)
 186        {
 0187            ViewBag.ClearPurchasePriceOnLoad = config.ClearPurchasePriceOnLoad;
 188
 0189            List<StatusTransition>? transitions = null;
 0190            HashSet<int>? validSourceStatusIds = null;
 0191            Dictionary<int, (AppStatus ToStatus, StatusTransition Transition)>? fromStatusToTarget = null;
 192
 0193            if (config.UseStatusTransitions && StatusTransitionDataServices != null)
 194            {
 0195                transitions = await StatusTransitionDataServices.GetByTransactionType(config.TransactionType);
 0196                if (transitions.Any())
 197                {
 0198                    validSourceStatusIds = transitions.Select(t => t.FromStatusId).ToHashSet();
 0199                    fromStatusToTarget = transitions
 0200                        .GroupBy(t => t.FromStatusId)
 0201                        .ToDictionary(g => g.Key, g => (g.First().ToStatus!, g.First()));
 202                }
 203            }
 204
 0205            AppStatus? primaryStatus = null;
 0206            var filteredStatuses = new List<AppStatus>();
 207
 0208            if (transitions != null && transitions.Any())
 209            {
 0210                var allToStatuses = transitions.Select(t => t.ToStatus!).Where(s => s != null).Distinct().ToList();
 0211                var allFromStatuses = transitions.Select(t => t.FromStatus!).Where(s => s != null).Distinct().ToList();
 0212                filteredStatuses = allToStatuses.Union(allFromStatuses).GroupBy(s => s!.Id).Select(g => g.First()!)
 0213                    .OrderBy(s => s.Name).ToList();
 0214                primaryStatus = filteredStatuses.FirstOrDefault();
 215
 216                // Добавляем дополнительные статусы если указаны флаги
 0217                if (config.IncludeCancelledOrders)
 218                {
 0219                    var cancelledStatus = await GetOrCreateAppStatus("Отменен");
 0220                    if (!filteredStatuses.Any(s => s.Id == cancelledStatus.Id))
 0221                        filteredStatuses.Add(cancelledStatus);
 0222                }
 223
 0224                if (config.IncludeToCancellationStatus)
 225                {
 0226                    var toCancellationStatus = await GetOrCreateAppStatus("К отмене");
 0227                    if (!filteredStatuses.Any(s => s.Id == toCancellationStatus.Id))
 0228                        filteredStatuses.Add(toCancellationStatus);
 0229                }
 230
 0231                if (config.IncludeNotReadyStatus)
 232                {
 0233                    var notReadyStatus = await GetOrCreateAppStatus("Не готов");
 0234                    if (!filteredStatuses.Any(s => s.Id == notReadyStatus.Id))
 0235                        filteredStatuses.Add(notReadyStatus);
 0236                }
 237            }
 238            else
 239            {
 0240                if (!string.IsNullOrEmpty(config.PrimaryStatusName))
 0241                    primaryStatus = await GetOrCreateAppStatus(config.PrimaryStatusName);
 0242                foreach (var statusName in config.AdditionalStatusNames)
 243                {
 0244                    var status = await GetOrCreateAppStatus(statusName);
 0245                    filteredStatuses.Add(status);
 246                }
 247
 0248                if (primaryStatus != null)
 0249                    filteredStatuses.Insert(0, primaryStatus);
 0250                if (config.IncludeCancelledOrders)
 251                {
 0252                    var cancelledStatus = await AppStatusServices.GetAppStatusAsync(new AppStatus { Name = "Отменен" });
 0253                    if (cancelledStatus != null)
 0254                        filteredStatuses.Add(cancelledStatus);
 255                }
 256
 0257                if (config.IncludeToCancellationStatus)
 258                {
 0259                    var toCancellationStatus = await GetOrCreateAppStatus("К отмене");
 0260                    filteredStatuses.Add(toCancellationStatus);
 261                }
 262
 0263                if (string.IsNullOrEmpty(config.PrimaryStatusName))
 264                {
 0265                    var allStatuses = await AppStatusServices.GetAppStatuses();
 0266                    filteredStatuses = allStatuses.OrderBy(s => s.Name).ToList();
 267                }
 268            }
 269
 0270            filteredStatuses = filteredStatuses.GroupBy(s => s.Id).Select(g => g.First()).ToList();
 0271            filteredStatuses = await FilterByOrderScopeIdsAsync(filteredStatuses, s => s.Id, "AppStatusId");
 272
 0273            ViewBag.AppStatuses = new SelectList(filteredStatuses, "Id", "Name");
 0274            ViewBag.StatusColors = filteredStatuses.ToDictionary(s => s.Id.ToString(), s => s.GetStatusColor());
 275
 276            // Проверка на пустой или null ids
 0277            if (string.IsNullOrWhiteSpace(ids))
 278            {
 0279                SetTransactionErrorViewData(
 0280                    TransactionErrorCode.NoOrdersSelected,
 0281                    "Ни один заказ не был выбран. Выберите заказы с помощью галочек.");
 282
 0283                var emptyUser = (await UserCacheService.GetCachedUserAsync(User))!;
 0284                var emptyRates = await CurrencyRatesViewBagService.GetRatesAsync();
 0285                var emptyArmtekSettings = await HttpContext.RequestServices
 0286                    .GetRequiredService<ApiSupplierSettingsService>()
 0287                    .GetArmtekAsync(HttpContext.RequestAborted);
 0288                var emptyArmtekBySettings = await HttpContext.RequestServices
 0289                    .GetRequiredService<ApiSupplierSettingsService>()
 0290                    .GetArmtekByAsync(HttpContext.RequestAborted);
 0291                var emptyShateMSettings = await HttpContext.RequestServices
 0292                    .GetRequiredService<ApiSupplierSettingsService>()
 0293                    .GetShateMAsync(HttpContext.RequestAborted);
 0294                var emptyAvdSettings = await HttpContext.RequestServices
 0295                    .GetRequiredService<ApiSupplierSettingsService>()
 0296                    .GetAvdAsync(HttpContext.RequestAborted);
 0297                var emptyBergSettings = await HttpContext.RequestServices
 0298                    .GetRequiredService<ApiSupplierSettingsService>()
 0299                    .GetBergAsync(HttpContext.RequestAborted);
 0300                var emptyMotexSettings = await HttpContext.RequestServices
 0301                    .GetRequiredService<ApiSupplierSettingsService>()
 0302                    .GetMotexAsync(HttpContext.RequestAborted);
 0303                var emptyMlAutoSettings = await HttpContext.RequestServices
 0304                    .GetRequiredService<ApiSupplierSettingsService>()
 0305                    .GetMlAutoAsync(HttpContext.RequestAborted);
 0306                var emptyMlAutoRuSettings = await HttpContext.RequestServices
 0307                    .GetRequiredService<ApiSupplierSettingsService>()
 0308                    .GetMlAutoRuAsync(HttpContext.RequestAborted);
 0309                var emptyForumAutoSettings = await HttpContext.RequestServices
 0310                    .GetRequiredService<ApiSupplierSettingsService>()
 0311                    .GetForumAutoAsync(HttpContext.RequestAborted);
 0312                var emptyForumAutoInterpartsSettings = await HttpContext.RequestServices
 0313                    .GetRequiredService<ApiSupplierSettingsService>()
 0314                    .GetForumAutoInterpartsAsync(HttpContext.RequestAborted);
 0315                var emptyForumAutoPiterSettings = await HttpContext.RequestServices
 0316                    .GetRequiredService<ApiSupplierSettingsService>()
 0317                    .GetForumAutoPiterAsync(HttpContext.RequestAborted);
 0318                var emptyForumAutoRostovSettings = await HttpContext.RequestServices
 0319                    .GetRequiredService<ApiSupplierSettingsService>()
 0320                    .GetForumAutoRostovAsync(HttpContext.RequestAborted);
 0321                var emptyForumAutoIstraSettings = await HttpContext.RequestServices
 0322                    .GetRequiredService<ApiSupplierSettingsService>()
 0323                    .GetForumAutoIstraAsync(HttpContext.RequestAborted);
 0324                var emptyFavoritPartsSettings = await HttpContext.RequestServices
 0325                    .GetRequiredService<ApiSupplierSettingsService>()
 0326                    .GetFavoritPartsAsync(HttpContext.RequestAborted);
 0327                var emptyFavoritPartsIstraSettings = await HttpContext.RequestServices
 0328                    .GetRequiredService<ApiSupplierSettingsService>()
 0329                    .GetFavoritPartsIstraAsync(HttpContext.RequestAborted);
 0330                var emptyFavoritPartsRostovSettings = await HttpContext.RequestServices
 0331                    .GetRequiredService<ApiSupplierSettingsService>()
 0332                    .GetFavoritPartsRostovAsync(HttpContext.RequestAborted);
 0333                var emptyMoskvorechieIstraSettings = await HttpContext.RequestServices
 0334                    .GetRequiredService<ApiSupplierSettingsService>()
 0335                    .GetMoskvorechieIstraAsync(HttpContext.RequestAborted);
 0336                var emptyProfitLigaSettings = await HttpContext.RequestServices
 0337                    .GetRequiredService<ApiSupplierSettingsService>()
 0338                    .GetProfitLigaAsync(HttpContext.RequestAborted);
 0339                var emptyNikeiSettings = await HttpContext.RequestServices
 0340                    .GetRequiredService<ApiSupplierSettingsService>()
 0341                    .GetNikeiAsync(HttpContext.RequestAborted);
 0342                var emptyMikadoSettings = await HttpContext.RequestServices
 0343                    .GetRequiredService<ApiSupplierSettingsService>()
 0344                    .GetMikadoMskHod20Async(HttpContext.RequestAborted);
 0345                var emptyZZapMoscowSettings = await HttpContext.RequestServices
 0346                    .GetRequiredService<ApiSupplierSettingsService>()
 0347                    .GetZZapMoscowAsync(HttpContext.RequestAborted);
 0348                var emptyJapartsSettings = await HttpContext.RequestServices
 0349                    .GetRequiredService<ApiSupplierSettingsService>()
 0350                    .GetJapartsAsync(HttpContext.RequestAborted);
 0351                var emptyViewModel = new MultiplayEditOrderViewModel
 0352                {
 0353                    RedirectPage = page,
 0354                    Orders = new List<Order>(),
 0355                    User = emptyUser,
 0356                    Suppliers = (await SupplierDataServices.GetSuppliers()).OrderBy(s => s.Name).ToList(),
 0357                    RateUSD = emptyRates.RateUsd,
 0358                    RateEUR = emptyRates.RateEur,
 0359                    RateBYN = emptyRates.RateByn,
 0360                    ArmtekQuotationEnabled = emptyArmtekSettings.IsQuotationEnabled,
 0361                    ArmtekByQuotationEnabled = emptyArmtekBySettings.IsQuotationEnabled,
 0362                    ShateMQuotationEnabled = emptyShateMSettings.IsQuotationEnabled,
 0363                    AvdQuotationEnabled = emptyAvdSettings.IsQuotationEnabled,
 0364                    BergQuotationEnabled = emptyBergSettings.IsQuotationEnabled,
 0365                    MotexQuotationEnabled = emptyMotexSettings.IsQuotationEnabled,
 0366                    MlAutoQuotationEnabled = emptyMlAutoSettings.IsQuotationEnabled,
 0367                    MlAutoRuQuotationEnabled = emptyMlAutoRuSettings.IsQuotationEnabled,
 0368                    ForumAutoQuotationEnabled = emptyForumAutoSettings.IsQuotationEnabled,
 0369                    ForumAutoInterpartsQuotationEnabled = emptyForumAutoInterpartsSettings.IsQuotationEnabled,
 0370                    ForumAutoPiterQuotationEnabled = emptyForumAutoPiterSettings.IsQuotationEnabled,
 0371                    ForumAutoRostovQuotationEnabled = emptyForumAutoRostovSettings.IsQuotationEnabled,
 0372                    ForumAutoIstraQuotationEnabled = emptyForumAutoIstraSettings.IsQuotationEnabled,
 0373                    ForumAutoDisplayName = emptyForumAutoSettings.DisplayName ?? ApiSupplierSettingsService.GetDefaultDi
 0374                    ForumAutoInterpartsDisplayName = emptyForumAutoInterpartsSettings.DisplayName ?? ApiSupplierSettings
 0375                    ForumAutoPiterDisplayName = emptyForumAutoPiterSettings.DisplayName ?? ApiSupplierSettingsService.Ge
 0376                    ForumAutoRostovDisplayName = emptyForumAutoRostovSettings.DisplayName ?? ApiSupplierSettingsService.
 0377                    ForumAutoIstraDisplayName = emptyForumAutoIstraSettings.DisplayName ?? ApiSupplierSettingsService.Ge
 0378                    FavoritPartsQuotationEnabled = emptyFavoritPartsSettings.IsQuotationEnabled,
 0379                    FavoritPartsIstraQuotationEnabled = emptyFavoritPartsIstraSettings.IsQuotationEnabled,
 0380                    FavoritPartsRostovQuotationEnabled = emptyFavoritPartsRostovSettings.IsQuotationEnabled,
 0381                    MoskvorechieIstraQuotationEnabled = emptyMoskvorechieIstraSettings.IsQuotationEnabled,
 0382                    ProfitLigaQuotationEnabled = emptyProfitLigaSettings.IsQuotationEnabled,
 0383                    NikeiQuotationEnabled = emptyNikeiSettings.IsQuotationEnabled,
 0384                    MikadoMskHod20QuotationEnabled = emptyMikadoSettings.IsQuotationEnabled,
 0385                    ZZapMoscowQuotationEnabled = emptyZZapMoscowSettings.IsQuotationEnabled,
 0386                    JapartsQuotationEnabled = emptyJapartsSettings.IsQuotationEnabled,
 0387                    ApiSupplierAllowedWarehouses = HttpContext.RequestServices.GetService<ApiSupplierWarehouseService>()
 0388                        ? await emptyWhService.GetAllAllowedWarehouseIdsByProviderAsync(HttpContext.RequestAborted)
 0389                        : new Dictionary<string, List<int>>(),
 0390                    UniqueArticles = new Dictionary<string, int>(),
 0391                    UniqueDeliveryCitys = new Dictionary<string, int>(),
 0392                    UniqueNumbers = new Dictionary<string, int>(),
 0393                    AppStatus = primaryStatus ?? new AppStatus()
 0394                };
 395
 0396                if (emptyUser.AccessProfileId != null)
 397                {
 0398                    emptyViewModel.User.AccessProfile = await Context.AccessProfiles.FindAsync(emptyUser.AccessProfileId
 399                }
 400
 0401                return View(config.ViewName, emptyViewModel);
 402            }
 403
 0404            int[] idArray = ids.Split(',', StringSplitOptions.RemoveEmptyEntries)
 0405                .Select(s => int.TryParse(s.Trim(), out var v) ? v : (int?)null)
 0406                .Where(v => v.HasValue).Select(v => v!.Value).ToArray();
 0407            idArray = await FilterOrderIdsByDataScopeAsync(idArray);
 408            // Уникальные id в порядке первого вхождения — один заказ = одна строка, без дубликатов
 0409            int[] orderIds = idArray.Distinct().ToArray();
 0410            List<Order> ordersToTransaction = new();
 0411            string? errorReason = null;
 0412            var errorCode = TransactionErrorCode.Unknown;
 413
 414            // Удаляем заказы из корзины одним запросом вместо N
 0415            if (OrderCartServices != null)
 416            {
 0417                await OrderCartServices.DeleteByOrderIds(orderIds);
 418            }
 419
 420            // ВАЖНО: Загружаем заказы БЕЗ ТРЕКИНГА — изменения статусов для отображения
 421            // НЕ сохранятся в БД при отмене транзакции. CartItemStatus подгружается пакетно.
 422            // Репозиторий возвращает уникальные заказы (дедупликация по Id из-за вложенных моделей).
 0423            var orders = await OrderServices.GetOrdersNoTracking(orderIds);
 0424            var ordersById = orders.ToDictionary(o => o.Id);
 425
 426            // Итерация по orderIds сохраняет порядок выбора и гарантирует одну строку на заказ
 0427            foreach (var orderId in orderIds)
 428            {
 0429                if (!ordersById.TryGetValue(orderId, out var order))
 430                    continue;
 431
 0432                if (validSourceStatusIds != null && fromStatusToTarget != null)
 433                {
 0434                    var fromId = order.AppStatusId ?? 0;
 0435                    if (!validSourceStatusIds.Contains(fromId))
 436                    {
 0437                        var allowedNames = string.Join("', '",
 0438                            transitions!.Select(t => t.FromStatus?.Name).Where(n => !string.IsNullOrEmpty(n))
 0439                                .Distinct());
 0440                        errorReason = $"Заказ {order.ShipmentNumber} имеет статус '{order.AppStatus?.Name}', " +
 0441                                      $"а должен быть одним из: '{allowedNames}'.";
 0442                        errorCode = TransactionErrorCode.InvalidSourceStatus;
 0443                        continue;
 444                    }
 445
 0446                    if (fromStatusToTarget.TryGetValue(fromId, out var target))
 447                    {
 448                        // Устанавливаем целевой статус для ОТОБРАЖЕНИЯ в форме
 449                        // Заказ без трекинга - изменения НЕ сохранятся в БД
 0450                        order.AppStatusId = target.ToStatus.Id;
 0451                        order.AppStatus = target.ToStatus;
 452                    }
 453                }
 0454                else if (config.RequiredSourceStatuses.Any())
 455                {
 0456                    if (!config.RequiredSourceStatuses.Contains(order.AppStatus?.Name ?? ""))
 457                    {
 0458                        errorReason = $"Заказ {order.ShipmentNumber} имеет статус '{order.AppStatus?.Name}', " +
 0459                                      $"а должен быть '{string.Join("' или '", config.RequiredSourceStatuses)}'.";
 0460                        errorCode = TransactionErrorCode.InvalidSourceStatus;
 0461                        continue;
 462                    }
 463                }
 464
 0465                var orderFilter = config.OrderFilter ?? TransactionConfig.StandardOrderFilter;
 0466                if (!orderFilter(order))
 467                    continue;
 468
 0469                order.UpdatedBy = User.Identity?.Name;
 0470                ordersToTransaction.Add(order);
 471            }
 472
 0473            if (!ordersToTransaction.Any() && errorReason == null)
 474            {
 0475                var transactionName = config.TransactionType.GetTransactionDisplayName();
 0476                List<string>? allowedFromStatuses = null;
 0477                if (transitions != null && transitions.Any())
 478                {
 0479                    allowedFromStatuses = transitions
 0480                        .Select(t => t.FromStatus?.Name)
 0481                        .OfType<string>()
 0482                        .Where(n => !string.IsNullOrWhiteSpace(n))
 0483                        .Distinct()
 0484                        .ToList();
 485                }
 0486                else if (config.RequiredSourceStatuses.Any())
 487                {
 0488                    allowedFromStatuses = config.RequiredSourceStatuses;
 489                }
 490
 491                // Заказы дошли до этой точки (не найдены в ordersToTransaction), значит все они были
 492                // отсеяны либо проверкой статуса, либо OrderFilter — показываем юзеру их РЕАЛЬНЫЙ
 493                // текущий статус, а не только абстрактный список допустимых (иначе непонятно, почему
 494                // заказ, чей статус вроде бы есть в списке разрешённых, всё равно не прошёл).
 0495                var currentStatusLines = ordersById.Values
 0496                    .Take(10)
 0497                    .Select(o => $"Заказ {o.ShipmentNumber} сейчас в статусе «{o.AppStatus?.Name ?? "не указан"}»")
 0498                    .ToList();
 0499                var moreCount = ordersById.Count - currentStatusLines.Count;
 500
 0501                var reasonParts = new List<string>();
 0502                if (currentStatusLines.Count > 0)
 503                {
 0504                    var line = string.Join(", ", currentStatusLines);
 0505                    if (moreCount > 0)
 0506                        line += $" и ещё {moreCount}";
 0507                    reasonParts.Add(line + ".");
 508                }
 0509                reasonParts.Add(allowedFromStatuses is { Count: > 0 }
 0510                    ? $"Допустимые исходные статусы для «{transactionName}»: «{string.Join("», «", allowedFromStatuses)}
 0511                    : $"Проверьте настройки переходов статусов (StatusTransition) для «{transactionName}».");
 512
 0513                errorReason = string.Join(" ", reasonParts);
 0514                errorCode = TransactionErrorCode.NoOrdersForTransaction;
 515            }
 516
 0517            SetTransactionErrorViewData(errorCode, errorReason);
 518
 0519            var conditionsMessage = transitions != null && transitions.Any()
 0520                ? string.Join("<br/>", transitions
 0521                    .Select(t => t.Conditions)
 0522                    .Where(c => !string.IsNullOrWhiteSpace(c))
 0523                    .Select(c => System.Net.WebUtility.HtmlEncode(c!.Trim()))
 0524                    .Distinct())
 0525                : null;
 0526            ViewData["TransactionConditionsMessage"] = conditionsMessage;
 527
 528            // Загружаем данные из Bitrix если нужно
 0529            if (config.LoadBitrixData && BitrixStockRepository != null)
 530            {
 0531                await BitrixStockRepository.SetPricesForOrdersAsyncFromBitrix(ordersToTransaction);
 0532                var stockData = await BitrixStockRepository.SetStocksForOrdersAsyncFromBitrix(ordersToTransaction);
 0533                ViewData["StockData"] = stockData;
 534            }
 535
 536            // === ОПТИМИЗАЦИЯ: Параллелизуем non-DB вызовы, DB-запросы выполняем последовательно ===
 537            // Non-DB вызовы (API, кэш) — можно запускать параллельно
 0538            var userTask = UserCacheService.GetCachedUserAsync(User);
 0539            var rateTasks = CurrencyRatesViewBagService.StartFetch();
 0540            var zasylTask = OneCDataManager != null
 0541                ? OneCDataManager.GetWarehouseListAsync()
 0542                : Task.FromResult(new List<global::Services.ApiServices._1CApi.Models.WarehouseResponse>());
 543
 544            // DB-запросы — последовательно (один DbContext не поддерживает параллельные запросы)
 0545            var suppliers = await SupplierDataServices.GetSuppliers();
 0546            var armtekSettings = await HttpContext.RequestServices
 0547                .GetRequiredService<ApiSupplierSettingsService>()
 0548                .GetArmtekAsync(HttpContext.RequestAborted);
 0549            var armtekBySettings = await HttpContext.RequestServices
 0550                .GetRequiredService<ApiSupplierSettingsService>()
 0551                .GetArmtekByAsync(HttpContext.RequestAborted);
 0552            var shateMSettings = await HttpContext.RequestServices
 0553                .GetRequiredService<ApiSupplierSettingsService>()
 0554                .GetShateMAsync(HttpContext.RequestAborted);
 0555            var avdSettings = await HttpContext.RequestServices
 0556                .GetRequiredService<ApiSupplierSettingsService>()
 0557                .GetAvdAsync(HttpContext.RequestAborted);
 0558            var bergSettings = await HttpContext.RequestServices
 0559                .GetRequiredService<ApiSupplierSettingsService>()
 0560                .GetBergAsync(HttpContext.RequestAborted);
 0561            var motexSettings = await HttpContext.RequestServices
 0562                .GetRequiredService<ApiSupplierSettingsService>()
 0563                .GetMotexAsync(HttpContext.RequestAborted);
 0564            var mlAutoSettings = await HttpContext.RequestServices
 0565                .GetRequiredService<ApiSupplierSettingsService>()
 0566                .GetMlAutoAsync(HttpContext.RequestAborted);
 0567            var mlAutoRuSettings = await HttpContext.RequestServices
 0568                .GetRequiredService<ApiSupplierSettingsService>()
 0569                .GetMlAutoRuAsync(HttpContext.RequestAborted);
 0570            var forumAutoSettings = await HttpContext.RequestServices
 0571                .GetRequiredService<ApiSupplierSettingsService>()
 0572                .GetForumAutoAsync(HttpContext.RequestAborted);
 0573            var forumAutoInterpartsSettings = await HttpContext.RequestServices
 0574                .GetRequiredService<ApiSupplierSettingsService>()
 0575                .GetForumAutoInterpartsAsync(HttpContext.RequestAborted);
 0576            var forumAutoPiterSettings = await HttpContext.RequestServices
 0577                .GetRequiredService<ApiSupplierSettingsService>()
 0578                .GetForumAutoPiterAsync(HttpContext.RequestAborted);
 0579            var forumAutoRostovSettings = await HttpContext.RequestServices
 0580                .GetRequiredService<ApiSupplierSettingsService>()
 0581                .GetForumAutoRostovAsync(HttpContext.RequestAborted);
 0582            var forumAutoIstraSettings = await HttpContext.RequestServices
 0583                .GetRequiredService<ApiSupplierSettingsService>()
 0584                .GetForumAutoIstraAsync(HttpContext.RequestAborted);
 0585            var favoritPartsSettings = await HttpContext.RequestServices
 0586                .GetRequiredService<ApiSupplierSettingsService>()
 0587                .GetFavoritPartsAsync(HttpContext.RequestAborted);
 0588            var favoritPartsIstraSettings = await HttpContext.RequestServices
 0589                .GetRequiredService<ApiSupplierSettingsService>()
 0590                .GetFavoritPartsIstraAsync(HttpContext.RequestAborted);
 0591            var favoritPartsRostovSettings = await HttpContext.RequestServices
 0592                .GetRequiredService<ApiSupplierSettingsService>()
 0593                .GetFavoritPartsRostovAsync(HttpContext.RequestAborted);
 0594            var moskvorechieIstraSettings = await HttpContext.RequestServices
 0595                .GetRequiredService<ApiSupplierSettingsService>()
 0596                .GetMoskvorechieIstraAsync(HttpContext.RequestAborted);
 0597            var profitLigaSettings = await HttpContext.RequestServices
 0598                .GetRequiredService<ApiSupplierSettingsService>()
 0599                .GetProfitLigaAsync(HttpContext.RequestAborted);
 0600            var nikeiSettings = await HttpContext.RequestServices
 0601                .GetRequiredService<ApiSupplierSettingsService>()
 0602                .GetNikeiAsync(HttpContext.RequestAborted);
 0603            var mikadoSettings = await HttpContext.RequestServices
 0604                .GetRequiredService<ApiSupplierSettingsService>()
 0605                .GetMikadoMskHod20Async(HttpContext.RequestAborted);
 0606            var zzapMoscowSettings = await HttpContext.RequestServices
 0607                .GetRequiredService<ApiSupplierSettingsService>()
 0608                .GetZZapMoscowAsync(HttpContext.RequestAborted);
 0609            var japartsSettings = await HttpContext.RequestServices
 0610                .GetRequiredService<ApiSupplierSettingsService>()
 0611                .GetJapartsAsync(HttpContext.RequestAborted);
 0612            var (uniqueArticles, uniqueCities, uniqueNumbers) =
 0613                await GetUniqueCountsForOrdersAsync(ordersToTransaction);
 0614            var warehouses = WarehouseDataServices != null
 0615                ? await WarehouseDataServices.GetWarehouses()
 0616                : new List<Warehouse>();
 0617            suppliers = await FilterByOrderScopeIdsAsync(suppliers, s => s.Id, "SupplierId");
 0618            warehouses = await FilterByOrderScopeIdsAsync(warehouses, w => w.Id, "ShipmentWarehouseId");
 0619            var expenseTypes = ExpenseTypeDataServices != null
 0620                ? await ExpenseTypeDataServices.GetExpenseTypes()
 0621                : new List<ExpenseType>();
 0622            var expenses = OrderExtraExpenseDataServices != null && ordersToTransaction.Any()
 0623                ? await OrderExtraExpenseDataServices.GetByOrderIds(ordersToTransaction.Select(o => o.Id).ToList())
 0624                : new Dictionary<int, List<OrderExtraExpense>>();
 625
 626            // Ожидаем non-DB задачи (скорее всего уже завершились пока шли DB-запросы)
 0627            await Task.WhenAll(userTask, rateTasks.Usd, rateTasks.Eur, rateTasks.Byn, zasylTask);
 628
 0629            var pageUser = userTask.Result!;
 0630            var rates = rateTasks.ToSnapshot();
 0631            var pageViewModel = new MultiplayEditOrderViewModel
 0632            {
 0633                RedirectPage = page,
 0634                Orders = ordersToTransaction.OrderBy(ot => ot.ShipmentNumber).ToList(),
 0635                User = pageUser,
 0636                Suppliers = suppliers.OrderBy(s => s.Name).ToList(),
 0637                RateUSD = rates.RateUsd,
 0638                RateEUR = rates.RateEur,
 0639                RateBYN = rates.RateByn,
 0640                ArmtekQuotationEnabled = armtekSettings.IsQuotationEnabled,
 0641                ArmtekByQuotationEnabled = armtekBySettings.IsQuotationEnabled,
 0642                ShateMQuotationEnabled = shateMSettings.IsQuotationEnabled,
 0643                AvdQuotationEnabled = avdSettings.IsQuotationEnabled,
 0644                BergQuotationEnabled = bergSettings.IsQuotationEnabled,
 0645                MotexQuotationEnabled = motexSettings.IsQuotationEnabled,
 0646                MlAutoQuotationEnabled = mlAutoSettings.IsQuotationEnabled,
 0647                MlAutoRuQuotationEnabled = mlAutoRuSettings.IsQuotationEnabled,
 0648                ForumAutoQuotationEnabled = forumAutoSettings.IsQuotationEnabled,
 0649                ForumAutoInterpartsQuotationEnabled = forumAutoInterpartsSettings.IsQuotationEnabled,
 0650                ForumAutoPiterQuotationEnabled = forumAutoPiterSettings.IsQuotationEnabled,
 0651                ForumAutoRostovQuotationEnabled = forumAutoRostovSettings.IsQuotationEnabled,
 0652                ForumAutoIstraQuotationEnabled = forumAutoIstraSettings.IsQuotationEnabled,
 0653                ForumAutoDisplayName = forumAutoSettings.DisplayName ?? ApiSupplierSettingsService.GetDefaultDisplayName
 0654                ForumAutoInterpartsDisplayName = forumAutoInterpartsSettings.DisplayName ?? ApiSupplierSettingsService.G
 0655                ForumAutoPiterDisplayName = forumAutoPiterSettings.DisplayName ?? ApiSupplierSettingsService.GetDefaultD
 0656                ForumAutoRostovDisplayName = forumAutoRostovSettings.DisplayName ?? ApiSupplierSettingsService.GetDefaul
 0657                ForumAutoIstraDisplayName = forumAutoIstraSettings.DisplayName ?? ApiSupplierSettingsService.GetDefaultD
 0658                FavoritPartsQuotationEnabled = favoritPartsSettings.IsQuotationEnabled,
 0659                FavoritPartsIstraQuotationEnabled = favoritPartsIstraSettings.IsQuotationEnabled,
 0660                FavoritPartsRostovQuotationEnabled = favoritPartsRostovSettings.IsQuotationEnabled,
 0661                MoskvorechieIstraQuotationEnabled = moskvorechieIstraSettings.IsQuotationEnabled,
 0662                ProfitLigaQuotationEnabled = profitLigaSettings.IsQuotationEnabled,
 0663                NikeiQuotationEnabled = nikeiSettings.IsQuotationEnabled,
 0664                MikadoMskHod20QuotationEnabled = mikadoSettings.IsQuotationEnabled,
 0665                ZZapMoscowQuotationEnabled = zzapMoscowSettings.IsQuotationEnabled,
 0666                JapartsQuotationEnabled = japartsSettings.IsQuotationEnabled,
 0667                ApiSupplierAllowedWarehouses = HttpContext.RequestServices.GetService<ApiSupplierWarehouseService>() is 
 0668                    ? await whService.GetAllAllowedWarehouseIdsByProviderAsync(HttpContext.RequestAborted)
 0669                    : new Dictionary<string, List<int>>(),
 0670                UniqueArticles = uniqueArticles,
 0671                UniqueDeliveryCitys = uniqueCities,
 0672                UniqueNumbers = uniqueNumbers,
 0673                AppStatus = primaryStatus ?? filteredStatuses.FirstOrDefault() ?? new AppStatus(),
 0674                ZasylValues = OneCDataManager != null
 0675                    ? await FilterWarehouseNamesByOrderScopeAsync(
 0676                        zasylTask.Result.Select(w => w.Name).Where(n => !string.IsNullOrWhiteSpace(n)).Select(n => n!))
 0677                    : new List<string>(),
 0678                Warehouses = warehouses
 0679            };
 680
 681            // Старт трекинга времени проведения: при открытии страницы создания транзакции.
 0682            var trackingService = HttpContext.RequestServices.GetService<ITransactionTimeTrackingService>();
 0683            if (trackingService != null && pageViewModel.Orders.Any())
 684            {
 0685                var accessProfileId = pageViewModel.User?.AccessProfileId;
 0686                var clientIp = HttpContext.Connection.RemoteIpAddress?.ToString();
 0687                var userAgent = Request.Headers.UserAgent.ToString();
 0688                var sourceUrl = $"{Request.Path}{Request.QueryString}";
 0689                pageViewModel.TrackingKey = await trackingService.StartSessionAsync(
 0690                    User.Identity?.Name ?? "unknown",
 0691                    accessProfileId,
 0692                    config.TransactionType,
 0693                    pageViewModel.Orders.Select(o => o.Id),
 0694                    sourceUrl,
 0695                    clientIp,
 0696                    userAgent);
 697            }
 698
 0699            pageViewModel.OrderListFieldReadAccess = await BuildOrderListFieldReadMapAsync(pageUser.Id);
 700
 701            // Типы расходов
 0702            if (ExpenseTypeDataServices != null)
 703            {
 0704                ViewBag.ExpenseTypes = expenseTypes.Where(et => et.IsActive).OrderBy(et => et.Name).ToList();
 705            }
 706
 707            // Существующие расходы для заказов
 0708            ViewBag.ExistingExpenses = expenses;
 709
 710            // Применяем кастомную настройку ViewModel если задана
 0711            config.ConfigureViewModel?.Invoke(pageViewModel);
 0712            if (config.ConfigureViewModelAsync != null)
 0713                await config.ConfigureViewModelAsync(pageViewModel);
 714
 715            // Панель "Отменить в Ozon", раскрывающаяся при выборе статуса "Отменен" в per-row select
 0716            if (config.SupportsOzonCancelPanel && OzonCancellationService != null && pageViewModel.Orders.Count > 0)
 717            {
 0718                pageViewModel.OzonPostingCancelReasons =
 0719                    await OzonCancellationService.LoadCancelReasonsAsync(pageViewModel.Orders);
 0720                ViewBag.OzonCancelPanelLinkedToStatusSelect = true;
 721            }
 722
 0723            if (config.LoadWarehouseListForSelector && OneCDataManager != null)
 724            {
 0725                var bitrixWarehouseNames = zasylTask.Result
 0726                    .Select(w => w.Name)
 0727                    .Where(n => !string.IsNullOrWhiteSpace(n))
 0728                    .Select(n => n!)
 0729                    .OrderBy(n => n)
 0730                    .ToList();
 0731                ViewData["BitrixWarehousesList"] = await FilterBitrixWarehouseNamesByScope(bitrixWarehouseNames);
 732            }
 733
 0734            if (config.ClearPurchasePriceOnLoad && pageViewModel.Orders.Count > 0)
 735            {
 0736                var warehouseC0ByOrderId = await LoadWarehouseStockC0MapAsync(pageViewModel.Orders);
 0737                pageViewModel.WarehouseStockC0ByOrderId = warehouseC0ByOrderId;
 0738                ApplyWarehouseStockC0ToOrders(pageViewModel.Orders, warehouseC0ByOrderId);
 739            }
 740
 741            // Запускаем фоновое кэширование
 0742            RunBackgroundCacheTask(pageViewModel.Orders);
 743
 0744            return View(config.ViewName, pageViewModel);
 0745        }
 746
 747        /// <summary>
 748        /// Хук перед проведением транзакции. Возвращает текст ошибки или null при успехе.
 749        /// </summary>
 750        protected virtual Task<string?> ExecutePreTransactionAsync(List<Order> orders) =>
 0751            Task.FromResult<string?>(null);
 752
 753        /// <summary>
 754        /// Дополнительный HTML-блок к сообщению об успешной транзакции (TempData TransactionResult).
 755        /// </summary>
 756        protected virtual string? GetPostTransactionSuccessAppendix() =>
 0757            OzonOrderCancellationService.BuildSuccessHtml(OzonCancelSuccessEntries);
 758
 759        /// <summary>
 760        /// Дополнительный async-хук для обогащения сообщения об успешной транзакции.
 761        /// Выполняется только после успешного проведения в системе (changeCount &gt; 0).
 762        /// </summary>
 763        protected virtual Task<string?> GetPostTransactionSuccessAppendixAsync(
 764            List<Order> ordersToTransaction,
 0765            TransactionConfig config) => Task.FromResult<string?>(null);
 766
 767        /// <summary>
 768        /// Заказы, успешно отменённые в Ozon в рамках текущей транзакции
 769        /// (заполняется при <see cref="TransactionConfig.SupportsOzonCancelPanel"/>).
 770        /// </summary>
 0771        protected List<OzonCancelSuccessEntry> OzonCancelSuccessEntries { get; private set; } = new();
 772
 773        /// <summary>
 774        /// Сообщение об успехе последнего вызова CreateTransactionPostBase с suppressTempData=true.
 775        /// </summary>
 0776        protected string? LastTransactionSuccessMessage { get; private set; }
 777
 778        /// <summary>
 779        /// Базовый POST метод для создания транзакции
 780        /// </summary>
 781        protected async Task<IActionResult> CreateTransactionPostBase(
 782            List<Order> orders,
 783            string userName,
 784            string? comment,
 785            int page,
 786            string? deletedOrders,
 787            Dictionary<int, string?>? orderSupplierMap = null,
 788            bool processIn1C = false,
 789            string? warehouseSender = null,
 790            string? warehouseRecipient = null,
 791            Dictionary<int, string>? recipientWarehouseByOrder = null,
 792            Dictionary<int, string>? senderWarehouseByOrder = null,
 793            TransactionConfig? configOverride = null,
 794            bool suppressTempData = false)
 795        {
 0796            var userCommentOnly = TransactionCommentNormalizer.NormalizeUserComment(comment);
 0797            comment = userCommentOnly;
 0798            var config = configOverride ?? GetTransactionConfig();
 0799            var scheduleAiComment = string.IsNullOrEmpty(userCommentOnly);
 0800            TransactionCommentAiContextBuilder? aiCommentBuilder = null;
 0801            if (scheduleAiComment)
 802            {
 0803                var shipInOzon = Request.Form.TryGetValue("shipInOzon", out var shipValues) &&
 0804                                 shipValues.Any(v => string.Equals(v, "true", StringComparison.OrdinalIgnoreCase));
 0805                aiCommentBuilder = new TransactionCommentAiContextBuilder(config.TransactionType, userName, DateTime.Utc
 0806                    .WithFlags(processIn1C, shipInOzon);
 0807                if (OzonCancelSuccessEntries is { Count: > 0 })
 808                {
 0809                    aiCommentBuilder.WithOzonCancelNotes(OzonCancelSuccessEntries.Select(e =>
 0810                    {
 0811                        var note = e.ShipmentNumber;
 0812                        if (!string.IsNullOrWhiteSpace(e.ReasonTitle))
 0813                            note += $", причина: {e.ReasonTitle}";
 0814                        if (!string.IsNullOrWhiteSpace(e.ReasonMessage))
 0815                            note += $" ({e.ReasonMessage})";
 0816                        return note;
 0817                    }));
 818                }
 819            }
 820
 0821            var trackingKey = Request.Form["TrackingKey"].FirstOrDefault();
 0822            var oneCOrganizationInn = Request.Form["oneCOrganizationInn"].FirstOrDefault();
 0823            var createdTransactionId = 0;
 0824            var stockCheck = StockCheckResult.Empty;
 0825            var createDraftsForStockShortage = false;
 826
 0827            if (orders == null)
 828            {
 0829                SetTransactionErrorTempData(
 0830                    TransactionErrorCode.NoOrdersForTransaction,
 0831                    "Не удалось провести выбранные заказы.<br>Было передано слишком большое количество записей.");
 0832                return RedirectToAction("Index", "Orders");
 833            }
 834
 0835            orders = await FilterOrdersByDataScopeAsync(orders);
 836
 0837            await ReportStageAsync("received", "Данные формы приняты",
 0838                orders.Count == 1 ? "1 заказ" : $"заказов: {orders.Count}");
 839
 840            // Определяем список удаленных заказов и снимаем трекинг для них
 841            // Это предотвращает сохранение изменений статуса и других полей для удаленных заказов
 0842            if (!string.IsNullOrEmpty(deletedOrders))
 843            {
 0844                var deletedOrderIds = DeletedOrderIds.Parse(deletedOrders);
 845
 846                // Снимаем трекинг для всех удаленных заказов, чтобы изменения не сохранялись
 0847                foreach (var deletedOrderId in deletedOrderIds)
 848                {
 849                    // Ищем заказ в отслеживаемых сущностях контекста
 0850                    var trackedOrder = Context.ChangeTracker.Entries<Order>()
 0851                        .FirstOrDefault(e => e.Entity.Id == deletedOrderId)?.Entity;
 852
 0853                    if (trackedOrder != null)
 854                    {
 855                        // Снимаем трекинг для этого заказа
 0856                        Context.Entry(trackedOrder).State = EntityState.Detached;
 857                    }
 858                }
 859
 860                // Удаляем исключенные заказы из списка для обработки
 0861                orders = orders.Where(o => !deletedOrderIds.Contains(o.Id)).ToList();
 862            }
 863
 864            // Очищаем пустые дополнительные расходы перед сохранением
 865            // Это гарантирует, что пустые элементы не попадут в базу данных
 0866            foreach (var order in orders)
 867            {
 0868                if (order.ExtraExpenses != null && order.ExtraExpenses.Any())
 869                {
 870                    // Удаляем все расходы, где не заполнены оба поля (тип и сумма)
 0871                    order.ExtraExpenses = order.ExtraExpenses
 0872                        .Where(e => e != null && e.ExpenseTypeId > 0 && e.Amount > 0)
 0873                        .ToList();
 874
 875                    // Если список стал пустым, устанавливаем null
 0876                    if (!order.ExtraExpenses.Any())
 877                    {
 0878                        order.ExtraExpenses = [];
 879                    }
 880                }
 881            }
 882
 883            try
 884            {
 0885                if (orders.Count != 0)
 886                {
 0887                    if (config.SupportsOzonCancelPanel && OzonCancellationService != null)
 888                    {
 0889                        var cancelRequests = OzonOrderCancellationService.ParseFormRequests(Request.Form);
 0890                        if (cancelRequests.Count > 0)
 891                        {
 0892                            var ordersById = orders.ToDictionary(o => o.Id);
 0893                            await ReportStageAsync("ozon-cancel", "Отменяем отправления в Ozon",
 0894                                cancelRequests.Count == 1 ? "1 отправление" : $"отправлений: {cancelRequests.Count}");
 0895                            var cancelResult = await OzonCancellationService.CancelOrdersAsync(cancelRequests, ordersByI
 0896                            if (cancelResult.Errors.Count > 0)
 0897                                return Json(new { error = string.Join("<br/>", cancelResult.Errors) });
 898
 0899                            OzonCancelSuccessEntries = cancelResult.SuccessEntries;
 0900                        }
 0901                    }
 902
 903                    // Остатки спрашиваем до изменения статусов: при нехватке 1С создаст документ,
 904                    // но откажет в проведении, не указав проблемные позиции.
 0905                    if (processIn1C && config.CheckStockBefore1C)
 906                    {
 0907                        var stockOrders = await TransactionStockCheckLineBuilder.LoadForStockCheckAsync(
 0908                            Context, orders.Select(o => o.Id));
 909
 0910                        await ReportStageAsync("stock-check", "Проверяем остатки в 1С");
 0911                        stockCheck = await OneCStockGate.RunAsync(
 0912                            HttpContext,
 0913                            TransactionStockCheckLineBuilder.FromExplicitWarehouses(
 0914                                stockOrders,
 0915                                senderWarehouseByOrder,
 0916                                warehouseSender,
 0917                                TransactionStockCheckLineBuilder.QuantitiesFromForm(orders)));
 0918                        await OneCStockGate.ReportAsync(HttpContext, stockCheck, "Проведение документа 1С");
 919
 920                        // Для «Возврата поставщику» дефицитные позиции уходят отдельным черновиком.
 921                        // Остальные типы транзакций сохраняют прежнее безопасное поведение: 1С не вызывается.
 0922                        createDraftsForStockShortage =
 0923                            config.TransactionType == TransactionType.ReturnToSupplier && stockCheck.HasBlocking;
 0924                        if (!createDraftsForStockShortage && OneCStockGate.TrySkipOneC(TempData, stockCheck))
 0925                            processIn1C = false;
 0926                    }
 927
 0928                    var preTransactionError = await ExecutePreTransactionAsync(orders);
 0929                    if (!string.IsNullOrWhiteSpace(preTransactionError))
 0930                        return Json(new { error = preTransactionError });
 931
 0932                    DateTime createAt = DateTime.Now;
 0933                    int changeCount = 0;
 0934                    string dateTime = "";
 0935                    List<Order> ordersToUpdate = new();
 936
 937                    // Сохраняем статусы до перехода (из БД) для записи истории после успешного проведения
 0938                    var orderIds = orders.Select(o => o.Id).Distinct().ToList();
 0939                    var fromStatusByOrderId = await Context.Orders.AsNoTracking()
 0940                        .Where(o => orderIds.Contains(o.Id))
 0941                        .Select(o => new { o.Id, FromStatusId = o.AppStatusId ?? 0 })
 0942                        .ToDictionaryAsync(x => x.Id, x => x.FromStatusId);
 943
 944                    // Подготавливаем заказы для транзакции
 0945                    await ReportStageAsync("orders", "Меняем статусы заказов",
 0946                        orders.Count == 1 ? "1 заказ" : $"заказов: {orders.Count}");
 0947                    var submittedFields = SubmittedOrderFields.FromForm(Request.HasFormContentType ? Request.Form : null
 0948                    foreach (var order in orders)
 949                    {
 0950                        ordersToUpdate.Add(await OrderServices.TransactOrder(order, submittedFields.For(order.Id)));
 951                    }
 952
 953                    // Дополнительная пост-обработка заказов, специфичная для конкретного типа транзакции
 0954                    if (config.AfterTransactOrdersAsync != null)
 955                    {
 0956                        await config.AfterTransactOrdersAsync(ordersToUpdate);
 957                    }
 958
 959                    List<Order> ordersToTransaction;
 0960                    string oneCResult = string.Empty;
 0961                    IEnumerable<string> oneCHash = Array.Empty<string>();
 0962                    List<OneCResponse> oneCPostingResponses = new();
 0963                    List<(string Sender, string Recipient, List<Order> Orders, List<OneCResponse> Result)> oneCGroupedDe
 0964                    var transitions = config.UseStatusTransitions && StatusTransitionDataServices != null
 0965                        ? await StatusTransitionDataServices.GetByTransactionType(config.TransactionType)
 0966                        : new List<StatusTransition>();
 967
 0968                    if (transitions.Any())
 969                    {
 0970                        var validTargetStatusIds = transitions.Select(t => t.ToStatusId).ToHashSet();
 0971                        ordersToTransaction = ordersToUpdate
 0972                            .Where(o => o.AppStatus != null && validTargetStatusIds.Contains(o.AppStatus.Id))
 0973                            .ToList();
 974                    }
 975                    else
 976                    {
 0977                        AppStatus? primaryStatus = null;
 0978                        if (!string.IsNullOrEmpty(config.PrimaryStatusName))
 0979                            primaryStatus = await AppStatusServices.GetAppStatusAsync(new AppStatus
 0980                                { Name = config.PrimaryStatusName });
 0981                        AppStatus? cancelledStatus = null;
 0982                        if (config.IncludeCancelledOrders)
 0983                            cancelledStatus =
 0984                                await AppStatusServices.GetAppStatusAsync(new AppStatus { Name = "Отменен" });
 985
 0986                        if (string.IsNullOrEmpty(config.PrimaryStatusName))
 987                        {
 0988                            ordersToTransaction = ordersToUpdate.ToList();
 989                        }
 990                        else
 991                        {
 0992                            ordersToTransaction = ordersToUpdate.Where(o =>
 0993                                (primaryStatus != null && o.AppStatus != null && o.AppStatus.Id == primaryStatus.Id) ||
 0994                                (cancelledStatus != null && o.AppStatus != null &&
 0995                                 o.AppStatus.Id == cancelledStatus.Id)).ToList();
 996                        }
 0997                    }
 998
 999                    // 1С: перемещение с указанными складами (напр. «Возврат поставщику»)
 01000                    if (processIn1C && config.UseCustomWarehousesFor1C && OneCTransferManager != null)
 1001                    {
 01002                        await ReportStageAsync("onec", "Создаём документы в 1С");
 01003                        var hasPerOrderSender = senderWarehouseByOrder is { Count: > 0 };
 01004                        var hasPerOrderRecipient = recipientWarehouseByOrder is { Count: > 0 };
 01005                        if (string.IsNullOrWhiteSpace(warehouseSender) && !hasPerOrderSender)
 1006                        {
 01007                            throw new TransactionException(
 01008                                TransactionErrorCode.OneCSenderWarehouseMissing,
 01009                                "Не указан склад отправителя.");
 1010                        }
 1011
 01012                        if (string.IsNullOrWhiteSpace(warehouseRecipient) && !hasPerOrderRecipient)
 1013                        {
 01014                            throw new TransactionException(
 01015                                TransactionErrorCode.OneCRecipientWarehouseMissing,
 01016                                "Не указан склад получателя.");
 1017                        }
 1018
 01019                        if (!hasPerOrderSender && !hasPerOrderRecipient &&
 01020                            !string.IsNullOrWhiteSpace(warehouseRecipient) &&
 01021                            string.Equals(warehouseSender!.Trim(), warehouseRecipient.Trim(),
 01022                                StringComparison.OrdinalIgnoreCase))
 1023                        {
 01024                            throw new TransactionException(
 01025                                TransactionErrorCode.OneCWarehousesMustDiffer,
 01026                                "Склад отправителя и получателя не могут совпадать.");
 1027                        }
 1028
 1029                        try
 1030                        {
 01031                            var transferResult = new List<OneCResponse>();
 01032                            var groupedMode = config.TransactionType == TransactionType.CustomerReturn && hasPerOrderRec
 01033                                              config.TransactionType == TransactionType.ReturnToSupplier &&
 01034                                              hasPerOrderSender && hasPerOrderRecipient;
 1035
 01036                            if (groupedMode)
 1037                            {
 01038                                var recipientMap = recipientWarehouseByOrder!;
 01039                                var blockingOrderIds = createDraftsForStockShortage
 01040                                    ? OneCStockGate.GetBlockingOrderIds(stockCheck)
 01041                                    : [];
 01042                                var ordersWithoutRecipient = ordersToTransaction
 01043                                    .Where(o => !recipientMap.TryGetValue(o.Id, out var recipient) || string.IsNullOrWhi
 01044                                    .Select(o => o.ShipmentNumber)
 01045                                    .ToList();
 01046                                if (ordersWithoutRecipient.Count > 0)
 1047                                {
 01048                                    var noRecipientMessage =
 01049                                        $"Не удалось определить склад получателя для заказов: {string.Join(", ", ordersW
 01050                                    throw new TransactionException(
 01051                                        TransactionErrorCode.OneCRecipientWarehouseMissing,
 01052                                        noRecipientMessage);
 1053                                }
 1054
 01055                                var ordersWithoutSender = hasPerOrderSender
 01056                                    ? ordersToTransaction
 01057                                        .Where(o => !senderWarehouseByOrder!.TryGetValue(o.Id, out var sender) || string
 01058                                        .Select(o => o.ShipmentNumber)
 01059                                        .ToList()
 01060                                    : [];
 01061                                if (ordersWithoutSender.Count > 0)
 1062                                {
 01063                                    throw new TransactionException(
 01064                                        TransactionErrorCode.OneCSenderWarehouseMissing,
 01065                                        $"Не удалось определить склад отправителя для заказов: {string.Join(", ", orders
 1066                                }
 1067
 01068                                var groupedOrders = ordersToTransaction
 01069                                    .GroupBy(o => $"{(hasPerOrderSender ? senderWarehouseByOrder![o.Id] : warehouseSende
 01070                                    .ToList();
 1071
 01072                                foreach (var group in groupedOrders)
 1073                                {
 01074                                    var firstOrder = group.First();
 01075                                    var sender = (hasPerOrderSender ? senderWarehouseByOrder![firstOrder.Id] : warehouse
 01076                                    var recipient = recipientMap[firstOrder.Id].Trim();
 01077                                    if (string.Equals(sender, recipient, StringComparison.OrdinalIgnoreCase))
 1078                                    {
 01079                                        var sameWarehouseMessage =
 01080                                            $"Склад отправителя и получателя не могут совпадать (направление '{sender} →
 01081                                        throw new TransactionException(
 01082                                            TransactionErrorCode.OneCWarehousesMustDiffer,
 01083                                            sameWarehouseMessage);
 1084                                    }
 1085
 01086                                    var groupOrders = group.ToList();
 01087                                    var groupResult = new List<OneCResponse>();
 01088                                    var ordersToPost = groupOrders
 01089                                        .Where(order => !blockingOrderIds.Contains(order.Id))
 01090                                        .ToList();
 01091                                    if (ordersToPost.Count > 0)
 1092                                    {
 01093                                        groupResult.AddRange(await OneCTransferManager.TransferStockWithCustomWarehouses
 01094                                            ordersToPost,
 01095                                            sender,
 01096                                            recipient,
 01097                                            config.TransactionType,
 01098                                            userName: userName,
 01099                                            orgInnOverride: oneCOrganizationInn));
 1100                                    }
 1101
 01102                                    var ordersForDraft = groupOrders
 01103                                        .Where(order => blockingOrderIds.Contains(order.Id))
 01104                                        .ToList();
 01105                                    if (ordersForDraft.Count > 0)
 1106                                    {
 01107                                        groupResult.AddRange(await OneCTransferManager.TransferStockWithCustomWarehouses
 01108                                            ordersForDraft,
 01109                                            sender,
 01110                                            recipient,
 01111                                            config.TransactionType,
 01112                                            userName: userName,
 01113                                            orgInnOverride: oneCOrganizationInn,
 01114                                            createAsDraft: true,
 01115                                            draftReason: "Недостаточно остатка в 1С. Проведите после оприходования товар
 1116                                    }
 01117                                    oneCGroupedDetails.Add((sender, recipient, groupOrders, groupResult));
 01118                                    transferResult.AddRange(groupResult);
 01119                                }
 1120                            }
 1121                            else
 1122                            {
 01123                                var singleResult = await OneCTransferManager.TransferStockWithCustomWarehouses(
 01124                                    ordersToTransaction,
 01125                                    warehouseSender!.Trim(),
 01126                                    warehouseRecipient!.Trim(),
 01127                                    config.TransactionType,
 01128                                    userName: userName,
 01129                                    orgInnOverride: oneCOrganizationInn);
 01130                                transferResult.AddRange(singleResult);
 1131                            }
 1132
 01133                            oneCPostingResponses = transferResult;
 01134                            var successfulTransfers = transferResult.Where(tr => tr.Success).ToList();
 01135                            var failedTransfers = transferResult.Where(tr => !tr.Success).ToList();
 1136
 01137                            if (failedTransfers.Count > 0)
 1138                            {
 01139                                var errorMessages = failedTransfers.Select(tr => tr.Message).Distinct().Take(3);
 01140                                var errorCount = failedTransfers.Count;
 01141                                var successCount = successfulTransfers.Count;
 01142                                var totalCount = transferResult.Count;
 1143
 01144                                var encodedErrors = string.Join("; ", errorMessages.Select(System.Net.WebUtility.HtmlEnc
 01145                                TempData["TransactionResult"] = $"<div class='alert alert-warning'>" +
 01146                                                                $"<strong>⚠️ Частичный результат обработки в 1С</strong>
 01147                                                                $"Успешно: {successCount} из {totalCount} документов<br/
 01148                                                                $"С ошибками: {errorCount} документов<br/>" +
 01149                                                                $"<small>Ошибки: {encodedErrors}" +
 01150                                                                (errorCount > 3 ? "..." : "") + "</small>" +
 01151                                                                "</div>";
 01152                                return Json(new
 01153                                {
 01154                                    redirectTo = Url.Action("Index", "Orders",
 01155                                        new { sortOrder = GetSortStateCookie(), page })
 01156                                });
 1157                            }
 1158
 01159                            oneCHash = successfulTransfers
 01160                                .Select(h => h.TransactionId)
 01161                                .Where(id => id != null)
 01162                                .Select(id => id!);
 01163                            var effectiveWarehouseSender = oneCGroupedDetails.Count > 0
 01164                                ? string.Join(", ", oneCGroupedDetails.Select(group => group.Sender)
 01165                                    .Distinct(StringComparer.OrdinalIgnoreCase))
 01166                                : warehouseSender ?? string.Empty;
 01167                            var effectiveWarehouseRecipient = oneCGroupedDetails.Count > 0
 01168                                ? string.Join(", ", oneCGroupedDetails.Select(group => group.Recipient)
 01169                                    .Distinct(StringComparer.OrdinalIgnoreCase))
 01170                                : warehouseRecipient ?? string.Empty;
 01171                            oneCResult = OneCTransactionSummaryHelper.BuildAlertSummary(
 01172                                ordersToTransaction, transferResult, effectiveWarehouseSender, effectiveWarehouseRecipie
 01173                                + OneCStockGate.PartialPostingNotice(
 01174                                    stockCheck,
 01175                                    ordersToTransaction.Count - OneCStockGate.GetBlockingOrderIds(stockCheck).Count,
 01176                                    oneCPostingResponses.Any(response => response.Success && response.IsDraft))
 01177                                + OneCStockGate.SkippedNotice(stockCheck);
 01178                            if (scheduleAiComment && aiCommentBuilder != null)
 1179                            {
 01180                                aiCommentBuilder.WithOneCResults(
 01181                                    transferResult,
 01182                                    effectiveWarehouseSender,
 01183                                    effectiveWarehouseRecipient);
 1184                            }
 1185                            else
 1186                            {
 01187                                comment += OneCTransactionSummaryHelper.BuildCommentDetails(
 01188                                    ordersToTransaction, transferResult, effectiveWarehouseSender, effectiveWarehouseRec
 1189                            }
 1190
 01191                            if (oneCGroupedDetails.Count > 0)
 1192                            {
 01193                                var lines = oneCGroupedDetails.Select(g =>
 01194                                {
 01195                                    var encodedRoute = System.Net.WebUtility.HtmlEncode($"{g.Sender} → {g.Recipient}");
 01196                                    var encodedNumbers = string.Join(", ", g.Orders.Select(o => System.Net.WebUtility.Ht
 01197                                    var documents = g.Result
 01198                                        .Where(response => response.Success)
 01199                                        .Select(response =>
 01200                                        {
 01201                                            var label = string.IsNullOrWhiteSpace(response.DocumentNumber)
 01202                                                ? "Документ 1С"
 01203                                                : $"№{response.DocumentNumber}";
 01204                                            var encodedLabel = System.Net.WebUtility.HtmlEncode(label);
 01205                                            var documentLink = !string.IsNullOrWhiteSpace(response.TransactionId) &&
 01206                                                               Guid.TryParse(response.TransactionId, out _)
 01207                                                ? $"<a href='/Studio2/OneCDocument/Details?refKey={System.Net.WebUtility
 01208                                                : encodedLabel;
 01209                                            var status = response.IsDraft
 01210                                                ? "<span class='badge bg-warning-subtle text-warning'>черновик</span>"
 01211                                                : response.IsPosted
 01212                                                    ? "<span class='badge bg-success-subtle text-success'>проведён</span
 01213                                                    : "<span class='badge bg-warning-subtle text-warning'>не проведён</s
 01214                                            var reason = response.IsDraft
 01215                                                ? " — недостаточно остатка, проведите после оприходования"
 01216                                                : !string.IsNullOrWhiteSpace(response.PostingError)
 01217                                                    ? $" — {System.Net.WebUtility.HtmlEncode(Services.ApiServices._1CApi
 01218                                                    : string.Empty;
 01219                                            return $"<li>{status} {documentLink}{reason}</li>";
 01220                                        })
 01221                                        .ToList();
 01222                                    return $"<li class='mb-2'><strong>{encodedRoute}</strong>: {g.Orders.Count} заказ(ов
 01223                                           $"({encodedNumbers})" +
 01224                                           (documents.Count > 0 ? $"<ul class='mb-0 mt-1'>{string.Join(string.Empty, doc
 01225                                           "</li>";
 01226                                });
 1227
 01228                                oneCResult += "<div class='alert alert-info mt-2'><strong>Что создано в 1С по направлени
 01229                                              $"<ul class='mb-0'>{string.Join("", lines)}</ul></div>";
 1230                            }
 01231                        }
 01232                        catch (Exception ex1C)
 1233                        {
 01234                            ReportErrorToTelegram("1C processing error", ex1C);
 01235                            throw new TransactionException(
 01236                                TransactionErrorCode.OneCIntegrationFailed,
 01237                                $"Ошибка обработки в 1С: {ex1C.Message}",
 01238                                ex1C);
 1239                        }
 01240                    }
 1241
 1242                    // Создаем транзакцию через TransactionManager
 01243                    var changedFieldsByOrder = ordersToTransaction
 01244                        .ToDictionary(
 01245                            o => o.Id,
 01246                            o => (o.UpdatedColumns ?? new List<string>())
 01247                                .Where(c => !string.IsNullOrWhiteSpace(c))
 01248                                .Distinct()
 01249                                .ToList());
 1250
 01251                    var historyWindowStart = DateTime.Now;
 01252                    var commentForDb = scheduleAiComment
 01253                        ? TransactionCommentAiConstants.DefaultPlaceholder
 01254                        : comment;
 01255                    await ReportStageAsync("document", "Записываем документ в Studio2");
 01256                    if (config.CreateTransactionMethod != null)
 1257                    {
 01258                        (changeCount, dateTime, createdTransactionId) = await config.CreateTransactionMethod(
 01259                            TransactionManager,
 01260                            ordersToTransaction,
 01261                            userName,
 01262                            createAt,
 01263                            commentForDb);
 1264                    }
 1265
 01266                    int cancelCount = ordersToTransaction.Count(o => o.AppStatus?.Name == "Отменен");
 1267
 01268                    if (changeCount > 0)
 1269                    {
 01270                        if (processIn1C && oneCPostingResponses.Count > 0)
 1271                        {
 01272                            if (oneCGroupedDetails.Count > 0)
 1273                            {
 01274                                foreach (var group in oneCGroupedDetails)
 1275                                {
 01276                                    await OrderHistoryOneCPostingRecorder.TryRecordMovementAsync(
 01277                                        HttpContext.RequestServices,
 01278                                        group.Orders,
 01279                                        group.Result,
 01280                                        userName,
 01281                                        createdTransactionId > 0 ? (int?)createdTransactionId : null,
 01282                                        historyWindowStart,
 01283                                        group.Sender,
 01284                                        group.Recipient);
 1285                                }
 1286                            }
 1287                            else
 1288                            {
 01289                                await OrderHistoryOneCPostingRecorder.TryRecordMovementAsync(
 01290                                    HttpContext.RequestServices,
 01291                                    ordersToTransaction,
 01292                                    oneCPostingResponses,
 01293                                    userName,
 01294                                    createdTransactionId > 0 ? (int?)createdTransactionId : null,
 01295                                    historyWindowStart,
 01296                                    warehouseSender,
 01297                                    warehouseRecipient);
 1298                            }
 1299                        }
 1300
 1301                        // Записываем историю переходов статусов только после успешного проведения транзакции
 01302                        var statusTransitionHistoryService =
 01303                            HttpContext.RequestServices.GetService<IStatusTransitionHistoryService>();
 01304                        if (transitions.Any() && statusTransitionHistoryService != null)
 1305                        {
 01306                            var user = await UserCacheService.GetCachedUserAsync(User);
 01307                            await statusTransitionHistoryService.RecordStatusTransitionHistoryAsync(
 01308                                config.TransactionType,
 01309                                ordersToTransaction,
 01310                                fromStatusByOrderId,
 01311                                transitions,
 01312                                commentForDb,
 01313                                user?.AccessProfileId,
 01314                                createAt,
 01315                                null);
 1316                        }
 1317
 01318                        await CacheUpdater.Update(OrderCache);
 1319
 1320                        // Имя статуса для сообщения: фактический новый статус после проведения (из заказов или из перех
 01321                        var statusNameForMessage = string.Join(", ", ordersToTransaction
 01322                            .Select(o => o.AppStatus?.Name)
 01323                            .Where(n => !string.IsNullOrEmpty(n))
 01324                            .Distinct());
 01325                        if (string.IsNullOrEmpty(statusNameForMessage) && transitions.Any())
 01326                            statusNameForMessage = string.Join(", ",
 01327                                transitions.Select(t => t.ToStatus?.Name).Where(n => !string.IsNullOrEmpty(n))
 01328                                    .Distinct());
 01329                        if (string.IsNullOrEmpty(statusNameForMessage))
 01330                            statusNameForMessage = config.PrimaryStatusName ?? "";
 1331
 01332                        string msg = config.SuccessMessageBuilder != null
 01333                            ? config.SuccessMessageBuilder(changeCount, cancelCount, dateTime, userName, commentForDb,
 01334                                orders)
 01335                            : TransactionConfig.BuildStandardSuccessMessage(changeCount, cancelCount, dateTime,
 01336                                userName, commentForDb, orders, statusNameForMessage ?? "", oneCResult);
 1337
 01338                        var changedFieldsMessage =
 01339                            BuildChangedFieldsMessage(config.TransactionType, ordersToTransaction, changedFieldsByOrder)
 01340                        if (!string.IsNullOrWhiteSpace(changedFieldsMessage))
 1341                        {
 01342                            msg += "<br/>" + changedFieldsMessage;
 1343                        }
 1344
 01345                        if (config.SuccessMessageBuilder != null && !string.IsNullOrWhiteSpace(oneCResult))
 1346                        {
 01347                            msg += "<br/>" + oneCResult;
 1348                        }
 1349
 01350                        var successAppendix = GetPostTransactionSuccessAppendix();
 01351                        if (!string.IsNullOrWhiteSpace(successAppendix))
 01352                            msg += "<br/>" + successAppendix;
 1353
 01354                        var asyncSuccessAppendix =
 01355                            await GetPostTransactionSuccessAppendixAsync(ordersToTransaction, config);
 01356                        if (!string.IsNullOrWhiteSpace(asyncSuccessAppendix))
 01357                            msg += "<br/>" + asyncSuccessAppendix;
 1358
 01359                        if (scheduleAiComment && aiCommentBuilder != null && createdTransactionId > 0)
 1360                        {
 01361                            if (HttpContext.Items.TryGetValue(TransactionCommentAiHttpContextKeys.OzonShipResult,
 01362                                    out var ozonObj) &&
 01363                                ozonObj is OrderToSupplierOzonShipResult ozonShipResult)
 1364                            {
 01365                                aiCommentBuilder.WithOzonShip(
 01366                                    TransactionCommentAiOzonShipMapper.ToSummary(ozonShipResult));
 1367                            }
 1368
 01369                            await TransactionCommentAiSchedulingHelper.TryScheduleAsync(
 01370                                HttpContext.RequestServices,
 01371                                aiCommentBuilder,
 01372                                scheduleAiComment,
 01373                                createdTransactionId,
 01374                                ordersToTransaction,
 01375                                fromStatusByOrderId,
 01376                                Context,
 01377                                changedFieldsByOrder,
 01378                                HttpContext.RequestAborted);
 1379                        }
 1380
 01381                        await NotificationService.NotifyAllAsync(msg);
 01382                        if (suppressTempData)
 01383                            LastTransactionSuccessMessage = msg;
 1384                        else
 01385                            TempData["TransactionResult"] = msg;
 1386
 1387                        // Финализация трекинга только после успешного проведения.
 01388                        var trackingService = HttpContext.RequestServices.GetService<ITransactionTimeTrackingService>();
 01389                        if (trackingService != null &&
 01390                            !string.IsNullOrWhiteSpace(trackingKey) &&
 01391                            createdTransactionId > 0)
 1392                        {
 01393                            var transactionUrl = $"/Studio2/Transaction/GetTransaction?transactionId={createdTransaction
 01394                            await trackingService.CompleteSessionAsync(
 01395                                trackingKey,
 01396                                createdTransactionId,
 01397                                ordersToTransaction.Count,
 01398                                commentForDb,
 01399                                transactionUrl);
 1400                        }
 01401                    }
 1402
 01403                    await TransactionCache.Update();
 01404                }
 1405
 01406                ClearSelectedIdsSession();
 1407
 1408                // Очищаем cookies для заказов
 01409                foreach (var order in orders)
 1410                {
 01411                    var cookieKey = $"PurchasePrice_{order.Id}";
 01412                    if (Request.Cookies.ContainsKey(cookieKey))
 1413                    {
 01414                        Response.Cookies.Delete(cookieKey);
 1415                    }
 1416                }
 1417
 1418                // Сохраняем засылы если есть
 01419                if (orderSupplierMap != null && orderSupplierMap.Any())
 1420                {
 01421                    await OrderServices.SetZasylAsync(orderSupplierMap);
 1422                }
 1423
 1424
 1425                // Сохраняем дополнительные расходы через слой сервиса заказов
 01426                if (OrderServices != null)
 1427                {
 01428                    await OrderServices.SaveExtraExpensesForOrders(orders);
 1429                }
 1430
 01431                var calculationSnapshots = HttpContext.RequestServices.GetService<OrderCalculationSnapshotService>();
 01432                if (calculationSnapshots != null)
 1433                {
 01434                    await calculationSnapshots.CaptureAsync(
 01435                        orders.Select(x => x.Id), Guid.NewGuid(), "Transaction",
 01436                        User.Identity?.Name, HttpContext.RequestAborted);
 1437                }
 1438
 01439                await ReportProgressFinishedAsync(true);
 1440
 01441                return Json(new
 01442                {
 01443                    redirectTo = Url.Action("Index", "Orders", new { sortOrder = GetSortStateCookie(), page })
 01444                });
 1445            }
 01446            catch (Exception ex)
 1447            {
 01448                await ReportProgressFinishedAsync(false, ex.Message);
 01449                ReportErrorToTelegram("Transaction processing error", ex);
 01450                if (ex is TransactionException transactionException)
 1451                {
 01452                    SetTransactionErrorTempData(transactionException.Code, transactionException.Message);
 1453                }
 1454                else
 1455                {
 01456                    SetTransactionErrorTempData(
 01457                        TransactionErrorCode.Unknown,
 01458                        $"Не удалось провести выбранные заказы ({ex.Message})");
 1459                }
 1460
 1461                // Сохраняем засылы даже при ошибке
 01462                if (orderSupplierMap != null && orderSupplierMap.Any())
 1463                {
 01464                    await OrderServices.SetZasylAsync(orderSupplierMap);
 1465                }
 1466
 01467                return Json(new
 01468                {
 01469                    redirectTo = Url.Action("Index", "Orders", new { sortOrder = GetSortStateCookie(), page })
 01470                });
 1471            }
 01472        }
 1473
 1474        protected void SetTransactionErrorTempData(TransactionErrorCode code, string message)
 1475        {
 01476            TempData[TransactionErrorCodeTempDataKey] = code.ToString();
 01477            TempData["ErorrResult"] = message;
 01478        }
 1479
 1480
 1481        /// <summary>Сообщает в Telegram об исключении при проведении транзакции; не ждёт отправки и не бросает исключен
 1482        private void ReportErrorToTelegram(string stage, Exception ex)
 1483        {
 1484            try
 1485            {
 01486                var notifier = HttpContext.RequestServices.GetService<ITelegramNotifier>();
 01487                if (notifier is not { IsConfigured: true }) return;
 1488
 01489                var message = TelegramMessageFactory.Exception("Транзакция: " + stage, ex,
 01490                    new Dictionary<string, string?>
 01491                    {
 01492                        ["URL"] = $"{Request.Method} {Request.Path}{Request.QueryString}",
 01493                        ["Пользователь"] = User?.Identity?.Name ?? "Анонимный",
 01494                        ["UserId"] = User?.FindFirstValue(ClaimTypes.NameIdentifier),
 01495                        ["TraceId"] = HttpContext.TraceIdentifier
 01496                    });
 01497                _ = notifier.SendAsync(message);
 01498            }
 01499            catch
 1500            {
 1501                // Ошибка отправки в Telegram не должна ломать основной поток выполнения
 01502            }
 01503        }
 1504
 1505        private static string BuildChangedFieldsMessage(
 1506            TransactionType transactionType,
 1507            IReadOnlyCollection<Order> ordersToTransaction,
 1508            IReadOnlyDictionary<int, List<string>> changedFieldsByOrder)
 1509        {
 01510            if (transactionType != TransactionType.WarehouseChange || changedFieldsByOrder.Count == 0)
 1511            {
 01512                return string.Empty;
 1513            }
 1514
 01515            var technicalFieldsToHide = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
 01516            {
 01517                "EtProducerId",
 01518                "EtProducer",
 01519                "TransactionId",
 01520                "OzonClientId",
 01521                "ExcelFileDataId",
 01522                "ExcelFileData",
 01523                "DeliveryId",
 01524                "Delivery",
 01525                "ExtraExpenses",
 01526                "OrderExtensions",
 01527                "UpdatedColumns",
 01528                "UpdatedBy",
 01529                "IsVerified",
 01530                "IsAccepted"
 01531            };
 1532
 01533            static string GetFieldDisplayName(string fieldName) => fieldName switch
 01534            {
 01535                "ShipmentWarehouseId" => "Склад отгрузки",
 01536                "AppStatusId" => "Статус",
 01537                "SupplierId" => "Поставщик",
 01538                "OrderNumberToSupplier" => "Номер заказа поставщику",
 01539                "OriginalPurchasePrice" => "Цена закупки",
 01540                "PurchasePrice" => "Цена закупки (RUB)",
 01541                "Quantity" => "Количество",
 01542                _ => GetDisplayNameFromOrderModel(fieldName)
 01543            };
 1544
 1545            static string GetDisplayNameFromOrderModel(string fieldName)
 1546            {
 01547                var property = typeof(Order).GetProperty(
 01548                    fieldName,
 01549                    BindingFlags.Public | BindingFlags.Instance | BindingFlags.IgnoreCase);
 1550
 01551                if (property != null)
 1552                {
 01553                    var display = property.GetCustomAttribute<DisplayAttribute>();
 01554                    if (!string.IsNullOrWhiteSpace(display?.Name))
 1555                    {
 01556                        return display.Name!;
 1557                    }
 1558
 01559                    var displayName = property.GetCustomAttribute<DisplayNameAttribute>();
 01560                    if (!string.IsNullOrWhiteSpace(displayName?.DisplayName))
 1561                    {
 01562                        return displayName.DisplayName!;
 1563                    }
 1564                }
 1565
 1566                // Fallback: humanize camelCase/PascalCase and underscores.
 01567                var normalized = fieldName.Replace("_", " ").Trim();
 01568                if (string.IsNullOrWhiteSpace(normalized))
 1569                {
 01570                    return fieldName;
 1571                }
 1572
 01573                var humanized = System.Text.RegularExpressions.Regex.Replace(
 01574                    normalized,
 01575                    "(?<!^)([A-Z])",
 01576                    " $1");
 1577
 01578                return humanized.Trim();
 1579            }
 1580
 01581            var lines = ordersToTransaction
 01582                .Select(order =>
 01583                {
 01584                    if (!changedFieldsByOrder.TryGetValue(order.Id, out var fields) || fields.Count == 0)
 01585                    {
 01586                        return null;
 01587                    }
 01588
 01589                    var displayFields = fields
 01590                        .Where(field => !technicalFieldsToHide.Contains(field))
 01591                        .Select(GetFieldDisplayName)
 01592                        .Distinct()
 01593                        .ToList();
 01594
 01595                    if (displayFields.Count == 0)
 01596                    {
 01597                        return null;
 01598                    }
 01599
 01600                    var shipmentNumber = string.IsNullOrWhiteSpace(order.ShipmentNumber)
 01601                        ? $"ID {order.Id}"
 01602                        : order.ShipmentNumber;
 01603
 01604                    return $"<li><b>{System.Net.WebUtility.HtmlEncode(shipmentNumber)}</b>: {string.Join(", ", displayFi
 01605                })
 01606                .Where(line => !string.IsNullOrWhiteSpace(line))
 01607                .ToList();
 1608
 01609            if (lines.Count == 0)
 1610            {
 01611                return string.Empty;
 1612            }
 1613
 01614            return $"Измененные поля:<ul>{string.Join("", lines)}</ul>";
 1615        }
 1616
 1617
 1618        /// <summary>
 1619        /// Получает существующий статус приложения или создает новый
 1620        /// </summary>
 1621        protected async Task<AppStatus> GetOrCreateAppStatus(string statusName)
 1622        {
 01623            var status = await AppStatusServices.GetAppStatusAsync(new AppStatus { Name = statusName });
 01624            if (status == null)
 1625            {
 01626                status = new AppStatus { Name = statusName };
 01627                Context.AppStatuses.Add(status);
 01628                await Context.SaveChangesAsync();
 1629            }
 1630
 01631            return status;
 01632        }
 1633
 1634        /// <summary>
 1635        /// Запускает фоновое кэширование данных из внешних API
 1636        /// </summary>
 1637        protected void RunBackgroundCacheTask(List<Order> orders)
 1638        {
 01639            if (ProxyHttpClientService == null) return;
 1640
 01641            _ = Task.Run(async () =>
 01642            {
 01643                foreach (var order in orders)
 01644                {
 01645                    try
 01646                    {
 01647                        if (order.EtProducer != null && !string.IsNullOrEmpty(order.Article))
 01648                        {
 01649                            await ProxyHttpClientService.GetJsonAsync(
 01650                                $"https://api.interparts.ru/detail-full-info/{order.EtProducer.Name}/{order.Article}");
 01651                            await ProxyHttpClientService.GetJsonAsync(
 01652                                $"https://api.interparts.ru/product-information/product/?article_number={order.Article}&
 01653                        }
 01654                    }
 01655                    catch
 01656                    {
 01657                        /* Игнорируем ошибки фонового кэширования */
 01658                    }
 01659                }
 01660            });
 01661        }
 1662
 1663        /// <summary>
 1664        /// Очищает выбранные ID из сессии
 1665        /// </summary>
 1666        [HttpPost]
 1667        public IActionResult ClearSelectedIdsSession()
 1668        {
 01669            HttpContext.Session.SetString("selectedIds", JsonConvert.SerializeObject(new List<int>()));
 01670            return Ok();
 1671        }
 1672
 1673        /// <summary>
 1674        /// Получает состояние сортировки из cookie
 1675        /// </summary>
 1676        protected OrderSortState GetSortStateCookie()
 1677        {
 01678            var sortStateCookie = Request.Cookies["SortState"];
 01679            if (!string.IsNullOrEmpty(sortStateCookie) &&
 01680                Enum.TryParse<OrderSortState>(sortStateCookie, out var savedSortState))
 1681            {
 01682                return savedSortState;
 1683            }
 1684
 01685            return OrderSortState.StandardState;
 1686        }
 1687
 1688        protected async Task<List<string>> FilterBitrixWarehouseNamesByScope(IEnumerable<string> warehouseNames)
 1689        {
 01690            var names = warehouseNames
 01691                .Where(n => !string.IsNullOrWhiteSpace(n))
 01692                .Distinct(StringComparer.OrdinalIgnoreCase)
 01693                .OrderBy(n => n)
 01694                .ToList();
 1695
 01696            if (ScopeFilterValuesService == null)
 1697            {
 01698                return names;
 1699            }
 1700
 01701            var userId = User?.FindFirstValue(ClaimTypes.NameIdentifier);
 01702            if (string.IsNullOrWhiteSpace(userId))
 1703            {
 01704                return names;
 1705            }
 1706
 01707            var allowed = await ScopeFilterValuesService.GetAllowedValuesAsync(
 01708                userId,
 01709                "BitrixWarehouse",
 01710                HttpContext.RequestAborted);
 1711
 01712            if (!allowed.TryGetValue("Name", out var allowedNames) || allowedNames.Count == 0)
 1713            {
 01714                return names;
 1715            }
 1716
 01717            return names
 01718                .Where(n => allowedNames.Contains(n))
 01719                .OrderBy(n => n)
 01720                .ToList();
 01721        }
 1722
 1723        protected async Task<List<T>> FilterByOrderScopeIdsAsync<T>(
 1724            IEnumerable<T> source,
 1725            Func<T, int> idSelector,
 1726            string scopeFieldName)
 1727        {
 01728            var list = source?.ToList() ?? new List<T>();
 01729            if (list.Count == 0 || ScopeFilterValuesService == null)
 1730            {
 01731                return list;
 1732            }
 1733
 01734            var userId = User?.FindFirstValue(ClaimTypes.NameIdentifier);
 01735            if (string.IsNullOrWhiteSpace(userId))
 1736            {
 01737                return list;
 1738            }
 1739
 01740            var allowed = await ScopeFilterValuesService.GetAllowedValuesAsync(
 01741                userId,
 01742                "Order",
 01743                HttpContext.RequestAborted);
 1744
 01745            if (!allowed.TryGetValue(scopeFieldName, out var rawIds) || rawIds.Count == 0)
 1746            {
 01747                return list;
 1748            }
 1749
 01750            var allowedIds = rawIds
 01751                .Select(v => int.TryParse(v, out var id) ? id : (int?)null)
 01752                .Where(v => v.HasValue)
 01753                .Select(v => v!.Value)
 01754                .ToHashSet();
 1755
 01756            if (allowedIds.Count == 0)
 1757            {
 01758                return list;
 1759            }
 1760
 01761            return list.Where(item => allowedIds.Contains(idSelector(item))).ToList();
 01762        }
 1763
 1764        protected async Task<List<string>> FilterWarehouseNamesByOrderScopeAsync(IEnumerable<string> names)
 1765        {
 01766            var normalized = names
 01767                .Where(n => !string.IsNullOrWhiteSpace(n))
 01768                .Distinct(StringComparer.OrdinalIgnoreCase)
 01769                .OrderBy(n => n)
 01770                .ToList();
 1771
 01772            if (normalized.Count == 0 || ScopeFilterValuesService == null || WarehouseDataServices == null)
 1773            {
 01774                return normalized;
 1775            }
 1776
 01777            var userId = User?.FindFirstValue(ClaimTypes.NameIdentifier);
 01778            if (string.IsNullOrWhiteSpace(userId))
 1779            {
 01780                return normalized;
 1781            }
 1782
 01783            var allowed = await ScopeFilterValuesService.GetAllowedValuesAsync(userId, "Order", HttpContext.RequestAbort
 01784            if (!allowed.TryGetValue("ShipmentWarehouseId", out var rawIds) || rawIds.Count == 0)
 1785            {
 01786                return normalized;
 1787            }
 1788
 01789            var allowedIds = rawIds
 01790                .Select(v => int.TryParse(v, out var id) ? id : (int?)null)
 01791                .Where(v => v.HasValue)
 01792                .Select(v => v!.Value)
 01793                .ToHashSet();
 1794
 01795            if (allowedIds.Count == 0)
 1796            {
 01797                return normalized;
 1798            }
 1799
 01800            var allWarehouses = await WarehouseDataServices.GetWarehouses();
 01801            var allowedNames = allWarehouses
 01802                .Where(w => allowedIds.Contains(w.Id))
 01803                .Select(w => w.Name)
 01804                .Where(n => !string.IsNullOrWhiteSpace(n))
 01805                .ToHashSet(StringComparer.OrdinalIgnoreCase);
 1806
 01807            return normalized.Where(n => allowedNames.Contains(n)).OrderBy(n => n).ToList();
 01808        }
 1809
 1810        protected async Task<int[]> FilterOrderIdsByDataScopeAsync(IEnumerable<int> ids)
 1811        {
 01812            var distinctIds = ids?
 01813                .Where(id => id > 0)
 01814                .Distinct()
 01815                .ToArray() ?? Array.Empty<int>();
 1816
 01817            if (distinctIds.Length == 0)
 1818            {
 01819                return Array.Empty<int>();
 1820            }
 1821
 01822            var scopedQuery = Context.Orders
 01823                .AsNoTracking()
 01824                .Where(o => distinctIds.Contains(o.Id));
 1825
 01826            scopedQuery = await ApplyOrderDataScopeAsync(scopedQuery);
 1827
 01828            return await scopedQuery
 01829                .Select(o => o.Id)
 01830                .ToArrayAsync(HttpContext.RequestAborted);
 01831        }
 1832
 1833        protected async Task<List<Order>> FilterOrdersByDataScopeAsync(IEnumerable<Order> orders)
 1834        {
 01835            var list = orders?.ToList() ?? new List<Order>();
 01836            if (list.Count == 0)
 1837            {
 01838                return list;
 1839            }
 1840
 01841            var allowedIds = await FilterOrderIdsByDataScopeAsync(list.Select(o => o.Id));
 01842            if (allowedIds.Length == 0)
 1843            {
 01844                return new List<Order>();
 1845            }
 1846
 01847            var allowedSet = allowedIds.ToHashSet();
 01848            return list.Where(o => allowedSet.Contains(o.Id)).ToList();
 01849        }
 1850
 1851        private async Task<IQueryable<Order>> ApplyOrderDataScopeAsync(IQueryable<Order> ordersQuery)
 1852        {
 01853            if (DataScopeService == null)
 1854            {
 01855                return ordersQuery;
 1856            }
 1857
 01858            var userId = User?.FindFirstValue(ClaimTypes.NameIdentifier);
 01859            if (string.IsNullOrWhiteSpace(userId))
 1860            {
 01861                return ordersQuery;
 1862            }
 1863
 01864            return await DataScopeService.ApplyDataScopeAsync(
 01865                ordersQuery,
 01866                userId,
 01867                entityName: "Order",
 01868                extraParameters: null,
 01869                ct: HttpContext.RequestAborted);
 01870        }
 1871
 1872        private async Task<IReadOnlyDictionary<string, bool>> BuildOrderListFieldReadMapAsync(string userId)
 1873        {
 01874            var permissionService = HttpContext.RequestServices.GetService<IPermissionService>();
 01875            if (permissionService == null)
 1876            {
 01877                return OrderListColumnCatalog.All.ToDictionary(c => c.FieldName, _ => true, StringComparer.OrdinalIgnore
 1878            }
 1879
 01880            var bundle = await permissionService.GetUserPermissionsAsync(userId, HttpContext.RequestAborted);
 01881            if (bundle.IsSuperAdmin)
 1882            {
 01883                return OrderListColumnCatalog.All.ToDictionary(c => c.FieldName, _ => true, StringComparer.OrdinalIgnore
 1884            }
 1885
 01886            var fromBundle = bundle.Fields
 01887                .Where(f => string.Equals(f.EntityName, OrderListColumnCatalog.EntityName, StringComparison.OrdinalIgnor
 01888                .ToDictionary(f => f.FieldName, f => f.CanRead, StringComparer.OrdinalIgnoreCase);
 1889
 01890            var dict = new Dictionary<string, bool>(StringComparer.OrdinalIgnoreCase);
 01891            foreach (var col in OrderListColumnCatalog.All)
 01892                dict[col.FieldName] = fromBundle.TryGetValue(col.FieldName, out var r) ? r : true;
 01893            return dict;
 01894        }
 1895
 1896        private static void EnsureOrderColumnsVisibilityProfile(CustomIdentityUser? user)
 1897        {
 01898            if (user == null)
 1899            {
 01900                return;
 1901            }
 1902
 01903            if (user.AccessProfileId != null && user.AccessProfile?.AvailableOrderColumns?.Any() == true)
 1904            {
 01905                return;
 1906            }
 1907
 01908            user.AccessProfileId ??= -1;
 01909            user.AccessProfile ??= new AccessProfile();
 01910            user.AccessProfile.AvailableOrderColumns = OrderListColumnCatalog.All
 01911                .Select(c => c.LegacyAccessKey)
 01912                .Where(k => !string.IsNullOrWhiteSpace(k))
 01913                .Distinct(StringComparer.OrdinalIgnoreCase)
 01914                .ToList()!;
 01915        }
 1916
 1917        protected void SetTransactionErrorViewData(TransactionErrorCode code, string? reason)
 1918        {
 01919            ViewData["TransactionErrorCode"] = code.ToString();
 01920            ViewData["ErrorReason"] = reason;
 01921        }
 1922
 1923        protected async Task<Dictionary<int, decimal>> LoadWarehouseStockC0MapAsync(IReadOnlyList<Order> orders)
 1924        {
 01925            if (orders.Count == 0)
 01926                return new Dictionary<int, decimal>();
 1927
 01928            var lookupService = HttpContext.RequestServices.GetService<IWarehouseStockLookupService>();
 01929            if (lookupService == null)
 01930                return new Dictionary<int, decimal>();
 1931
 01932            return await lookupService.BuildC0MapAsync(orders, HttpContext.RequestAborted);
 01933        }
 1934
 1935        protected static void ApplyWarehouseStockC0ToOrders(
 1936            IEnumerable<Order> orders,
 1937            IReadOnlyDictionary<int, decimal> c0ByOrderId)
 1938        {
 01939            foreach (var order in orders)
 1940            {
 01941                if (c0ByOrderId.TryGetValue(order.Id, out var c0))
 01942                    order.OriginalPurchasePrice = c0;
 1943            }
 01944        }
 1945
 1946        /// <summary>Счётчики (N) у артикула/города/номера — только по ключам видимых заказов, без GROUP BY всей таблицы
 1947        protected async Task<(Dictionary<string, int> Articles, Dictionary<string, int> Cities, Dictionary<string, int> 
 1948            GetUniqueCountsForOrdersAsync(IEnumerable<Order> orders, bool includeArchived = false)
 1949        {
 01950            return (
 01951                await OrderServices.GetArticleCountsForOrdersAsync(orders, includeArchived),
 01952                await OrderServices.GetDeliveryCityCountsForOrdersAsync(orders, includeArchived),
 01953                await OrderServices.GetShipmentBaseCountsForOrdersAsync(orders, includeArchived));
 01954        }
 1955    }
 1956}

Methods/Properties

.ctor(OzonRepositories.Context.ApplicationDbContext,Services.DataServices.AppStatusDataServices,Services.DataServices.OrdersDataServices,Services.TransactionUtilsServices.TransactionManager,Services.CacheServices.Cache.OzonOrdersCache.TransactionCache,Services.DataServices.SupplierDataServices,Services.CacheServices.Cache.UserCacheService.IUserCacheService,OzonOrdersWeb.WebServices.Currency.ICurrencyRatesViewBagService,Services.CacheServices.Cache.CacheUpdater`1<OzonDomains.Models.Order>,Services.CacheServices.Cache.OzonOrdersCache.OrderCache,Services.ApiServices.OneCDataManager,Services.DataServices.WarehouseDataServices,OzonRepositories.Data.Bitrix.BitrixStockRepository,PartsInfo.HttpUtils.ProxyHttpClientService,Services.ApiServices._1CApi.OneCTransferManager,Services.DataServices.OrderCartServices,Services.DataServices.WarehouseMappingDataServices,Services.DataServices.ExpenseTypeDataServices,Services.DataServices.OrderExtraExpenseDataServices,Services.DataServices.StatusTransitionDataServices,Services.Authorization.Services.IScopeFilterValuesService,Services.Authorization.Services.IDataScopeService,Services.ApiServices.OzonApi.OzonOrderCancellationService)
get_ProgressKey()
ReportStageAsync(System.String,System.String,System.String)
ReportProgressFinishedAsync(System.Boolean,System.String)
CreateTransactionBase()
CreateTransactionBase()
ExecutePreTransactionAsync(System.Collections.Generic.List`1<OzonDomains.Models.Order>)
GetPostTransactionSuccessAppendix()
GetPostTransactionSuccessAppendixAsync(System.Collections.Generic.List`1<OzonDomains.Models.Order>,OzonOrdersWeb.Areas.Studio2.Controllers.Base.TransactionConfig)
get_OzonCancelSuccessEntries()
get_LastTransactionSuccessMessage()
CreateTransactionPostBase()
SetTransactionErrorTempData(OzonOrdersWeb.Areas.Studio2.Models.Transactions.TransactionErrorCode,System.String)
ReportErrorToTelegram(System.String,System.Exception)
BuildChangedFieldsMessage(OzonDomains.TransactionType,System.Collections.Generic.IReadOnlyCollection`1<OzonDomains.Models.Order>,System.Collections.Generic.IReadOnlyDictionary`2<System.Int32,System.Collections.Generic.List`1<System.String>>)
GetFieldDisplayName()
GetDisplayNameFromOrderModel()
GetOrCreateAppStatus()
RunBackgroundCacheTask(System.Collections.Generic.List`1<OzonDomains.Models.Order>)
ClearSelectedIdsSession()
GetSortStateCookie()
FilterBitrixWarehouseNamesByScope()
FilterByOrderScopeIdsAsync()
FilterWarehouseNamesByOrderScopeAsync()
FilterOrderIdsByDataScopeAsync()
FilterOrdersByDataScopeAsync()
ApplyOrderDataScopeAsync()
BuildOrderListFieldReadMapAsync()
EnsureOrderColumnsVisibilityProfile(OzonRepositories.Context.Identity.CustomIdentityUser)
SetTransactionErrorViewData(OzonOrdersWeb.Areas.Studio2.Models.Transactions.TransactionErrorCode,System.String)
LoadWarehouseStockC0MapAsync()
ApplyWarehouseStockC0ToOrders(System.Collections.Generic.IEnumerable`1<OzonDomains.Models.Order>,System.Collections.Generic.IReadOnlyDictionary`2<System.Int32,System.Decimal>)
GetUniqueCountsForOrdersAsync()