< Summary

Information
Line coverage
0%
Covered lines: 0
Uncovered lines: 695
Coverable lines: 695
Total lines: 1163
Line coverage: 0%
Branch coverage
0%
Covered branches: 0
Total branches: 320
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/Transactions/ShippedToClientTransactionController.cs

#LineLine coverage
 1using System.Text;
 2using Microsoft.AspNetCore.Authorization;
 3using Microsoft.AspNetCore.Mvc;
 4using Microsoft.Extensions.DependencyInjection;
 5using Microsoft.AspNetCore.Mvc.Rendering;
 6using Microsoft.EntityFrameworkCore;
 7using Newtonsoft.Json;
 8using OServices.FiltersServices.FilterModels;
 9using OzonDomains;
 10using OzonDomains.Models;
 11using OzonOrdersWeb.Areas.Studio2.Controllers.Base;
 12using OzonOrdersWeb.Areas.Studio2.Helpers;
 13using OzonOrdersWeb.Areas.Studio2.Controllers.Transactions;
 14using OzonOrdersWeb.Areas.Studio2.Models.Transactions;
 15using OzonOrdersWeb.Areas.Studio2.ViewModels;
 16using OzonOrdersWeb.Areas.Studio2.ViewModels.ShippedToClientTransaction;
 17using OzonOrdersWeb.ViewModels.OrderViewModels;
 18using OzonRepositories.Context;
 19using OzonRepositories.Data.Bitrix;
 20using PartsInfo.HttpUtils;
 21using Services.ApiServices;
 22using Services.ApiServices._1CApi;
 23using Services.ApiServices._1CApi.Models;
 24using Services.ApiServices._1CApi.Stock;
 25using Services.CacheServices.Cache.OzonOrdersCache;
 26using Services.CacheServices.Cache.UserCacheService;
 27using Services.DataServices;
 28using Services.FiltersServices.SortModels;
 29using OzonOrdersWeb.WebServices.Currency;
 30using Services.SignalRServices;
 31using Services.TransactionUtilsServices;
 32using OzonOrdersWeb.TransactionServices;
 33using Services.CacheServices.Cache;
 34
 35
 36[Authorize(Roles = "User,Admin")]
 37[Area("Studio2")]
 38public class ShippedToClientTransactionController : Controller
 39{
 040    private static readonly TimeZoneInfo RussianTimeZone = TimeZoneInfo.FindSystemTimeZoneById("Russian Standard Time");
 41
 42    private readonly ApplicationDbContext _context;
 43    private readonly AppStatusDataServices _appStatusServices;
 44    private readonly OrdersDataServices _orderServices;
 45    private readonly TransactionManager _transaction;
 46    private readonly TransactionCache _transactionCache;
 47
 48    private readonly SupplierDataServices _supplierDataServices;
 49    private readonly IUserCacheService _userCacheService;
 50    private readonly ICurrencyRatesViewBagService _currencyRatesViewBagService;
 51    private readonly BitrixStockRepository _bitrixStockRepository;
 52    private readonly CacheUpdater<Order> _cacheUpdater;
 53    private readonly OrderCache _cache;
 54    private readonly ProxyHttpClientService _proxyHttpClientService;
 55    private readonly OneCTransferManager _oneCTransferManager;
 56    private readonly OneCExpenseManager _oneCExpenseManager;
 57    private readonly OneCDataManager _oneCDataManager;
 58    private readonly WarehouseDataServices _warehouseDataServices;
 59    private readonly WarehouseMappingDataServices _warehouseMappingDataServices;
 60    private readonly StatusTransitionDataServices _statusTransitionDataServices;
 61
 062    public ShippedToClientTransactionController(
 063        ApplicationDbContext context,
 064        AppStatusDataServices appStatusServices,
 065        OrdersDataServices orderServices,
 066        TransactionManager transaction,
 067        TransactionCache transactionCache,
 068        SupplierDataServices supplierDataServices,
 069        IUserCacheService userCacheService,
 070        ICurrencyRatesViewBagService currencyRatesViewBagService,
 071        BitrixStockRepository bitrixStockRepository,
 072        CacheUpdater<Order> cacheUpdater,
 073        OrderCache cache,
 074        ProxyHttpClientService proxyHttpClientService,
 075        OneCTransferManager oneCTransferManager,
 076        OneCExpenseManager oneCExpenseManager,
 077        OneCDataManager oneCDataManager,
 078        WarehouseDataServices warehouseDataServices,
 079        WarehouseMappingDataServices warehouseMappingDataServices,
 080        StatusTransitionDataServices statusTransitionDataServices)
 81    {
 082        _context = context;
 083        _appStatusServices = appStatusServices;
 084        _orderServices = orderServices;
 085        _transaction = transaction;
 086        _transactionCache = transactionCache;
 087        _supplierDataServices = supplierDataServices;
 088        _userCacheService = userCacheService;
 089        _currencyRatesViewBagService = currencyRatesViewBagService;
 090        _bitrixStockRepository = bitrixStockRepository;
 091        _cacheUpdater = cacheUpdater;
 092        _cache = cache;
 093        _proxyHttpClientService = proxyHttpClientService;
 094        _oneCTransferManager = oneCTransferManager;
 095        _oneCExpenseManager = oneCExpenseManager;
 096        _oneCDataManager = oneCDataManager;
 097        _warehouseDataServices = warehouseDataServices;
 098        _warehouseMappingDataServices = warehouseMappingDataServices;
 099        _statusTransitionDataServices = statusTransitionDataServices;
 0100    }
 101
 102
 103    public async Task<IActionResult> CreateShippedToClientTransactionFromFile(string ids, int page, DateTime? oneCOperat
 104    {
 0105        var effectiveOneCOperationDate = GetEffectiveOneCOperationDate(oneCOperationDate);
 106        // Получаем статус "Отгружен клиенту" (основной статус для этой транзакции)
 0107        var appStatus = await _appStatusServices.GetAppStatusAsync(new AppStatus { Name = "Отгружен клиенту" });
 0108        if (appStatus == null)
 109        {
 0110            appStatus = new AppStatus { Name = "Отгружен клиенту" };
 0111            _context.AppStatuses.Add(appStatus);
 0112            await _context.SaveChangesAsync();
 113        }
 114
 115        // Получаем статус "Отменен"
 0116        var cancelledStatus = await _appStatusServices.GetAppStatusAsync(new AppStatus() { Name = "Отменен" });
 117
 118        // Формируем список статусов без дубликатов
 0119        var filteredStatuses = new List<AppStatus>();
 0120        if (appStatus != null)
 0121            filteredStatuses.Add(appStatus);
 0122        if (cancelledStatus != null)
 0123            filteredStatuses.Add(cancelledStatus);
 124
 125        // Убираем дубликаты по ID
 126        filteredStatuses = filteredStatuses.GroupBy(s => s.Id).Select(g => g.First()).ToList();
 127
 0128        ViewBag.AppStatuses = new SelectList(filteredStatuses, "Id", "Name");
 129        ViewBag.StatusColors = filteredStatuses.ToDictionary(s => s.Id.ToString(), s => s.GetStatusColor());
 130
 0131        if (string.IsNullOrWhiteSpace(ids))
 0132            return BadRequest("Не выбраны заказы для транзакции.");
 133
 0134        int[] idArray = ids.Split(',', StringSplitOptions.RemoveEmptyEntries).Select(int.Parse).ToArray();
 135
 0136        if (idArray.Length == 0)
 0137            return BadRequest("Не выбраны заказы для транзакции.");
 138
 0139        List<Order> ordersToTransaction = new();
 140
 141        // ВАЖНО: Загружаем заказы БЕЗ ТРЕКИНГА - изменения статусов для отображения
 142        // НЕ сохранятся в БД при отмене транзакции
 0143        var orders = await _orderServices.GetOrdersNoTracking(idArray);
 144
 0145        foreach (var order in orders)
 146        {
 0147            if (order == null)
 148            {
 149                continue;
 150            }
 151
 152            // Добавляем в транзакцию все заказы, у которых статус отсутствует
 153            // или отличается от "Отгружен клиенту"
 0154            if (order.AppStatus == null || order.AppStatus.Name != "Отгружен клиенту")
 155            {
 156                // Если заказ отменен - оставляем статус "Отменен"
 157                // (статус будет установлен позже в CreateShippedToClientTransactionFromFileView)
 0158                ordersToTransaction.Add(order);
 159            }
 160        }
 161
 0162        if (ordersToTransaction.Any())
 163        {
 164            // Группировка заказов по складам с пометкой проблемных групп.
 0165            var groupedByWarehouse = ordersToTransaction
 166                .Select(o => new
 167                {
 168                    Order = o,
 169                    EffectiveWarehouse = TransactionWarehouseHelper.GetEffectiveWarehouseName(o),
 170                    SupplierName = o.Supplier?.Name?.Trim()
 171                })
 172                .GroupBy(x => string.IsNullOrWhiteSpace(x.EffectiveWarehouse)
 173                    ? "Склад не определен"
 174                    : x.EffectiveWarehouse!)
 175                .OrderBy(g => g.Key)
 0176                .ToList();
 177
 0178            var pages = new List<WarehouseTransactionPage>();
 179
 0180            foreach (var group in groupedByWarehouse)
 181            {
 0182                var warehouseName = group.Key;
 183                var groupIds = string.Join(",", group.Select(x => x.Order.Id));
 0184                var missingWarehouse = warehouseName == "Склад не определен";
 185                var missingSupplierCount = group.Count(x => string.IsNullOrWhiteSpace(x.SupplierName));
 0186                var warnings = new List<string>();
 187
 0188                if (missingWarehouse)
 189                {
 0190                    warnings.Add("У части заказов не определен эффективный склад");
 191                }
 192
 0193                if (missingSupplierCount > 0)
 194                {
 0195                    warnings.Add($"Не определен поставщик у заказов: {missingSupplierCount}");
 196                }
 197
 0198                var url = missingWarehouse
 0199                    ? string.Empty
 0200                    : Url.Action(
 0201                        "CreateShippedToClientTransactionFromFileView",
 0202                        "ShippedToClientTransaction",
 0203                        new { ids = groupIds, warehouse = warehouseName, page, oneCOperationDate = effectiveOneCOperatio
 0204                    ) ?? string.Empty;
 205
 0206                pages.Add(new WarehouseTransactionPage
 0207                {
 0208                    WarehouseName = warehouseName,
 0209                    Url = url,
 0210                    OrdersCount = group.Count(),
 0211                    HasWarnings = warnings.Count > 0,
 0212                    WarningText = string.Join(". ", warnings),
 0213                    CanOpen = !missingWarehouse && !string.IsNullOrWhiteSpace(url),
 0214                    GroupKey = $"shipped-to-client:{warehouseName}:{groupIds}"
 0215                });
 216            }
 217
 218            // Модель для представления
 0219            var model = new MultipleTransactionPagesViewModel
 0220            {
 0221                WarehousePages = pages,
 0222                OneCOperationDate = effectiveOneCOperationDate
 0223            };
 224
 0225            return View("MultipleTransactionPagesByWarehouse", model);
 226        }
 227
 228        // === ОПТИМИЗАЦИЯ: non-DB параллельно, DB последовательно ===
 0229        var userTask = _userCacheService.GetCachedUserAsync(User);
 0230        var rateTasks = _currencyRatesViewBagService.StartFetch();
 231
 0232        var suppliers = await _supplierDataServices.GetSuppliers();
 0233        var articles = await _orderServices.GetArticleCountsForOrdersAsync(ordersToTransaction);
 0234        var cities = await _orderServices.GetDeliveryCityCountsForOrdersAsync(ordersToTransaction);
 0235        var numbers = await _orderServices.GetShipmentBaseCountsForOrdersAsync(ordersToTransaction);
 0236        var warehouses = await _warehouseDataServices.GetWarehouses();
 237
 0238        await Task.WhenAll(userTask, rateTasks.Usd, rateTasks.Eur, rateTasks.Byn);
 239
 0240        var rates = rateTasks.ToSnapshot();
 0241        var pageViewModel = new CreateShippedToClientTransactionModel
 0242        {
 0243            RedirectPage = page,
 0244            OneCOperationDate = effectiveOneCOperationDate,
 245            Orders = ordersToTransaction.OrderBy(o => o.ShipmentNumber).ToList(),
 0246            User = userTask.Result!,
 247            Suppliers = suppliers.OrderBy(s => s.Name).ToList(),
 0248            RateUSD = rates.RateUsd,
 0249            RateEUR = rates.RateEur,
 0250            RateBYN = rates.RateByn,
 0251            UniqueArticles = articles,
 0252            UniqueDeliveryCitys = cities,
 0253            UniqueNumbers = numbers,
 0254            Warehouses = warehouses,
 0255            AppStatus = appStatus!,
 0256            Treaties = await _oneCDataManager.GetTreatyListAsync(GetClientInn(ordersToTransaction))
 0257        };
 0258        await AttachTrackingKeyAsync(pageViewModel, TransactionType.ShippedToClient);
 259
 0260        if (pageViewModel.User.AccessProfileId != null)
 0261            pageViewModel.User.AccessProfile = _context.AccessProfiles.Find(pageViewModel.User.AccessProfileId);
 262
 0263        await AttachStockCheckSenderMapAsync(pageViewModel.Orders);
 0264        RunBackgroundCacheTask(pageViewModel.Orders);
 0265        return View("CreateShippedToClientTransaction", pageViewModel);
 0266    }
 267
 268    public async Task<IActionResult> CreateShippedToClientTransactionFromFileView(string ids, int page, DateTime? oneCOp
 269    {
 0270        var effectiveOneCOperationDate = GetEffectiveOneCOperationDate(oneCOperationDate);
 0271        var transitions = await _statusTransitionDataServices.GetByTransactionType(TransactionType.ShippedToClient);
 0272        var validSourceStatusIds = transitions.Any()
 273            ? transitions.Select(t => t.FromStatusId).ToHashSet()
 0274            : null;
 275
 276        // Получаем статус "Отгружен клиенту" (основной статус для этой транзакции)
 0277        var appStatusShippedToClient =
 0278            await _appStatusServices.GetAppStatusAsync(new AppStatus { Name = "Отгружен клиенту" });
 0279        if (appStatusShippedToClient == null)
 280        {
 0281            appStatusShippedToClient = new AppStatus { Name = "Отгружен клиенту" };
 0282            _context.AppStatuses.Add(appStatusShippedToClient);
 0283            await _context.SaveChangesAsync();
 284        }
 285
 286        // Получаем статус "Заказан реализатору"
 0287        var orderedToSellerStatus =
 0288            await _appStatusServices.GetAppStatusAsync(new AppStatus() { Name = "Заказан реализатору" });
 289
 290        // Получаем статус "Отменен"
 0291        var cancelledStatus = await _appStatusServices.GetAppStatusAsync(new AppStatus() { Name = "Отменен" });
 292
 293        // Формируем список статусов без дубликатов
 0294        var filteredStatuses = new List<AppStatus>();
 0295        if (appStatusShippedToClient != null)
 0296            filteredStatuses.Add(appStatusShippedToClient);
 0297        if (orderedToSellerStatus != null)
 0298            filteredStatuses.Add(orderedToSellerStatus);
 0299        if (cancelledStatus != null)
 0300            filteredStatuses.Add(cancelledStatus);
 301
 302        // Убираем дубликаты по ID
 303        filteredStatuses = filteredStatuses.GroupBy(s => s.Id).Select(g => g.First()).ToList();
 304
 0305        ViewBag.AppStatuses = new SelectList(filteredStatuses, "Id", "Name");
 306        ViewBag.StatusColors = filteredStatuses.ToDictionary(s => s.Id.ToString(), s => s.GetStatusColor());
 307        // Устанавливаем начальный статус как "Отгружен клиенту"
 0308        ViewBag.StatusId = appStatusShippedToClient?.Id ??
 309                           filteredStatuses.FirstOrDefault(s => s.Name == "Отгружен клиенту")?.Id;
 310
 0311        if (string.IsNullOrWhiteSpace(ids))
 0312            return BadRequest("Не выбраны заказы для транзакции.");
 313
 0314        int[] idArray = ids.Split(',', StringSplitOptions.RemoveEmptyEntries).Select(int.Parse).ToArray();
 315
 0316        if (idArray.Length == 0)
 0317            return BadRequest("Не выбраны заказы для транзакции.");
 318
 0319        List<Order> ordersToTransaction = new();
 320
 0321        var appStatus = appStatusShippedToClient;
 322
 323        // ВАЖНО: Загружаем заказы БЕЗ ТРЕКИНГА - изменения для отображения
 324        // НЕ сохранятся в БД при отмене транзакции
 0325        var orders = await _orderServices.GetOrdersNoTracking(idArray);
 326
 0327        var warningMessages = new List<string>();
 328
 0329        foreach (var order in orders)
 330        {
 0331            var allowedStatus = validSourceStatusIds != null
 0332                ? order.AppStatusId.HasValue && validSourceStatusIds.Contains(order.AppStatusId.Value)
 0333                : order.AppStatus?.Name == "Заказан реализатору";
 334
 0335            if (!allowedStatus)
 336            {
 0337                warningMessages.Add(
 0338                    $"Заказ {order.ShipmentNumber} пропущен: статус '{order.AppStatus?.Name}' не подходит для транзакции
 0339                continue;
 340            }
 341
 342            // В представлении показываем планируемый статус через ViewBag.StatusId,
 343            // поэтому здесь не меняем статус заказа в базе
 0344            order.UpdatedBy = User.Identity?.Name;
 0345            ordersToTransaction.Add(order);
 346        }
 347
 0348        ViewData["TransactionWarningMessage"] = BuildSkippedOrdersWarningHtml(warningMessages);
 349
 350        // === ОПТИМИЗАЦИЯ: non-DB параллельно, DB последовательно ===
 0351        var userTask2 = _userCacheService.GetCachedUserAsync(User);
 0352        var rateTasks2 = _currencyRatesViewBagService.StartFetch();
 0353        var zasylTask2 = _oneCDataManager.GetWarehouseListAsync();
 354
 0355        var suppliers2 = await _supplierDataServices.GetSuppliers();
 0356        var articles2 = await _orderServices.GetArticleCountsForOrdersAsync(ordersToTransaction);
 0357        var cities2 = await _orderServices.GetDeliveryCityCountsForOrdersAsync(ordersToTransaction);
 0358        var numbers2 = await _orderServices.GetShipmentBaseCountsForOrdersAsync(ordersToTransaction);
 0359        var warehouses2 = await _warehouseDataServices.GetWarehouses();
 360
 0361        await Task.WhenAll(userTask2, rateTasks2.Usd, rateTasks2.Eur, rateTasks2.Byn, zasylTask2);
 362
 0363        var rates2 = rateTasks2.ToSnapshot();
 0364        var pageViewModel = new CreateShippedToClientTransactionModel
 0365        {
 0366            RedirectPage = page,
 0367            OneCOperationDate = effectiveOneCOperationDate,
 368            Orders = ordersToTransaction.OrderBy(o => o.ShipmentNumber).ToList(),
 0369            User = userTask2.Result!,
 370            Suppliers = suppliers2.OrderBy(s => s.Name).ToList(),
 0371            RateUSD = rates2.RateUsd,
 0372            RateEUR = rates2.RateEur,
 0373            RateBYN = rates2.RateByn,
 0374            UniqueArticles = articles2,
 0375            UniqueDeliveryCitys = cities2,
 0376            UniqueNumbers = numbers2,
 0377            AppStatus = appStatus!,
 378            ZasylValues = zasylTask2.Result.Select(w => w.Name).Where(n => !string.IsNullOrWhiteSpace(n)).Select(n => n!
 0379            Warehouses = warehouses2,
 0380            Treaties = await _oneCDataManager.GetTreatyListAsync(GetClientInn(ordersToTransaction))
 0381        };
 0382        await AttachTrackingKeyAsync(pageViewModel, TransactionType.ShippedToClient);
 383
 0384        if (pageViewModel.User.AccessProfileId != null)
 0385            pageViewModel.User.AccessProfile = _context.AccessProfiles.Find(pageViewModel.User.AccessProfileId);
 386
 0387        if (pageViewModel.User.AccessProfile?.AvailableOrderColumns?.Contains("Себестоимость") == true && ordersToTransa
 388        {
 0389            await _orderServices.LoadExtraExpensesForOrders(ordersToTransaction);
 390        }
 391
 0392        await AttachStockCheckSenderMapAsync(pageViewModel.Orders);
 0393        RunBackgroundCacheTask(pageViewModel.Orders);
 0394        return View("CreateShippedToClientTransaction", pageViewModel);
 0395    }
 396
 397
 398    // ==============================
 399    // GET: CreateShippedToClientTransaction
 400    // ==============================
 401    public async Task<IActionResult> CreateShippedToClientTransaction(string ids, int page, DateTime? oneCOperationDate 
 402    {
 0403        var effectiveOneCOperationDate = GetEffectiveOneCOperationDate(oneCOperationDate);
 0404        var transitions = await _statusTransitionDataServices.GetByTransactionType(TransactionType.ShippedToClient);
 405        List<AppStatus> filteredStatuses;
 406        AppStatus appStatus;
 0407        HashSet<int>? validSourceStatusIds = null;
 408
 0409        if (transitions.Any())
 410        {
 411            validSourceStatusIds = transitions.Select(t => t.FromStatusId).ToHashSet();
 0412            appStatus = transitions.First().ToStatus!;
 0413            var orderedToSellerStatus = await _appStatusServices.GetAppStatusAsync(new AppStatus() { Name = "Заказан реа
 0414            var cancelledStatus = await _appStatusServices.GetAppStatusAsync(new AppStatus() { Name = "Отменен" });
 0415            filteredStatuses = new List<AppStatus> { appStatus };
 0416            if (orderedToSellerStatus != null) filteredStatuses.Add(orderedToSellerStatus);
 0417            if (cancelledStatus != null) filteredStatuses.Add(cancelledStatus);
 418            filteredStatuses = filteredStatuses.GroupBy(s => s.Id).Select(g => g.First()).ToList();
 0419        }
 420        else
 421        {
 0422            var appStatusShippedToClient = await _appStatusServices.GetAppStatusAsync(new AppStatus { Name = "Отгружен к
 0423            if (appStatusShippedToClient == null)
 424            {
 0425                appStatusShippedToClient = new AppStatus { Name = "Отгружен клиенту" };
 0426                _context.AppStatuses.Add(appStatusShippedToClient);
 0427                await _context.SaveChangesAsync();
 428            }
 0429            var orderedToSellerStatus = await _appStatusServices.GetAppStatusAsync(new AppStatus() { Name = "Заказан реа
 0430            var cancelledStatus = await _appStatusServices.GetAppStatusAsync(new AppStatus() { Name = "Отменен" });
 0431            filteredStatuses = new List<AppStatus>();
 0432            if (appStatusShippedToClient != null) filteredStatuses.Add(appStatusShippedToClient);
 0433            if (orderedToSellerStatus != null) filteredStatuses.Add(orderedToSellerStatus);
 0434            if (cancelledStatus != null) filteredStatuses.Add(cancelledStatus);
 435            filteredStatuses = filteredStatuses.GroupBy(s => s.Id).Select(g => g.First()).ToList();
 0436            appStatus = appStatusShippedToClient!;
 0437        }
 438
 0439        ViewBag.AppStatuses = new SelectList(filteredStatuses, "Id", "Name");
 440        ViewBag.StatusColors = filteredStatuses.ToDictionary(s => s.Id.ToString(), s => s.GetStatusColor());
 441        ViewBag.StatusId = appStatus?.Id ?? filteredStatuses.FirstOrDefault(s => s.Name == "Отгружен клиенту")?.Id;
 442
 0443        if (string.IsNullOrWhiteSpace(ids))
 0444            return BadRequest("Не выбраны заказы для транзакции.");
 445
 0446        int[] idArray = ids.Split(',', StringSplitOptions.RemoveEmptyEntries).Select(int.Parse).ToArray();
 447
 0448        if (idArray.Length == 0)
 0449            return BadRequest("Не выбраны заказы для транзакции.");
 450
 0451        List<Order> ordersToTransaction = new();
 0452        List<string> warningMessages = new();
 0453        var orderStatusWarnings = new Dictionary<int, string>();
 454
 455        // ВАЖНО: Загружаем заказы БЕЗ ТРЕКИНГА - изменения для отображения
 456        // НЕ сохранятся в БД при отмене транзакции
 0457        var ordersFromDb = await _orderServices.GetOrdersNoTracking(idArray);
 458
 0459        foreach (var order in ordersFromDb)
 460        {
 0461            bool allowedStatus = validSourceStatusIds != null
 0462                ? (order.AppStatusId.HasValue && validSourceStatusIds.Contains(order.AppStatusId.Value))
 0463                : order.AppStatus?.Name == "Заказан реализатору";
 0464            if (allowedStatus)
 465            {
 0466                order.UpdatedBy = User.Identity?.Name;
 0467                ordersToTransaction.Add(order);
 468            }
 469            else
 470            {
 0471                var warning = $"Статус '{order.AppStatus?.Name}' не подходит для проведения.";
 0472                warningMessages.Add($"Заказ {order.ShipmentNumber} пропущен: {warning}");
 0473                orderStatusWarnings[order.Id] = warning;
 0474                order.UpdatedBy = User.Identity?.Name;
 0475                ordersToTransaction.Add(order);
 476            }
 477        }
 478
 0479        ViewData["TransactionWarningMessage"] = BuildSkippedOrdersWarningHtml(warningMessages);
 480
 0481        var conditionsMessage = transitions.Any()
 0482            ? string.Join("<br/>", transitions
 483                .Select(t => t.Conditions)
 484                .Where(c => !string.IsNullOrWhiteSpace(c))
 485                .Select(c => System.Net.WebUtility.HtmlEncode(c!.Trim()))
 0486                .Distinct())
 0487            : null;
 0488        ViewData["TransactionConditionsMessage"] = conditionsMessage;
 489
 490        // === ОПТИМИЗАЦИЯ: non-DB параллельно, DB последовательно ===
 0491        var userTask3 = _userCacheService.GetCachedUserAsync(User);
 0492        var rateTasks3 = _currencyRatesViewBagService.StartFetch();
 493
 0494        var suppliers3 = await _supplierDataServices.GetSuppliers();
 0495        var articles3 = await _orderServices.GetArticleCountsForOrdersAsync(ordersToTransaction);
 0496        var cities3 = await _orderServices.GetDeliveryCityCountsForOrdersAsync(ordersToTransaction);
 0497        var numbers3 = await _orderServices.GetShipmentBaseCountsForOrdersAsync(ordersToTransaction);
 0498        var warehouses3 = await _warehouseDataServices.GetWarehouses();
 499
 0500        await Task.WhenAll(userTask3, rateTasks3.Usd, rateTasks3.Eur, rateTasks3.Byn);
 501
 502        // Если ошибок нет
 0503        if (!ordersToTransaction.Any())
 504        {
 0505            ViewData["TransactionErrorCode"] = TransactionErrorCode.NoOrdersForTransaction.ToString();
 0506            ViewData["ErrorReason"] = "Нет заказов со статусом 'Заказан реализатору'.";
 507        }
 508
 0509        var rates3 = rateTasks3.ToSnapshot();
 0510        var pageViewModel = new CreateShippedToClientTransactionModel
 0511        {
 0512            RedirectPage = page,
 0513            OneCOperationDate = effectiveOneCOperationDate,
 514            Orders = ordersToTransaction.OrderBy(o => o.ShipmentNumber).ToList(),
 0515            User = userTask3.Result!,
 516            Suppliers = suppliers3.OrderBy(s => s.Name).ToList(),
 0517            RateUSD = rates3.RateUsd,
 0518            RateEUR = rates3.RateEur,
 0519            RateBYN = rates3.RateByn,
 0520            UniqueArticles = articles3,
 0521            UniqueDeliveryCitys = cities3,
 0522            UniqueNumbers = numbers3,
 0523            Warehouses = warehouses3,
 0524            OrderStatusWarnings = orderStatusWarnings,
 0525            AppStatus = appStatus!,
 0526            Treaties = await _oneCDataManager.GetTreatyListAsync(GetClientInn(ordersToTransaction))
 0527        };
 528
 0529        if (pageViewModel.User.AccessProfileId != null)
 0530            pageViewModel.User.AccessProfile = _context.AccessProfiles.Find(pageViewModel.User.AccessProfileId);
 531
 0532        if (pageViewModel.User.AccessProfile?.AvailableOrderColumns?.Contains("Себестоимость") == true && ordersToTransa
 533        {
 0534            await _orderServices.LoadExtraExpensesForOrders(ordersToTransaction);
 535        }
 536
 0537        await AttachStockCheckSenderMapAsync(pageViewModel.Orders);
 0538        RunBackgroundCacheTask(pageViewModel.Orders);
 0539        return View(pageViewModel);
 0540    }
 541
 542    private async Task AttachStockCheckSenderMapAsync(IEnumerable<Order> orders)
 543    {
 0544        var warehouseMappings = await _warehouseMappingDataServices.GetWarehouseMappings();
 0545        ViewData["StockCheckSenderByOrderId"] = TransactionStockCheckLineBuilder.BuildStockCheckSenderMap(
 0546            orders,
 0547            warehouseMappings);
 0548    }
 549
 550
 551    private static string? GetClientInn(List<Order> orders) =>
 0552        orders
 0553            .Where(o => o.OzonClient != null && !string.IsNullOrWhiteSpace(o.OzonClient.INNCode))
 0554            .Select(o => o.OzonClient!.INNCode)
 0555            .FirstOrDefault();
 556
 557    private void RunBackgroundCacheTask(List<Order> orders)    {
 0558        _ = Task.Run(async () =>
 0559        {
 0560            foreach (var order in orders)
 0561            {
 0562                try
 0563                {
 0564                    if (order.EtProducer != null && !string.IsNullOrEmpty(order.Article))
 0565                    {
 0566                        _ = await _proxyHttpClientService.GetJsonAsync(
 0567                            $"https://api.interparts.ru/detail-full-info/{order.EtProducer.Name}/{order.Article}");
 0568
 0569                        _ = await _proxyHttpClientService.GetJsonAsync(
 0570                            $"https://api.interparts.ru/product-information/product/?article_number={order.Article}&manu
 0571
 0572                        Console.WriteLine($"Кэширован для проведения: {order.EtProducer.Name}/{order.Article}");
 0573                    }
 0574                }
 0575                catch (Exception ex)
 0576                {
 0577                    Console.WriteLine($"Ошибка в фоне: {ex.Message}");
 0578                }
 0579            }
 0580        });
 0581    }
 582
 583    // ==============================
 584    // POST: CreateShippedToClientTransaction
 585    // ==============================
 586    [HttpPost]
 587    [ValidateAntiForgeryToken]
 588    public async Task<IActionResult> CreateShippedToClientTransaction(
 589        List<Order> orders,
 590        string userName,
 591        string? comment,
 592        int page,
 593        string? deletedOrders,
 594        Dictionary<int, string?>? orderSupplierMap,
 595        bool processIn1C = false,
 596        string? contractLink = null,
 597        string? transactionComment = null,
 598        int? vatRate = null,
 599        DateTime? oneCOperationDate = null)
 600    {
 0601        var aiFlow = TransactionCommentAiSchedulingHelper.BeginFlow(
 0602            comment,
 0603            TransactionType.ShippedToClient,
 0604            userName,
 0605            processIn1C);
 0606        comment = aiFlow.UserCommentOnly;
 607
 608        // «Засыл» из формы является складом-источником с приоритетом над ShipmentWarehouse.
 609        // Сохраняем его до серверной проверки остатков и создания расходной накладной, иначе
 610        // выбранный на этой же странице m2out применялся только к следующей транзакции.
 0611        if (orderSupplierMap is { Count: > 0 })
 0612            await _orderServices.SetZasylAsync(orderSupplierMap);
 613
 0614        var trackingKey = Request.Form["TrackingKey"].FirstOrDefault();
 0615        var oneCOrganizationInn = Request.Form["oneCOrganizationInn"].FirstOrDefault();
 0616        var missingOrdersResult = TransactionErrorResponseHelper.EnsureOrdersProvided(
 0617            this,
 0618            orders,
 0619            () => RedirectToAction("Index"));
 0620        if (missingOrdersResult != null)
 621        {
 0622            return missingOrdersResult;
 623        }
 624
 0625        ArgumentNullException.ThrowIfNull(orders);
 626
 0627        if (deletedOrders != null)
 628        {
 0629            var deletedOrderIds = DeletedOrderIds.Parse(deletedOrders);
 0630            orders = orders.Where(o => !deletedOrderIds.Contains(o.Id)).ToList();
 631        }
 632
 633        // Очищаем пустые дополнительные расходы перед сохранением
 634        // Это гарантирует, что пустые элементы не попадут в базу данных
 0635        foreach (var order in orders)
 636        {
 0637            if (order.ExtraExpenses != null && order.ExtraExpenses.Any())
 638            {
 639                // Удаляем все расходы, где не заполнены оба поля (тип и сумма)
 0640                order.ExtraExpenses = order.ExtraExpenses
 641                    .Where(e => e != null && e.ExpenseTypeId > 0 && e.Amount > 0)
 0642                    .ToList();
 643
 644                // Если список стал пустым, устанавливаем null
 0645                if (!order.ExtraExpenses.Any())
 646                {
 0647                    order.ExtraExpenses = [];
 648                }
 649            }
 650        }
 651
 0652        var stockCheck = StockCheckResult.Empty;
 653
 654        try
 655        {
 0656            var processedOrdersForRedirect = new List<Order>();
 0657            var postTransitions = await _statusTransitionDataServices.GetByTransactionType(TransactionType.ShippedToClie
 0658            if (orders.Count != 0)
 659            {
 0660                DateTime createAt = DateTime.Now;
 0661                var changeCount = 0;
 0662                long transactionId = 0;
 0663                var dateTime = "";
 0664                List<Order> ordersToUpdate = new List<Order>();
 0665                List<string> ordersNotFoundInOzone = [];
 666                var orderIds = orders.Select(o => o.Id).Distinct().ToList();
 667
 668                // Сохраняем исходные статусы заказов для отката при ошибке 1С.
 0669                var originalStatuses = new Dictionary<int, AppStatus>();
 670                var validSourceStatusIds = postTransitions.Any() ? postTransitions.Select(t => t.FromStatusId).ToHashSet
 0671                var currentSourceStatusByOrderId = await _context.Orders.AsNoTracking()
 0672                    .Where(o => orderIds.Contains(o.Id))
 0673                    .Select(o => new { o.Id, o.AppStatusId, StatusName = o.AppStatus != null ? o.AppStatus.Name : null }
 674                    .ToDictionaryAsync(x => x.Id, x => new { x.AppStatusId, x.StatusName });
 675
 0676                var skippedOrders = new List<string>();
 0677                var eligibleOrders = new List<Order>();
 678
 0679                foreach (var order in orders)
 680                {
 0681                    var currentStatus = currentSourceStatusByOrderId.GetValueOrDefault(order.Id);
 0682                    var allowedStatus = validSourceStatusIds != null
 0683                        ? currentStatus?.AppStatusId.HasValue == true && validSourceStatusIds.Contains(currentStatus.App
 0684                        : string.Equals(currentStatus?.StatusName, "Заказан реализатору", StringComparison.OrdinalIgnore
 685
 0686                    if (!allowedStatus)
 687                    {
 0688                        skippedOrders.Add($"{order.ShipmentNumber} ({currentStatus?.StatusName ?? "без статуса"})");
 0689                        continue;
 690                    }
 691
 0692                    eligibleOrders.Add(order);
 693                }
 694
 0695                orders = eligibleOrders;
 696
 697                // Остатки спрашиваем до изменения статусов: при нехватке 1С создаст расходную
 698                // накладную, но откажет в проведении, не указав проблемные позиции.
 0699                if (processIn1C)
 700                {
 0701                    var stockOrders = await TransactionStockCheckLineBuilder.LoadForStockCheckAsync(
 702                        _context, orders.Select(o => o.Id));
 0703                    var warehouseMappings = await _warehouseMappingDataServices.GetWarehouseMappings();
 704
 0705                    stockCheck = await OneCStockGate.RunAsync(
 0706                        HttpContext,
 0707                        TransactionStockCheckLineBuilder.FromWarehouseMappings(
 0708                            stockOrders,
 0709                            warehouseMappings,
 0710                            null,
 0711                            TransactionStockCheckLineBuilder.QuantitiesFromForm(orders)));
 0712                    await OneCStockGate.ReportAsync(HttpContext, stockCheck, "Отгружен клиенту");
 713
 0714                }
 715
 0716                foreach (var order in orders)
 717                {
 0718                    var fromDb = await _orderServices.GetOrder(order.Id);
 0719                    if (fromDb?.AppStatus != null)
 0720                        originalStatuses[order.Id] = fromDb.AppStatus;
 0721                }
 722
 0723                var submittedFields = SubmittedOrderFields.FromForm(Request.HasFormContentType ? Request.Form : null);
 0724                foreach (var order in orders)
 725                {
 0726                    ordersToUpdate.Add(await _orderServices.TransactOrder(order, submittedFields.For(order.Id)));
 727                }
 728
 0729                AppStatus appStatus = postTransitions.Any()
 0730                    ? postTransitions.First().ToStatus!
 0731                    : (await _appStatusServices.GetAppStatusAsync(new AppStatus() { Name = "Отгружен клиенту" }))!;
 732                HashSet<int>? validTargetStatusIds = postTransitions.Any() ? postTransitions.Select(t => t.ToStatusId).T
 0733                var oldStatus =
 0734                    await _appStatusServices.GetAppStatusAsync(new AppStatus() { Name = "Заказан реализатору" });
 0735                var backStatus =
 0736                    await _appStatusServices.GetAppStatusAsync(new AppStatus() { Name = "Утерян реализатором" });
 0737                if (backStatus == null)
 738                {
 0739                    AppStatus newStatus = new AppStatus()
 0740                    {
 0741                        Name = "Утерян реализатором"
 0742                    };
 0743                    _context.AppStatuses.Add(newStatus);
 0744                    await _context.SaveChangesAsync();
 0745                    backStatus = newStatus;
 0746                }
 747
 0748                var cancelledStatus = await _appStatusServices.GetAppStatusAsync(new AppStatus() { Name = "Отменен" });
 749
 0750                List<Order> ordersToTransaction = validTargetStatusIds != null
 0751                    ? ordersToUpdate.Where(o => o.AppStatus != null && validTargetStatusIds.Contains(o.AppStatus.Id) && 
 0752                    : ordersToUpdate.Where(o => ((appStatus != null && o.AppStatus?.Id == appStatus.Id) || (cancelledSta
 0753                processedOrdersForRedirect = ordersToTransaction.ToList();
 0754                var blockingOrderIds = OneCStockGate.GetBlockingOrderIds(stockCheck);
 0755                var ordersForOneC = ordersToTransaction
 0756                    .Where(order => !blockingOrderIds.Contains(order.Id))
 0757                    .ToList();
 0758                var ordersForOneCDraft = ordersToTransaction
 0759                    .Where(order => blockingOrderIds.Contains(order.Id))
 0760                    .ToList();
 761
 0762                var skippedOrdersMessage = skippedOrders.Any()
 0763                    ? "<div class='alert alert-warning'><strong>Некоторые заказы пропущены</strong><br/>" +
 0764                      string.Join("<br/>", skippedOrders.Select(System.Net.WebUtility.HtmlEncode)) +
 0765                      "</div>"
 0766                    : string.Empty;
 767
 0768                if (!ordersToTransaction.Any())
 769                {
 0770                    TempData["TransactionResult"] = string.IsNullOrWhiteSpace(skippedOrdersMessage)
 0771                        ? "<div class='alert alert-warning'><strong>Нет подходящих заказов для проведения</strong></div>
 0772                        : skippedOrdersMessage;
 0773                    await _orderServices.SetZasylAsync(orderSupplierMap ?? new Dictionary<int, string?>());
 0774                    return Json(new { redirectTo = Url.Action("Index", "Orders", new { sortOrder = GetSortStateCookie(),
 775                }
 776                List<Order> ordersToBack = ordersToUpdate.Where(o => o.Quantity <= 0).ToList();
 0777                foreach (var order in ordersToBack)
 778                {
 0779                    order.AppStatus = backStatus;
 780                }
 781
 0782                int ordersToBackResult = await _orderServices.UpdateOrders(ordersToBack);
 783
 784
 0785                string oneCResult = "";
 0786                IEnumerable<string> oneCHash = [];
 0787                List<OneCResponse> transferResult = new List<OneCResponse>();
 0788                List<OneCResponse> draftTransferResult = new List<OneCResponse>();
 0789                if (processIn1C && ordersForOneC.Any())
 790                {
 791                    try
 792                    {
 0793                        transferResult =
 0794                            await _oneCExpenseManager.SaleStockByWarehouses(ordersForOneC,
 0795                                TransactionType.ShippedToClient,
 0796                                contractLink,
 0797                                transactionComment,
 0798                                vatRate,
 0799                                oneCOperationDate,
 0800                                userName,
 0801                                oneCOrganizationInn);
 0802                    }
 0803                    catch (Exception e)
 804                    {
 0805                        string msg = $"<div class='alert alert-danger'>" +
 0806                                     $"<strong>⚠️ Ошибка обработки в 1С</strong><br/>" +
 0807                                     $"{e.Message}</div>";
 0808                        foreach (var item in ordersForOneC)
 809                        {
 0810                            item.AppStatus = originalStatuses.TryGetValue(item.Id, out var orig) ? orig : oldStatus;
 811                        }
 812
 0813                        int ordersToOldResult = await _orderServices.UpdateOrders(ordersForOneC);
 0814                        if (ordersToOldResult > 0)
 815                        {
 0816                            msg += $"<div class='alert alert-info'>" +
 0817                                   $"<strong>📊 Обновление статусов</strong><br/>" +
 0818                                   $"Исходный статус восстановлен для <strong>{ordersToOldResult}</strong> заказов" +
 0819                                   $"</div>";
 820                        }
 821
 0822                        TempData["TransactionResult"] = msg;
 0823                        await _orderServices.SetZasylAsync(orderSupplierMap ?? new Dictionary<int, string?>());
 0824                        return Json(new
 0825                        {
 0826                                redirectTo = BuildOrdersIndexRedirectUrl(page, ordersToTransaction)
 0827                        });
 828                    }
 829
 830                    var successfulTransfers = transferResult.Where(tr => tr.Success).ToList();
 831                    var failedTransfers = transferResult.Where(tr => !tr.Success).ToList();
 832
 0833                    if (failedTransfers.Count > 0)
 834                    {
 835                        // Есть ошибки
 836                        var errorMessages = failedTransfers.Select(tr => tr.Message).Distinct().Take(3);
 0837                        var errorCount = failedTransfers.Count;
 0838                        var successCount = successfulTransfers.Count;
 0839                        var totalCount = transferResult.Count;
 840
 0841                        string msg = $"<div class='alert alert-warning'>" +
 0842                                     $"<strong>⚠️ Частичный результат обработки в 1С</strong><br/>" +
 0843                                     $"Успешно: {successCount} из {totalCount} документов<br/>" +
 0844                                     $"С ошибками: {errorCount} документов<br/>" +
 0845                                     $"<small>Ошибки: {string.Join("; ", errorMessages)}" +
 0846                                     (errorCount > 3 ? "..." : "") + "</small>" +
 0847                                     "</div>";
 848
 849
 0850                        foreach (var item in ordersForOneC)
 851                        {
 0852                            item.AppStatus = originalStatuses.TryGetValue(item.Id, out var orig) ? orig : oldStatus;
 853                        }
 854
 0855                        int ordersToOldResult = await _orderServices.UpdateOrders(ordersForOneC);
 0856                        if (ordersToOldResult > 0)
 857                        {
 0858                            msg += $"<div class='alert alert-info'>" +
 0859                                   $"<strong>📊 Обновление статусов</strong><br/>" +
 0860                                   $"Исходный статус восстановлен для <strong>{ordersToOldResult}</strong> заказов" +
 0861                                   $"</div>";
 862                        }
 863
 0864                        TempData["TransactionResult"] = msg;
 0865                        await _orderServices.SetZasylAsync(orderSupplierMap ?? new Dictionary<int, string?>());
 0866                        return Json(new
 0867                        {
 0868                            redirectTo = BuildOrdersIndexRedirectUrl(page, ordersToTransaction)
 0869                        });
 870                    }
 871                    else
 872                    {
 0873                        oneCHash = successfulTransfers
 874                            .Select(h => h.TransactionId)
 875                            .Where(id => id != null)
 876                            .Select(id => id!);
 0877                        if (ordersForOneCDraft.Any())
 878                        {
 0879                            draftTransferResult = await _oneCExpenseManager.SaleStockByWarehouses(
 0880                                ordersForOneCDraft,
 0881                                TransactionType.ShippedToClient,
 0882                                contractLink,
 0883                                transactionComment,
 0884                                vatRate,
 0885                                oneCOperationDate,
 0886                                userName,
 0887                                oneCOrganizationInn,
 0888                                createAsDraft: true,
 0889                                draftReason: "недостаточно остатка в 1С. Проведите после оприходования товара.");
 0890                            transferResult.AddRange(draftTransferResult);
 891                        }
 892
 0893                        oneCResult = OneCTransactionSummaryHelper.WrapSection(
 0894                                         "Проведённые документы 1С",
 0895                                         OneCTransactionSummaryHelper.BuildAlertSummary(ordersForOneC, successfulTransfe
 0896                                     + OneCTransactionSummaryHelper.WrapSection(
 0897                                         "Черновики 1С по дефицитным позициям",
 0898                                         OneCTransactionSummaryHelper.BuildAlertSummary(ordersForOneCDraft, draftTransfe
 0899                                     + OneCStockGate.PartialPostingNotice(
 0900                                         stockCheck,
 0901                                         ordersForOneC.Count,
 902                                         draftTransferResult.Any(result => result.Success))
 0903                                     + OneCStockGate.SkippedNotice(stockCheck);
 904                    }
 0905                }
 906                else
 907                {
 0908                    if (processIn1C && ordersForOneCDraft.Any())
 909                    {
 0910                        draftTransferResult = await _oneCExpenseManager.SaleStockByWarehouses(
 0911                            ordersForOneCDraft,
 0912                            TransactionType.ShippedToClient,
 0913                            contractLink,
 0914                            transactionComment,
 0915                            vatRate,
 0916                            oneCOperationDate,
 0917                            userName,
 0918                            oneCOrganizationInn,
 0919                            createAsDraft: true,
 0920                            draftReason: "недостаточно остатка в 1С. Проведите после оприходования товара.");
 0921                        transferResult.AddRange(draftTransferResult);
 922                    }
 923
 0924                    oneCResult = stockCheck.HasBlocking
 0925                        ? OneCTransactionSummaryHelper.WrapSection(
 0926                              "Черновики 1С по дефицитным позициям",
 0927                              OneCTransactionSummaryHelper.BuildAlertSummary(ordersForOneCDraft, draftTransferResult))
 0928                          + OneCStockGate.PartialPostingNotice(
 0929                              stockCheck,
 0930                              0,
 931                              draftTransferResult.Any(result => result.Success))
 0932                        : OneCStockGate.NoDocumentNotice(stockCheck);
 933                }
 934
 0935                if (ordersToBackResult > 0)
 936                {
 0937                    oneCResult += $"<div class='alert alert-info'>" +
 0938                                  $"<strong>📊 Обновление статусов</strong><br/>" +
 0939                                  $"Статус 'Утерян реализатором' установлен для <strong>{ordersToBackResult}</strong> за
 0940                                  $"</div>";
 941                }
 942
 0943                await NotificationService.NotifyAllAsync(oneCResult);
 0944                var historyWindowStart = DateTime.Now;
 0945                var commentWithOneC = comment;
 0946                TransactionCommentAiSchedulingHelper.AppendOneCCommentDetails(
 0947                    aiFlow,
 0948                    ref commentWithOneC,
 0949                    ordersToTransaction,
 0950                    transferResult);
 0951                var commentForDb = TransactionCommentAiSchedulingHelper.ResolveDatabaseComment(aiFlow, commentWithOneC);
 952
 0953                (changeCount, dateTime, transactionId) = await _transaction.CreateShippedToClientTransaction(ordersToTra
 0954                    userName,
 0955                    createAt,
 0956                    commentForDb);
 957                int cancelCount = ordersToTransaction.Count(o => o.AppStatus?.Name == "Отменен");
 958
 0959                if (changeCount > 0)
 960                {
 0961                    if (processIn1C && transferResult.Count > 0)
 962                    {
 0963                        await OrderHistoryOneCPostingRecorder.TryRecordExpenseAsync(
 0964                            HttpContext.RequestServices,
 0965                            ordersForOneC,
 966                            transferResult.Where(result => !result.IsDraft).ToList(),
 0967                            userName,
 0968                            transactionId > 0 ? (int?)transactionId : null,
 0969                            historyWindowStart);
 970                    }
 971
 972                    var fromStatusByOrderId = originalStatuses.ToDictionary(x => x.Key, x => x.Value.Id);
 0973                    var statusTransitionHistoryService = HttpContext.RequestServices.GetService<IStatusTransitionHistory
 0974                    if (postTransitions.Any() && statusTransitionHistoryService != null)
 975                    {
 0976                        var user = await _userCacheService.GetCachedUserAsync(User);
 0977                        await statusTransitionHistoryService.RecordStatusTransitionHistoryAsync(
 0978                            TransactionType.ShippedToClient,
 0979                            ordersToTransaction,
 0980                            fromStatusByOrderId,
 0981                            postTransitions,
 0982                            commentForDb,
 0983                            user?.AccessProfileId,
 0984                            createAt,
 0985                            null);
 986                    }
 987
 0988                    await _cacheUpdater.Update(_cache);
 989                    var statusNameForMessage = string.Join(", ", ordersToTransaction.Select(o => o.AppStatus?.Name).Wher
 0990                    if (string.IsNullOrEmpty(statusNameForMessage) && postTransitions.Any())
 991                        statusNameForMessage = string.Join(", ", postTransitions.Select(t => t.ToStatus?.Name).Where(n =
 0992                    if (string.IsNullOrEmpty(statusNameForMessage)) statusNameForMessage = "Отгружен клиенту";
 0993                    string msg = TransactionConfig.BuildStandardSuccessMessage(
 0994                        changeCount,
 0995                        cancelCount,
 0996                        dateTime,
 0997                        userName,
 0998                        commentForDb,
 0999                        ordersToTransaction,
 01000                        statusNameForMessage,
 01001                        oneCResult);
 01002                    if (!string.IsNullOrWhiteSpace(skippedOrdersMessage))
 1003                    {
 01004                        msg += skippedOrdersMessage;
 1005                    }
 01006                    await NotificationService.NotifyAllAsync(msg);
 01007                    TempData["TransactionResult"] = msg;
 01008                    await CompleteTrackingAsync(trackingKey, transactionId, ordersToTransaction.Count, commentForDb);
 1009
 01010                    if (aiFlow.ScheduleAiComment && aiFlow.Builder != null && transactionId > 0)
 1011                    {
 01012                        await TransactionCommentAiSchedulingHelper.TryScheduleAsync(
 01013                            HttpContext.RequestServices,
 01014                            aiFlow.Builder,
 01015                            true,
 01016                            (int)transactionId,
 01017                            ordersToTransaction,
 01018                            fromStatusByOrderId,
 01019                            _context,
 01020                            ct: HttpContext.RequestAborted);
 1021                    }
 01022                }
 1023
 01024                await _transactionCache.Update();
 1025
 01026                if (ordersNotFoundInOzone.Count > 0)
 1027                {
 01028                    TempData["OrdersNotFoundInOzone"] =
 1029                        $"Заказы не найдены в системе Ozon - {ordersNotFoundInOzone.Aggregate((x, y) => x + ", " + y)}";
 1030                }
 01031            }
 1032
 01033            ClearSelectedIdsSession();
 01034            foreach (var order in orders)
 1035            {
 01036                var cookieKey = $"PurchasePrice_{order.Id}";
 01037                if (Request.Cookies.ContainsKey(cookieKey))
 1038                {
 01039                    Response.Cookies.Delete(cookieKey);
 1040                }
 1041            }
 1042
 01043            await _orderServices.SetZasylAsync(orderSupplierMap ?? new Dictionary<int, string?>());
 01044            return Json(new
 01045                { redirectTo = BuildOrdersIndexRedirectUrl(page, processedOrdersForRedirect) });
 1046        }
 01047        catch (TransactionException ex)
 1048        {
 01049            TempData["TransactionErrorCode"] = ex.Code.ToString();
 01050            TempData["ErorrResult"] = ex.Message;
 01051            await _orderServices.SetZasylAsync(orderSupplierMap ?? new Dictionary<int, string?>());
 01052            return Json(new
 01053                { redirectTo = Url.Action("Index", "Orders", new { sortOrder = GetSortStateCookie(), page }) });
 1054        }
 01055        catch (Exception ex)
 1056        {
 01057            TempData["TransactionErrorCode"] = TransactionErrorCode.Unknown.ToString();
 01058            TempData["ErorrResult"] = $"Не удалось провести выбранные заказы ({ex.Message})";
 01059            await _orderServices.SetZasylAsync(orderSupplierMap ?? new Dictionary<int, string?>());
 01060            return Json(new
 01061                { redirectTo = Url.Action("Index", "Orders", new { sortOrder = GetSortStateCookie(), page }) });
 1062        }
 01063    }
 1064
 1065    [HttpPost]
 1066    public IActionResult ClearSelectedIdsSession()
 1067    {
 01068        HttpContext.Session.SetString("selectedIds", JsonConvert.SerializeObject(new List<int>()));
 01069        return Ok();
 1070    }
 1071
 1072    private static DateTime GetDefaultOneCOperationDate()
 1073    {
 01074        return TimeZoneInfo.ConvertTimeFromUtc(DateTime.UtcNow, RussianTimeZone);
 1075    }
 1076
 1077    private static DateTime GetEffectiveOneCOperationDate(DateTime? oneCOperationDate)
 1078    {
 01079        return oneCOperationDate ?? GetDefaultOneCOperationDate();
 1080    }
 1081
 1082    private async Task AttachTrackingKeyAsync(MultiplayEditOrderViewModel model, TransactionType transactionType)
 1083    {
 01084        if (model.Orders == null || !model.Orders.Any())
 01085            return;
 01086        var trackingService = HttpContext.RequestServices.GetService<ITransactionTimeTrackingService>();
 01087        if (trackingService == null)
 01088            return;
 01089        model.TrackingKey = await trackingService.StartSessionAsync(
 01090            User.Identity?.Name ?? "unknown",
 01091            model.User?.AccessProfileId,
 01092            transactionType,
 1093            model.Orders.Select(o => o.Id),
 01094            $"{Request.Path}{Request.QueryString}",
 01095            HttpContext.Connection.RemoteIpAddress?.ToString(),
 01096            Request.Headers.UserAgent.ToString());
 01097    }
 1098
 1099    private async Task CompleteTrackingAsync(string? trackingKey, long transactionId, int ordersCount, string? comment)
 1100    {
 01101        if (string.IsNullOrWhiteSpace(trackingKey) || transactionId <= 0)
 01102            return;
 01103        var trackingService = HttpContext.RequestServices.GetService<ITransactionTimeTrackingService>();
 01104        if (trackingService == null)
 01105            return;
 01106        await trackingService.CompleteSessionAsync(
 01107            trackingKey,
 01108            transactionId,
 01109            ordersCount,
 01110            comment,
 01111            $"/Studio2/Transaction/GetTransaction?transactionId={transactionId}");
 01112    }
 1113
 1114    private string BuildOrdersIndexRedirectUrl(int page, IEnumerable<Order> orders)
 1115    {
 01116        var shipmentNumbers = orders
 01117            .Select(o => o.ShipmentNumber?.Trim())
 01118            .Where(n => !string.IsNullOrWhiteSpace(n))
 01119            .Distinct(StringComparer.OrdinalIgnoreCase)
 01120            .ToList();
 1121
 01122        var filterData = new OrderFilterModel
 01123        {
 01124            ShipmentNumber = shipmentNumbers.Any() ? string.Join(",", shipmentNumbers) : null,
 01125            GroupByShipment = true,
 01126            IncludeArchived = false,
 01127            SyncClientStatusesOnLoad = false,
 01128            OnlyWithZasyl = false
 01129        };
 1130
 01131        Response.Cookies.Append("FilterData", JsonConvert.SerializeObject(filterData));
 1132
 01133        return Url.Action("Index", "Orders", new
 01134               {
 01135                   sortOrder = GetSortStateCookie(),
 01136                   page
 01137               })
 01138               ?? "/Studio2/Orders";
 1139    }
 1140
 1141    private static string? BuildSkippedOrdersWarningHtml(IEnumerable<string> warningMessages)
 1142    {
 01143        var warnings = warningMessages
 01144            .Where(m => !string.IsNullOrWhiteSpace(m))
 01145            .Distinct(StringComparer.OrdinalIgnoreCase)
 01146            .ToList();
 1147
 01148        if (!warnings.Any())
 1149        {
 01150            return null;
 1151        }
 1152
 01153        return "<ul class='mb-0'>" + string.Join("", warnings.Select(w => $"<li>{System.Net.WebUtility.HtmlEncode(w)}</l
 1154    }
 1155
 1156    private OrderSortState GetSortStateCookie()
 1157    {
 01158        var cookie = Request.Cookies["SortState"];
 01159        if (!string.IsNullOrEmpty(cookie) && Enum.TryParse<OrderSortState>(cookie, out var saved))
 01160            return saved;
 01161        return OrderSortState.StandardState;
 1162    }
 1163}

Methods/Properties

.cctor()
.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,OzonRepositories.Data.Bitrix.BitrixStockRepository,Services.CacheServices.Cache.CacheUpdater`1<OzonDomains.Models.Order>,Services.CacheServices.Cache.OzonOrdersCache.OrderCache,PartsInfo.HttpUtils.ProxyHttpClientService,Services.ApiServices._1CApi.OneCTransferManager,Services.ApiServices._1CApi.OneCExpenseManager,Services.ApiServices.OneCDataManager,Services.DataServices.WarehouseDataServices,Services.DataServices.WarehouseMappingDataServices,Services.DataServices.StatusTransitionDataServices)
CreateShippedToClientTransactionFromFile()
CreateShippedToClientTransactionFromFileView()
CreateShippedToClientTransaction()
AttachStockCheckSenderMapAsync()
GetClientInn(System.Collections.Generic.List`1<OzonDomains.Models.Order>)
RunBackgroundCacheTask(System.Collections.Generic.List`1<OzonDomains.Models.Order>)
CreateShippedToClientTransaction()
ClearSelectedIdsSession()
GetDefaultOneCOperationDate()
GetEffectiveOneCOperationDate(System.Nullable`1<System.DateTime>)
AttachTrackingKeyAsync()
CompleteTrackingAsync()
BuildOrdersIndexRedirectUrl(System.Int32,System.Collections.Generic.IEnumerable`1<OzonDomains.Models.Order>)
BuildSkippedOrdersWarningHtml(System.Collections.Generic.IEnumerable`1<System.String>)
GetSortStateCookie()