< Summary

Information
Class: OzonOrdersWeb.Areas.Studio2.Controllers.Transactions.WarehouseTransferTransactionController
Assembly: OzonOrdersWeb
File(s): /home/runner/work/Studio2Prod/Studio2Prod/OzonOrdersWeb/Areas/Studio2/Controllers/Transactions/WarehouseTransferTransactionController.cs
Line coverage
0%
Covered lines: 0
Uncovered lines: 561
Coverable lines: 561
Total lines: 937
Line coverage: 0%
Branch coverage
0%
Covered branches: 0
Total branches: 244
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/WarehouseTransferTransactionController.cs

#LineLine coverage
 1using Microsoft.AspNetCore.Authorization;
 2using Microsoft.AspNetCore.Mvc;
 3using Microsoft.AspNetCore.Mvc.Rendering;
 4using Microsoft.Extensions.DependencyInjection;
 5using Newtonsoft.Json;
 6using OzonDomains;
 7using OzonDomains.Models;
 8using OzonOrdersWeb.Areas.Studio2.Controllers.Base;
 9using OzonOrdersWeb.Areas.Studio2.Helpers;
 10using OzonOrdersWeb.Areas.Studio2.Models.Transactions;
 11using Services.BackgroundJobsServices;
 12using OzonOrdersWeb.ViewModels.OrderViewModels;
 13using OzonRepositories.Context;
 14using OzonRepositories.Data.Bitrix;
 15using PartsInfo.HttpUtils;
 16using System.Security.Claims;
 17using Microsoft.EntityFrameworkCore;
 18using Services.ApiServices;
 19using Services.ApiServices._1CApi;
 20using Services.ApiServices._1CApi.Models;
 21using Services.ApiServices._1CApi.Stock;
 22using Services.Authorization.Services;
 23using Services.CacheServices.Cache.OzonOrdersCache;
 24using Services.CacheServices.Cache.UserCacheService;
 25using Services.DataServices;
 26using Services.FiltersServices.SortModels;
 27using OzonOrdersWeb.WebServices.Currency;
 28using Services.SignalRServices;
 29using Services.TransactionUtilsServices;
 30using OzonOrdersWeb.TransactionServices;
 31using Services.CacheServices.Cache;
 32
 33namespace OzonOrdersWeb.Areas.Studio2.Controllers.Transactions
 34{
 35    /// <summary>
 36    /// Контроллер для транзакции "Перемещение между складами"
 37    /// Позволяет выполнить перемещение товаров с явным указанием складов отправителя и получателя
 38    /// </summary>
 39    [Authorize(Roles = "User,Admin")]
 40    [Area("Studio2")]
 41    [Route("Studio2/WarehouseTransferTransaction")]
 42    public class WarehouseTransferTransactionController : Controller
 43    {
 44        private readonly ApplicationDbContext _context;
 45        private readonly AppStatusDataServices _appStatusServices;
 46        private readonly OrdersDataServices _orderServices;
 47        private readonly TransactionManager _transaction;
 48        private readonly TransactionCache _transactionCache;
 49        private readonly SupplierDataServices _supplierDataServices;
 50        private readonly IUserCacheService _userCacheService;
 51        private readonly ICurrencyRatesViewBagService _currencyRatesViewBagService;
 52        private readonly CacheUpdater<Order> _cacheUpdater;
 53        private readonly OrderCache _cache;
 54        private readonly OneCTransferManager _oneCTransferManager;
 55        private readonly OneCDataManager _oneCDataManager;
 56        private readonly ProxyHttpClientService _proxyHttpClientService;
 57        private readonly WarehouseDataServices _warehouseDataServices;
 58        private readonly StatusTransitionDataServices _statusTransitionDataServices;
 59        private readonly IScopeFilterValuesService _scopeFilterValuesService;
 60        private readonly WarehouseSelectionResolver _warehouseSelectionResolver;
 61
 062        public WarehouseTransferTransactionController(
 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            CacheUpdater<Order> cacheUpdater,
 072            OrderCache cache,
 073            OneCTransferManager oneCTransferManager,
 074            OneCDataManager oneCDataManager,
 075            ProxyHttpClientService proxyHttpClientService,
 076            WarehouseDataServices warehouseDataServices,
 077            StatusTransitionDataServices statusTransitionDataServices,
 078            IScopeFilterValuesService scopeFilterValuesService,
 079            WarehouseSelectionResolver warehouseSelectionResolver)
 80        {
 081            _context = context;
 082            _appStatusServices = appStatusServices;
 083            _orderServices = orderServices;
 084            _transaction = transaction;
 085            _transactionCache = transactionCache;
 086            _supplierDataServices = supplierDataServices;
 087            _userCacheService = userCacheService;
 088            _currencyRatesViewBagService = currencyRatesViewBagService;
 089            _cacheUpdater = cacheUpdater;
 090            _cache = cache;
 091            _oneCTransferManager = oneCTransferManager;
 092            _oneCDataManager = oneCDataManager;
 093            _proxyHttpClientService = proxyHttpClientService;
 094            _warehouseDataServices = warehouseDataServices;
 095            _statusTransitionDataServices = statusTransitionDataServices;
 096            _scopeFilterValuesService = scopeFilterValuesService;
 097            _warehouseSelectionResolver = warehouseSelectionResolver;
 098        }
 99
 100        // ==============================
 101        // GET: CreateWarehouseTransferTransaction
 102        // ==============================
 103        [HttpGet]
 104        [Route("CreateWarehouseTransferTransaction")]
 105        public async Task<IActionResult> CreateWarehouseTransferTransaction(string ids, int page)
 106        {
 0107            var transitions =
 0108                await _statusTransitionDataServices.GetByTransactionType(TransactionType.WarehouseTransfer);
 109            AppStatus appStatus;
 0110            HashSet<int>? validSourceStatusIds = null;
 111
 0112            if (transitions.Any())
 113            {
 0114                validSourceStatusIds = transitions.Select(t => t.FromStatusId).ToHashSet();
 0115                appStatus = transitions.First().ToStatus!;
 116            }
 117            else
 118            {
 0119                var appStatusWarehouseTransfer =
 0120                    await _appStatusServices.GetAppStatusAsync(new AppStatus { Name = "Перемещен между складами" });
 0121                if (appStatusWarehouseTransfer == null)
 122                {
 0123                    appStatusWarehouseTransfer = new AppStatus { Name = "Перемещен между складами" };
 0124                    _context.AppStatuses.Add(appStatusWarehouseTransfer);
 0125                    await _context.SaveChangesAsync();
 126                }
 127
 0128                appStatus = appStatusWarehouseTransfer;
 0129            }
 130
 0131            var cancelledStatus = await _appStatusServices.GetAppStatusAsync(new AppStatus { Name = "Отменен" });
 0132            var filteredStatuses = new List<AppStatus> { appStatus };
 0133            if (cancelledStatus != null) filteredStatuses.Add(cancelledStatus);
 0134            filteredStatuses = filteredStatuses.GroupBy(s => s.Id).Select(g => g.First()).ToList();
 135
 0136            ViewBag.AppStatuses = new SelectList(filteredStatuses, "Id", "Name");
 0137            ViewBag.StatusColors = filteredStatuses.ToDictionary(s => s.Id.ToString(), s => s.GetStatusColor());
 138
 0139            var bitrixWarehouses = await _oneCDataManager.GetWarehouseListAsync();
 0140            var bitrixWarehouseNames = (await FilterBitrixWarehouseNamesByScope(
 0141                bitrixWarehouses
 0142                    .Select(w => w.Name)
 0143                    .Where(n => !string.IsNullOrWhiteSpace(n))
 0144                    .Select(n => n!)
 0145                    .OrderBy(n => n)
 0146                    .ToList())).ToList();
 0147            ViewBag.BitrixWarehouses = new SelectList(bitrixWarehouseNames);
 0148            ViewData["BitrixWarehousesList"] = bitrixWarehouseNames;
 149
 0150            if (string.IsNullOrWhiteSpace(ids))
 0151                return BadRequest("Не выбраны заказы для транзакции.");
 152
 0153            int[] idArray = ids.Split(',', StringSplitOptions.RemoveEmptyEntries).Select(int.Parse).ToArray();
 154
 0155            if (idArray.Length == 0)
 0156                return BadRequest("Не выбраны заказы для транзакции.");
 157
 0158            List<Order> ordersToTransaction = new();
 0159            string? errorReason = null;
 0160            var errorCode = TransactionErrorCode.Unknown;
 161
 162            // ВАЖНО: Загружаем заказы БЕЗ ТРЕКИНГА - изменения статусов для отображения
 163            // НЕ сохранятся в БД при отмене транзакции
 0164            var orders = await _orderServices.GetOrdersNoTracking(idArray);
 165
 0166            foreach (var order in orders)
 167            {
 0168                bool notCancelled = order.Status != "Отменен покупателем"
 0169                                    && !(order.AppStatus?.Name == "Отменен" && order.Status == "Отменён");
 0170                bool allowedByStatus = validSourceStatusIds == null ||
 0171                                       (order.AppStatusId.HasValue &&
 0172                                        validSourceStatusIds.Contains(order.AppStatusId.Value));
 173
 0174                if (notCancelled && allowedByStatus)
 175                {
 0176                    ordersToTransaction.Add(order);
 177                }
 178            }
 179
 0180            if (!ordersToTransaction.Any())
 181            {
 0182                errorReason = "Нет заказов, доступных для перемещения между складами.";
 0183                errorCode = TransactionErrorCode.NoOrdersForTransaction;
 184            }
 185
 0186            var isAdmin = User.IsInRole("Admin");
 0187            ViewData["ForceSenderWarehouse"] = !isAdmin;
 0188            var senderResolution = await _warehouseSelectionResolver.ResolveSenderWarehouseAsync(ordersToTransaction);
 0189            var warehouseBlockingErrors = new List<string>();
 0190            if (!string.IsNullOrWhiteSpace(senderResolution.SenderWarehouseName))
 191            {
 0192                ViewData["FixedSenderWarehouse"] = senderResolution.SenderWarehouseName;
 0193                ViewData["PreferredSenderWarehouse"] = senderResolution.SenderWarehouseName;
 194
 0195                if (!bitrixWarehouseNames.Contains(senderResolution.SenderWarehouseName, StringComparer.OrdinalIgnoreCas
 196                {
 0197                    warehouseBlockingErrors.Add(
 0198                        $"Склад отправителя '{senderResolution.SenderWarehouseName}', полученный из настроек клиента Ozo
 199                }
 0200                else if (!bitrixWarehouseNames.Contains(senderResolution.SenderWarehouseName, StringComparer.OrdinalIgno
 201                {
 0202                    ViewData["WarehouseSenderRuleError"] =
 0203                        $"Автоопределенный склад отправителя '{senderResolution.SenderWarehouseName}' недоступен по теку
 0204                    ViewData["WarehouseSenderRuleSeverity"] = "soft";
 205                }
 206            }
 0207            else if (senderResolution.HasErrors && !isAdmin)
 208            {
 0209                warehouseBlockingErrors.AddRange(senderResolution.Errors);
 210            }
 0211            else if (senderResolution.HasErrors && isAdmin)
 212            {
 0213                ViewData["WarehouseSenderRuleError"] =
 0214                    "Автоопределение склада отправителя не выполнено. Выберите склад отправителя вручную из списка.";
 0215                ViewData["WarehouseSenderRuleSeverity"] = "soft";
 216            }
 217
 218            // Для не-админа получатель: в приоритете склады из WarehouseMapping по (клиент + склад заказа),
 219            // но также разрешаем выбрать любой другой доступный склад.
 0220            var recipientResolution = await _warehouseSelectionResolver.ResolveRecipientWarehousesAsync(ordersToTransact
 0221            if (recipientResolution.Warnings.Count > 0)
 222            {
 0223                ViewData["WarehouseRecipientRuleInfo"] = string.Join("<br/>", recipientResolution.Warnings);
 224            }
 0225            if (recipientResolution.HasBlockingErrors)
 226            {
 0227                warehouseBlockingErrors.AddRange(recipientResolution.BlockingErrors);
 228            }
 229
 0230            var preferredSet = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
 0231            if (recipientResolution.PreferredRecipientWarehouses.Count > 0)
 232            {
 0233                preferredSet = recipientResolution.PreferredRecipientWarehouses
 0234                    .ToHashSet(StringComparer.OrdinalIgnoreCase);
 235
 0236                bitrixWarehouseNames = bitrixWarehouseNames
 0237                    .OrderBy(n => preferredSet.Contains(n) ? 0 : 1)
 0238                    .ThenBy(n => n)
 0239                    .ToList();
 240            }
 241
 0242            if (!isAdmin)
 243            {
 0244                if (senderResolution.HasErrors)
 245                {
 0246                    ViewData["WarehouseSenderRuleError"] = string.Join("<br/>", senderResolution.Errors);
 247                }
 248
 249                // Дополнительно убираем из адресатов выбранный источник, если он определился.
 0250                if (!string.IsNullOrWhiteSpace(senderResolution.SenderWarehouseName))
 251                {
 0252                    bitrixWarehouseNames = bitrixWarehouseNames
 0253                        .Where(n => !string.Equals(n, senderResolution.SenderWarehouseName, StringComparison.OrdinalIgno
 0254                        .OrderBy(n => n)
 0255                        .ToList();
 0256                    ViewData["BitrixWarehousesList"] = bitrixWarehouseNames;
 257                }
 258
 259            }
 260
 261            // Подставляем первый доступный приоритетный склад получателя (для админа и не-админа).
 0262            var preferredRecipient = bitrixWarehouseNames
 0263                .FirstOrDefault(n => preferredSet.Contains(n));
 0264            if (!string.IsNullOrWhiteSpace(preferredRecipient))
 265            {
 0266                ViewData["PreferredRecipientWarehouse"] = preferredRecipient;
 267            }
 268
 0269            if (warehouseBlockingErrors.Count > 0)
 270            {
 0271                ViewData["WarehouseSelectionBlocked"] = true;
 0272                ViewData["WarehouseSenderRuleError"] = string.Join("<br/>",
 0273                    warehouseBlockingErrors.Distinct(StringComparer.OrdinalIgnoreCase));
 274            }
 275
 0276            ViewData["BitrixWarehousesList"] = bitrixWarehouseNames;
 277
 0278            ViewData["TransactionErrorCode"] = errorCode.ToString();
 0279            ViewData["ErrorReason"] = errorReason;
 280
 0281            var conditionsMessage = transitions.Any()
 0282                ? string.Join("<br/>", transitions
 0283                    .Select(t => t.Conditions)
 0284                    .Where(c => !string.IsNullOrWhiteSpace(c))
 0285                    .Select(c => System.Net.WebUtility.HtmlEncode(c!.Trim()))
 0286                    .Distinct())
 0287                : null;
 0288            ViewData["TransactionConditionsMessage"] = conditionsMessage;
 289
 290            // === ОПТИМИЗАЦИЯ: non-DB параллельно, DB последовательно ===
 0291            var userTask = _userCacheService.GetCachedUserAsync(User);
 0292            var rateTasks = _currencyRatesViewBagService.StartFetch();
 293
 0294            var suppliers = await _supplierDataServices.GetSuppliers();
 0295            var articles = await _orderServices.GetArticleCountsForOrdersAsync(ordersToTransaction);
 0296            var cities = await _orderServices.GetDeliveryCityCountsForOrdersAsync(ordersToTransaction);
 0297            var numbers = await _orderServices.GetShipmentBaseCountsForOrdersAsync(ordersToTransaction);
 0298            var warehouses = await _warehouseDataServices.GetWarehouses();
 299
 0300            await Task.WhenAll(userTask, rateTasks.Usd, rateTasks.Eur, rateTasks.Byn);
 301
 0302            var rates = rateTasks.ToSnapshot();
 0303            var pageViewModel = new MultiplayEditOrderViewModel
 0304            {
 0305                RedirectPage = page,
 0306                Orders = ordersToTransaction.OrderBy(o => o.ShipmentNumber).ToList(),
 0307                User = userTask.Result!,
 0308                Suppliers = suppliers.OrderBy(s => s.Name).ToList(),
 0309                RateUSD = rates.RateUsd,
 0310                RateEUR = rates.RateEur,
 0311                RateBYN = rates.RateByn,
 0312                UniqueArticles = articles,
 0313                UniqueDeliveryCitys = cities,
 0314                UniqueNumbers = numbers,
 0315                AppStatus = appStatus,
 0316                Warehouses = warehouses
 0317            };
 0318            await AttachTrackingKeyAsync(pageViewModel, TransactionType.WarehouseTransfer);
 319
 0320            if (pageViewModel.User.AccessProfileId != null)
 321            {
 0322                pageViewModel.User.AccessProfile = _context.AccessProfiles.Find(pageViewModel.User.AccessProfileId);
 323            }
 324
 0325            if (pageViewModel.User.AccessProfile?.AvailableOrderColumns?.Contains("Себестоимость") == true && ordersToTr
 326            {
 0327                await _orderServices.LoadExtraExpensesForOrders(ordersToTransaction);
 328            }
 329
 0330            RunBackgroundCacheTask(pageViewModel.Orders);
 0331            return View(pageViewModel);
 0332        }
 333
 334        private void RunBackgroundCacheTask(List<Order> orders)
 335        {
 0336            if (_proxyHttpClientService == null) return;
 337
 0338            _ = Task.Run(async () =>
 0339            {
 0340                foreach (var order in orders)
 0341                {
 0342                    try
 0343                    {
 0344                        if (order.EtProducer != null && !string.IsNullOrEmpty(order.Article))
 0345                        {
 0346                            await _proxyHttpClientService.GetJsonAsync(
 0347                                $"https://api.interparts.ru/detail-full-info/{order.EtProducer.Name}/{order.Article}");
 0348                            await _proxyHttpClientService.GetJsonAsync(
 0349                                $"https://api.interparts.ru/product-information/product/?article_number={order.Article}&
 0350                        }
 0351                    }
 0352                    catch
 0353                    {
 0354                        /* Игнорируем ошибки фонового кэширования */
 0355                    }
 0356                }
 0357            });
 0358        }
 359
 360        // ==============================
 361        // POST: CreateWarehouseTransferTransaction
 362        // ==============================
 363        [HttpPost]
 364        [ValidateAntiForgeryToken]
 365        [Route("CreateWarehouseTransferTransaction")]
 366        public async Task<IActionResult> CreateWarehouseTransferTransaction(
 367            List<Order> orders,
 368            string userName,
 369            string? comment,
 370            int page,
 371            string? deletedOrders,
 372            string warehouseSender,
 373            string warehouseRecipient,
 374            bool processIn1C = false)
 375        {
 0376            var aiFlow = TransactionCommentAiSchedulingHelper.BeginFlow(
 0377                comment,
 0378                TransactionType.WarehouseTransfer,
 0379                userName,
 0380                processIn1C);
 0381            comment = aiFlow.UserCommentOnly;
 0382            var trackingKey = Request.Form["TrackingKey"].FirstOrDefault();
 0383            var oneCOrganizationInn = Request.Form["oneCOrganizationInn"].FirstOrDefault();
 0384            var missingOrdersResult = TransactionErrorResponseHelper.EnsureOrdersProvided(
 0385                this,
 0386                orders,
 0387                () => RedirectToAction("Index", "Orders"));
 0388            if (missingOrdersResult != null)
 389            {
 0390                return missingOrdersResult;
 391            }
 392
 0393            ArgumentNullException.ThrowIfNull(orders);
 394
 395            // Удаляем исключенные заказы
 0396            if (!string.IsNullOrEmpty(deletedOrders))
 397            {
 0398                var deletedOrderIds = DeletedOrderIds.Parse(deletedOrders);
 0399                orders = orders.Where(o => !deletedOrderIds.Contains(o.Id)).ToList();
 400            }
 401
 0402            var stockCheck = StockCheckResult.Empty;
 403
 404            // Валидация складов
 0405            if (processIn1C)
 406            {
 0407                var bitrixWarehouses = await _oneCDataManager.GetWarehouseListAsync();
 0408                var                 bitrixWarehouseNames = (await FilterBitrixWarehouseNamesByScope(
 0409                    bitrixWarehouses
 0410                        .Select(w => w.Name)
 0411                        .Where(n => !string.IsNullOrWhiteSpace(n))
 0412                        .Select(n => n!)
 0413                        .OrderBy(n => n)
 0414                        .ToList())).ToList();
 415
 0416                var isAdmin = User.IsInRole("Admin");
 0417                var senderResolution = await _warehouseSelectionResolver.ResolveSenderWarehouseAsync(orders);
 0418                var recipientResolution = await _warehouseSelectionResolver.ResolveRecipientWarehousesAsync(orders);
 0419                var blockingErrors = new List<string>();
 420
 0421                if (!isAdmin && (senderResolution.HasErrors || string.IsNullOrWhiteSpace(senderResolution.SenderWarehous
 422                {
 0423                    blockingErrors.AddRange(senderResolution.Errors);
 0424                    if (!senderResolution.HasErrors)
 425                    {
 0426                        blockingErrors.Add("Не удалось определить склад отправителя по правилу 'склад покупателя'.");
 427                    }
 428                }
 0429                else if (!isAdmin && !string.IsNullOrWhiteSpace(senderResolution.SenderWarehouseName) &&
 0430                         !bitrixWarehouseNames.Contains(senderResolution.SenderWarehouseName, StringComparer.OrdinalIgno
 431                {
 0432                    blockingErrors.Add(
 0433                        $"Склад отправителя '{senderResolution.SenderWarehouseName}', полученный из настроек клиента Ozo
 434                }
 435
 0436                if (blockingErrors.Count > 0)
 437                {
 0438                    throw new TransactionException(
 0439                        TransactionErrorCode.InvalidSourceStatus,
 0440                        string.Join("<br/>", blockingErrors.Distinct(StringComparer.OrdinalIgnoreCase)));
 441                }
 442
 0443                if (!isAdmin)
 444                {
 0445                    warehouseSender = senderResolution.SenderWarehouseName ?? string.Empty;
 446                }
 447
 0448                if (string.IsNullOrEmpty(warehouseSender))
 449                {
 0450                    throw new TransactionException(
 0451                        TransactionErrorCode.OneCSenderWarehouseMissing,
 0452                        "Не указан склад отправителя.");
 453                }
 454
 0455                if (!bitrixWarehouseNames.Contains(warehouseSender, StringComparer.OrdinalIgnoreCase))
 456                {
 0457                    throw new TransactionException(
 0458                        TransactionErrorCode.OneCSenderWarehouseMissing,
 0459                        "Склад отправителя недоступен в списке складов 1С или по текущим правам.");
 460                }
 461
 0462                if (string.IsNullOrEmpty(warehouseRecipient))
 463                {
 0464                    throw new TransactionException(
 0465                        TransactionErrorCode.OneCRecipientWarehouseMissing,
 0466                        "Не указан склад получателя.");
 467                }
 468
 0469                if (warehouseSender == warehouseRecipient)
 470                {
 0471                    throw new TransactionException(
 0472                        TransactionErrorCode.OneCWarehousesMustDiffer,
 0473                        "Склад отправителя и получателя не могут совпадать.");
 474                }
 475
 476                // Остатки спрашиваем до изменения статусов: при нехватке 1С создаст документ, но
 477                // откажет в проведении, а сообщение об отказе не содержит проблемных позиций.
 0478                var stockOrders = await TransactionStockCheckLineBuilder.LoadForStockCheckAsync(
 0479                    _context, orders.Select(o => o.Id));
 480
 0481                stockCheck = await OneCStockGate.RunAsync(
 0482                    HttpContext,
 0483                    TransactionStockCheckLineBuilder.FromExplicitWarehouse(
 0484                        stockOrders,
 0485                        warehouseSender,
 0486                        TransactionStockCheckLineBuilder.QuantitiesFromForm(orders)));
 0487                await OneCStockGate.ReportAsync(HttpContext, stockCheck, "Перемещение между складами");
 488
 0489            }
 490
 491            try
 492            {
 0493                if (orders.Count != 0)
 494                {
 0495                    DateTime createAt = DateTime.Now;
 0496                    int changeCount = 0;
 0497                    long transactionId = 0;
 0498                    string dateTime = "";
 0499                    List<Order> ordersToUpdate = new();
 500
 501                    // Сохраняем исходные статусы заказов для возможного отката
 0502                    var originalStatuses = new Dictionary<int, AppStatus>();
 0503                    foreach (var order in orders)
 504                    {
 0505                        var originalOrder = await _orderServices.GetOrder(order.Id);
 0506                        if (originalOrder?.AppStatus != null)
 507                        {
 0508                            originalStatuses[order.Id] = originalOrder.AppStatus;
 509                        }
 0510                    }
 511
 512                    // Очищаем пустые дополнительные расходы перед сохранением
 513                    // Это гарантирует, что пустые элементы не попадут в базу данных
 0514                    foreach (var order in orders)
 515                    {
 0516                        if (order.ExtraExpenses != null && order.ExtraExpenses.Any())
 517                        {
 518                            // Удаляем все расходы, где не заполнены оба поля (тип и сумма)
 0519                            order.ExtraExpenses = order.ExtraExpenses
 0520                                .Where(e => e != null && e.ExpenseTypeId > 0 && e.Amount > 0)
 0521                                .ToList();
 522
 523                            // Если список стал пустым, устанавливаем null
 0524                            if (!order.ExtraExpenses.Any())
 525                            {
 0526                                order.ExtraExpenses = [];
 527                            }
 528                        }
 529                    }
 530
 0531                    var postTransitions =
 0532                        await _statusTransitionDataServices.GetByTransactionType(TransactionType.WarehouseTransfer);
 0533                    AppStatus appStatus = postTransitions.Any()
 0534                        ? postTransitions.First().ToStatus!
 0535                        : (await _appStatusServices.GetAppStatusAsync(
 0536                            new AppStatus { Name = "Перемещен между складами" }))!;
 0537                    var cancelledStatus =
 0538                        await _appStatusServices.GetAppStatusAsync(new AppStatus { Name = "Отменен" });
 539
 0540                    var submittedFields = SubmittedOrderFields.FromForm(Request.HasFormContentType ? Request.Form : null
 541
 542                    // Подготавливаем заказы для транзакции БЕЗ изменения статуса
 543                    // Статус будет изменен только после успешного создания транзакции
 0544                    foreach (var order in orders)
 545                    {
 546                        // Сохраняем исходный статус из модели, но не применяем изменения
 0547                        var transactedOrder = await _orderServices.TransactOrder(order, submittedFields.For(order.Id));
 548                        // Восстанавливаем исходный статус до сохранения транзакции
 0549                        if (originalStatuses.ContainsKey(order.Id))
 550                        {
 0551                            transactedOrder.AppStatus = originalStatuses[order.Id];
 552                        }
 553
 0554                        ordersToUpdate.Add(transactedOrder);
 0555                    }
 556
 557                    // Все заказы подходят для транзакции (фильтрация по статусу не нужна, так как статус еще не изменен
 0558                    List<Order> ordersToTransaction = ordersToUpdate.ToList();
 0559                    var blockingOrderIds = OneCStockGate.GetBlockingOrderIds(stockCheck);
 0560                    var ordersForOneC = ordersToTransaction.Where(o => !blockingOrderIds.Contains(o.Id)).ToList();
 0561                    var ordersForOneCDraft = ordersToTransaction.Where(o => blockingOrderIds.Contains(o.Id)).ToList();
 562
 0563                    string oneCResult = "";
 0564                    IEnumerable<string> oneCHash = [];
 0565                    List<OneCResponse> transferResult = new();
 0566                    List<OneCResponse> draftTransferResult = new();
 567
 568                    // Обработка в 1С
 0569                    if (processIn1C && ordersForOneC.Any())
 570                    {
 571                        try
 572                        {
 0573                            transferResult = await _oneCTransferManager.TransferStockWithCustomWarehouses(
 0574                                ordersForOneC,
 0575                                warehouseSender,
 0576                                warehouseRecipient,
 0577                                TransactionType.WarehouseTransfer,
 0578                                userName: userName,
 0579                                orgInnOverride: oneCOrganizationInn);
 0580                        }
 0581                        catch (Exception e)
 582                        {
 583                            // Восстанавливаем исходные статусы при ошибке
 0584                            foreach (var order in ordersToUpdate)
 585                            {
 0586                                if (originalStatuses.ContainsKey(order.Id))
 587                                {
 0588                                    order.AppStatus = originalStatuses[order.Id];
 0589                                    await _orderServices.SaveChanges();
 590                                }
 591                            }
 592
 0593                            string msg = $"<div class='alert alert-danger'>" +
 0594                                         $"<strong>⚠️ Ошибка обработки в 1С</strong><br/>" +
 0595                                         $"{e.Message}</div>";
 596
 0597                            TempData["TransactionResult"] = msg;
 0598                            return Json(new
 0599                            {
 0600                                redirectTo = Url.Action("Index", "Orders",
 0601                                    new { sortOrder = GetSortStateCookie(), page })
 0602                            });
 603                        }
 604
 0605                        var successfulTransfers = transferResult.Where(tr => tr.Success).ToList();
 0606                        var failedTransfers = transferResult.Where(tr => !tr.Success).ToList();
 607
 0608                        if (failedTransfers.Count > 0)
 609                        {
 610                            // Восстанавливаем исходные статусы при частичной ошибке
 0611                            foreach (var order in ordersToUpdate)
 612                            {
 0613                                if (originalStatuses.ContainsKey(order.Id))
 614                                {
 0615                                    order.AppStatus = originalStatuses[order.Id];
 0616                                    await _orderServices.SaveChanges();
 617                                }
 618                            }
 619
 0620                            var errorMessages = failedTransfers.Select(tr => tr.Message).Distinct().Take(3);
 0621                            var errorCount = failedTransfers.Count;
 0622                            var successCount = successfulTransfers.Count;
 0623                            var totalCount = transferResult.Count;
 624
 0625                            string msg = $"<div class='alert alert-warning'>" +
 0626                                         $"<strong>⚠️ Частичный результат обработки в 1С</strong><br/>" +
 0627                                         $"Успешно: {successCount} из {totalCount} документов<br/>" +
 0628                                         $"С ошибками: {errorCount} документов<br/>" +
 0629                                         $"<small>Ошибки: {string.Join("; ", errorMessages)}" +
 0630                                         (errorCount > 3 ? "..." : "") + "</small>" +
 0631                                         "</div>";
 632
 0633                            TempData["TransactionResult"] = msg;
 0634                            return Json(new
 0635                            {
 0636                                redirectTo = Url.Action("Index", "Orders",
 0637                                    new { sortOrder = GetSortStateCookie(), page })
 0638                            });
 639                        }
 640                        else
 641                        {
 0642                            oneCHash = successfulTransfers
 0643                                .Select(h => h.TransactionId)
 0644                                .Where(id => id != null)
 0645                                .Select(id => id!);
 0646                            if (ordersForOneCDraft.Any())
 647                            {
 0648                                draftTransferResult = await _oneCTransferManager.TransferStockWithCustomWarehouses(
 0649                                    ordersForOneCDraft, warehouseSender, warehouseRecipient, TransactionType.WarehouseTr
 0650                                    userName: userName, orgInnOverride: oneCOrganizationInn, createAsDraft: true,
 0651                                    draftReason: "недостаточно остатка в 1С. Проведите после оприходования товара.");
 0652                                transferResult.AddRange(draftTransferResult);
 653                            }
 0654                            oneCResult = OneCTransactionSummaryHelper.WrapSection("Проведённые документы 1С",
 0655                                             OneCTransactionSummaryHelper.BuildAlertSummary(ordersForOneC, successfulTra
 0656                                         + OneCTransactionSummaryHelper.WrapSection("Черновики 1С по дефицитным позициям
 0657                                             OneCTransactionSummaryHelper.BuildAlertSummary(ordersForOneCDraft, draftTra
 0658                                         + OneCStockGate.PartialPostingNotice(stockCheck, ordersForOneC.Count, draftTran
 0659                                         + OneCStockGate.SkippedNotice(stockCheck);
 660                        }
 0661                    }
 0662                    else if (processIn1C && ordersForOneCDraft.Any())
 663                    {
 0664                        draftTransferResult = await _oneCTransferManager.TransferStockWithCustomWarehouses(
 0665                            ordersForOneCDraft, warehouseSender, warehouseRecipient, TransactionType.WarehouseTransfer,
 0666                            userName: userName, orgInnOverride: oneCOrganizationInn, createAsDraft: true,
 0667                            draftReason: "недостаточно остатка в 1С. Проведите после оприходования товара.");
 0668                        transferResult.AddRange(draftTransferResult);
 0669                        oneCResult = OneCTransactionSummaryHelper.WrapSection("Черновики 1С по дефицитным позициям",
 0670                                         OneCTransactionSummaryHelper.BuildAlertSummary(ordersForOneCDraft, draftTransfe
 0671                                     + OneCStockGate.PartialPostingNotice(stockCheck, 0, draftTransferResult.Any(r => r.
 672                    }
 673                    else
 674                    {
 0675                        oneCResult = OneCStockGate.NoDocumentNotice(stockCheck);
 676                    }
 677
 0678                    await NotificationService.NotifyAllAsync(oneCResult);
 679
 680                    // Устанавливаем новый статус только после успешной обработки в 1С (если требуется)
 681                    // и перед созданием транзакции в БД
 0682                    foreach (var order in ordersToTransaction)
 683                    {
 0684                        if (order.AppStatus?.Name != "Отменен" && appStatus != null)
 685                        {
 0686                            order.AppStatus = appStatus;
 687                        }
 688                    }
 689
 690                    // Сохраняем изменения статусов
 0691                    await _orderServices.SaveChanges();
 692
 693                    // Создаем транзакцию в БД
 0694                    var historyWindowStart = DateTime.Now;
 0695                    var commentWithOneC = comment;
 0696                    if (transferResult.Count > 0)
 697                    {
 0698                        if (aiFlow.ScheduleAiComment && aiFlow.Builder != null)
 699                        {
 0700                            aiFlow.Builder.WithOneCResults(transferResult, warehouseSender, warehouseRecipient);
 701                        }
 702                        else
 703                        {
 0704                            commentWithOneC += " | " + OneCTransactionSummaryHelper.BuildCommentDetails(
 0705                                ordersForOneC,
 0706                                transferResult.Where(r => !r.IsDraft).ToList(),
 0707                                warehouseSender,
 0708                                warehouseRecipient);
 709                        }
 710                    }
 711
 0712                    var commentForDb = TransactionCommentAiSchedulingHelper.ResolveDatabaseComment(aiFlow, commentWithOn
 713
 714                    try
 715                    {
 0716                        (changeCount, dateTime, transactionId) = await _transaction.CreateWarehouseTransferTransaction(
 0717                            ordersToTransaction,
 0718                            userName,
 0719                            createAt,
 0720                            commentForDb);
 0721                    }
 0722                    catch (Exception ex)
 723                    {
 724                        // Восстанавливаем исходные статусы при ошибке создания транзакции
 0725                        foreach (var order in ordersToTransaction)
 726                        {
 0727                            if (originalStatuses.ContainsKey(order.Id))
 728                            {
 0729                                order.AppStatus = originalStatuses[order.Id];
 730                            }
 731                        }
 732
 0733                        await _orderServices.SaveChanges();
 734
 0735                        string msg = $"<div class='alert alert-danger'>" +
 0736                                     $"<strong>⚠️ Ошибка создания транзакции</strong><br/>" +
 0737                                     $"{ex.Message}</div>";
 738
 0739                        TempData["TransactionResult"] = msg;
 0740                        return Json(new
 0741                        {
 0742                            redirectTo = Url.Action("Index", "Orders",
 0743                                new { sortOrder = GetSortStateCookie(), page })
 0744                        });
 745                    }
 746
 0747                    int cancelCount = ordersToTransaction.Count(o => o.AppStatus?.Name == "Отменен");
 748
 0749                    if (changeCount > 0)
 750                    {
 0751                        if (processIn1C && transferResult.Count > 0)
 752                        {
 0753                            await OrderHistoryOneCPostingRecorder.TryRecordMovementAsync(
 0754                                HttpContext.RequestServices,
 0755                                ordersToTransaction,
 0756                                transferResult,
 0757                                userName,
 0758                                transactionId > 0 ? (int?)transactionId : null,
 0759                                historyWindowStart,
 0760                                warehouseSender,
 0761                                warehouseRecipient);
 762                        }
 763
 0764                        var fromStatusByOrderId = originalStatuses.ToDictionary(x => x.Key, x => x.Value.Id);
 0765                        var statusTransitionHistoryService =
 0766                            HttpContext.RequestServices.GetService<IStatusTransitionHistoryService>();
 0767                        if (postTransitions.Any() && statusTransitionHistoryService != null)
 768                        {
 0769                            var user = await _userCacheService.GetCachedUserAsync(User);
 0770                            await statusTransitionHistoryService.RecordStatusTransitionHistoryAsync(
 0771                                TransactionType.WarehouseTransfer,
 0772                                ordersToTransaction,
 0773                                fromStatusByOrderId,
 0774                                postTransitions,
 0775                                commentForDb,
 0776                                user?.AccessProfileId,
 0777                                createAt,
 0778                                null);
 779                        }
 780
 0781                        await _cacheUpdater.Update(_cache);
 0782                        var statusNameForMessage = string.Join(", ",
 0783                            ordersToTransaction.Select(o => o.AppStatus?.Name).Where(n => !string.IsNullOrEmpty(n))
 0784                                .Distinct());
 0785                        if (string.IsNullOrEmpty(statusNameForMessage) && postTransitions.Any())
 0786                            statusNameForMessage = string.Join(", ",
 0787                                postTransitions.Select(t => t.ToStatus?.Name).Where(n => !string.IsNullOrEmpty(n))
 0788                                    .Distinct());
 0789                        if (string.IsNullOrEmpty(statusNameForMessage))
 0790                            statusNameForMessage = "Перемещение между складами";
 791
 0792                        string msg = TransactionConfig.BuildStandardSuccessMessage(
 0793                            changeCount,
 0794                            cancelCount,
 0795                            dateTime,
 0796                            userName,
 0797                            commentForDb,
 0798                            orders,
 0799                            statusNameForMessage,
 0800                            oneCResult);
 801
 0802                        await NotificationService.NotifyAllAsync(msg);
 0803                        TempData["TransactionResult"] = msg;
 0804                        await CompleteTrackingAsync(trackingKey, transactionId, ordersToTransaction.Count, commentForDb)
 805
 0806                        if (aiFlow.ScheduleAiComment && aiFlow.Builder != null && transactionId > 0)
 807                        {
 0808                            await TransactionCommentAiSchedulingHelper.TryScheduleAsync(
 0809                                HttpContext.RequestServices,
 0810                                aiFlow.Builder,
 0811                                true,
 0812                                (int)transactionId,
 0813                                ordersToTransaction,
 0814                                fromStatusByOrderId,
 0815                                _context,
 0816                                ct: HttpContext.RequestAborted);
 817                        }
 0818                    }
 819
 0820                    await _transactionCache.Update();
 0821                }
 822
 0823                ClearSelectedIdsSession();
 824
 825                // Очищаем cookies для заказов
 0826                foreach (var order in orders)
 827                {
 0828                    var cookieKey = $"PurchasePrice_{order.Id}";
 0829                    if (Request.Cookies.ContainsKey(cookieKey))
 830                    {
 0831                        Response.Cookies.Delete(cookieKey);
 832                    }
 833                }
 834
 0835                return Json(new
 0836                {
 0837                    redirectTo = Url.Action("Index", "Orders", new { sortOrder = GetSortStateCookie(), page })
 0838                });
 839            }
 0840            catch (TransactionException ex)
 841            {
 0842                TempData["TransactionErrorCode"] = ex.Code.ToString();
 0843                TempData["ErorrResult"] = ex.Message;
 0844                return Json(new
 0845                {
 0846                    redirectTo = Url.Action("Index", "Orders", new { sortOrder = GetSortStateCookie(), page })
 0847                });
 848            }
 0849            catch (Exception ex)
 850            {
 0851                TempData["TransactionErrorCode"] = TransactionErrorCode.Unknown.ToString();
 0852                TempData["ErorrResult"] = $"Не удалось провести выбранные заказы ({ex.Message})";
 0853                return Json(new
 0854                {
 0855                    redirectTo = Url.Action("Index", "Orders", new { sortOrder = GetSortStateCookie(), page })
 0856                });
 857            }
 0858        }
 859
 860        [HttpPost]
 861        public IActionResult ClearSelectedIdsSession()
 862        {
 0863            HttpContext.Session.SetString("selectedIds", JsonConvert.SerializeObject(new List<int>()));
 0864            return Ok();
 865        }
 866
 867        private OrderSortState GetSortStateCookie()
 868        {
 0869            var cookie = Request.Cookies["SortState"];
 0870            if (!string.IsNullOrEmpty(cookie) && Enum.TryParse<OrderSortState>(cookie, out var saved))
 0871                return saved;
 0872            return OrderSortState.StandardState;
 873        }
 874
 875        private async Task<List<string>> FilterBitrixWarehouseNamesByScope(IEnumerable<string> warehouseNames)
 876        {
 0877            var names = warehouseNames
 0878                .Where(n => !string.IsNullOrWhiteSpace(n))
 0879                .Distinct(StringComparer.OrdinalIgnoreCase)
 0880                .OrderBy(n => n)
 0881                .ToList();
 882
 0883            var userId = User?.FindFirstValue(ClaimTypes.NameIdentifier);
 0884            if (string.IsNullOrWhiteSpace(userId))
 885            {
 0886                return names;
 887            }
 888
 0889            var allowed = await _scopeFilterValuesService.GetAllowedValuesAsync(
 0890                userId,
 0891                "BitrixWarehouse",
 0892                HttpContext.RequestAborted);
 893
 0894            if (!allowed.TryGetValue("Name", out var allowedNames) || allowedNames.Count == 0)
 895            {
 0896                return names;
 897            }
 898
 0899            return names
 0900                .Where(n => allowedNames.Contains(n))
 0901                .OrderBy(n => n)
 0902                .ToList();
 0903        }
 904
 905        private async Task AttachTrackingKeyAsync(MultiplayEditOrderViewModel model, TransactionType transactionType)
 906        {
 0907            if (model.Orders == null || !model.Orders.Any())
 0908                return;
 0909            var trackingService = HttpContext.RequestServices.GetService<ITransactionTimeTrackingService>();
 0910            if (trackingService == null)
 0911                return;
 0912            model.TrackingKey = await trackingService.StartSessionAsync(
 0913                User.Identity?.Name ?? "unknown",
 0914                model.User?.AccessProfileId,
 0915                transactionType,
 0916                model.Orders.Select(o => o.Id),
 0917                $"{Request.Path}{Request.QueryString}",
 0918                HttpContext.Connection.RemoteIpAddress?.ToString(),
 0919                Request.Headers.UserAgent.ToString());
 0920        }
 921
 922        private async Task CompleteTrackingAsync(string? trackingKey, long transactionId, int ordersCount, string? comme
 923        {
 0924            if (string.IsNullOrWhiteSpace(trackingKey) || transactionId <= 0)
 0925                return;
 0926            var trackingService = HttpContext.RequestServices.GetService<ITransactionTimeTrackingService>();
 0927            if (trackingService == null)
 0928                return;
 0929            await trackingService.CompleteSessionAsync(
 0930                trackingKey,
 0931                transactionId,
 0932                ordersCount,
 0933                comment,
 0934                $"/Studio2/Transaction/GetTransaction?transactionId={transactionId}");
 0935        }
 936    }
 937}