< Summary

Line coverage
0%
Covered lines: 0
Uncovered lines: 3143
Coverable lines: 3143
Total lines: 6835
Line coverage: 0%
Branch coverage
0%
Covered branches: 0
Total branches: 1495
Branch coverage: 0%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
File 1: MultiplayUpdateOrdersStatusV2()0%7280%
File 1: UpdateOrdersStatusInBackground()0%2040%
File 2: Details()0%620%
File 2: MultiplayEdit()0%7280%
File 2: MultiplayEdit()0%156120%
File 2: MultiplayEditV2()0%1640400%
File 2: MultiplayEditV2()0%1056320%
File 2: Create()0%2040%
File 2: Create()0%167701290%
File 2: ReturnCreateViewWithReloadedListsAsync()100%210%
File 3: ResolveOrdersPageSize(...)0%7280%
File 3: SanitizeFileName(...)0%4260%
File 3: .ctor(...)100%210%
File 3: BuildOrderListFieldReadMapAsync()100%210%
File 3: BuildOrderListFieldReadMapFromBundle(...)100%210%
File 3: Returns()0%1190340%
File 3: Returns()0%2040%
File 3: Index()0%2970540%
File 3: Index()0%2162460%
File 3: ViewOrders()0%506220%
File 3: DelSortStateCookie()100%210%
File 3: DelSortStateCookieForV2()100%210%
File 3: SetPageSize()100%210%
File 3: SetPageSizeForV2()100%210%
File 3: Update()100%210%
File 3: UpdateV2()100%210%
File 3: Delete()0%2040%
File 3: DeleteV2()0%2040%
File 3: GetFriendlySplitErrorMessage(...)0%110100%
File 3: PerformSplitOrderAsync()0%1640400%
File 3: SplitOrder()0%4260%
File 3: MultiplayDeleteOrders()0%4260%
File 3: DeleteOrderPage()0%4260%
File 3: MultiplayDeleteOrdersV2()0%4260%
File 3: CancellationOrders()0%4260%
File 3: Recover()100%210%
File 3: ConfirmAccepted()0%7280%
File 3: ConfirmAcceptedV2()0%7280%
File 3: SaveSelectedStockListToSession()100%210%
File 3: SplitOrders()0%7280%
File 3: DeleteDuplicateOrders()100%210%
File 3: OrderExists()100%210%
File 3: SetSortOrderViewData(...)0%620%
File 3: ApplySortOrder(...)100%210%
File 3: LoadOrdersIndexPageAsync()0%620%
File 3: ShapeOrdersAfterLoad(...)0%620%
File 3: ApplyOrdersIndexPageViewDataAsync()0%620%
File 3: ApplyFilterListsToViewAsync()0%2040%
File 3: SetFilterLists()0%620%
File 3: ApplyOrderDataScopeAsync()0%620%
File 3: FilterOrderIdsByDataScopeAsync()0%620%
File 3: FilterByOrderScopeIdsAsync()0%620%
File 3: EnsureOrderColumnsVisibilityProfile(...)0%210140%
File 3: NormalizeSyntheticAccessProfile(...)0%4260%
File 3: SaveSortStateCookie(...)100%210%
File 3: SaveSortStateCookie(...)0%620%
File 3: GetSortStateCookie()100%210%
File 3: GetSortStateCookie(...)0%2040%
File 3: SetInfoMessage()0%2550500%
File 3: RetrieveSelectedIdsFromSession()0%2040%
File 3: ClearSelectedIdsSession()100%210%
File 3: UpdateLabelCache(...)100%210%
File 3: GetOrdersByProductKey()0%620%
File 4: .cctor()100%210%
File 4: ResolveExcelColumnsAsync()0%110100%
File 4: PrintOrdersExcel()0%110100%
File 4: PrintOrdersExcelV2()0%156120%
File 4: PrintOrdersExcelForClient()0%2756520%
File 5: UploadHandoverJson()0%110100%
File 6: IndexTableAjax()0%210140%
File 6: IndexDateCalendarAjax()0%1056320%
File 6: <IndexDateCalendarAjax()0%2040%
File 6: get_Date()100%210%
File 6: get_Count()100%210%
File 6: get_ViewModel()100%210%
File 6: get_LoginRedirect()100%210%
File 6: BuildOrdersIndexTableAjaxViewModelAsync()0%272160%
File 7: PrintOrdersLable()0%702260%
File 7: <PrintOrdersLable()0%812280%
File 7: GetOzonLabelEligibleOrders()0%2040%
File 7: GetOrderProducts()0%420200%
File 7: ResolvePostingProductImage(...)0%342180%
File 7: GetOzonCountryRequirements()0%110100%
File 7: GetOzonExemplarRequirements()0%110100%
File 7: SetOzonExemplars()0%420200%
File 7: GetOzonProductCountries()0%4260%
File 7: SetOzonProductCountries()0%342180%
File 7: ParseOrderIds(...)0%2040%
File 7: CheckLabelsInDropbox()0%600240%
File 7: LoadOzonStatusesAsync()0%272160%
File 7: UpdateOrdersStatus()0%2550500%
File 7: GenerateLabels()0%702260%
File 7: GenerateYandexLabels()0%1806420%
File 7: <GenerateYandexLabels()0%420200%
File 7: CreateStudioLabelLog(...)0%2040%
File 7: DownloadLabels()0%506220%
File 7: ClearLabelsSession(...)100%210%
File 7: SendFilesToUser(...)0%7280%
File 8: Upload()0%210140%
File 8: UploadFile()0%210140%
File 8: Upload(...)100%210%
File 8: UploadInBackground()100%210%
File 8: UploadClientsData()0%7280%
File 8: UploadOrders()0%342180%
File 8: UploadExcelFile()0%342180%
File 8: PrepaExcelTable()0%1190340%
File 8: NormalizeHeaderForComparison(...)0%620%
File 8: GroupFileData()0%7280%
File 8: HasActiveOrderFilters(...)0%7280%
File 8: HasAnyString()100%210%
File 8: HasAnyNullableNumber()100%210%
File 8: HasAnyNullableInt()100%210%
File 8: HasAnyNullableDouble()100%210%
File 8: AppendFile()0%272160%
File 8: CheckFile()0%3422580%
File 8: NewValuesProcessing(...)0%506220%
File 8: UploadExcelOrders()0%110100%
File 8: SetNotFullOrdersData()0%930300%
File 8: GetDefaultAppStatus()0%2040%
File 8: SetNotFullOrdersData()0%420200%
File 8: ProcessNotFullOrdersInBackground()0%620%
File 8: ViewFile()100%210%
File 8: SplitOrderFromSetNotFull()0%4260%

File(s)

/home/runner/work/Studio2Prod/Studio2Prod/OzonOrdersWeb/Areas/Studio2/Controllers/OrdersController.ClientStatus.cs

#LineLine coverage
 1using Hangfire;
 2using Hangfire.States;
 3using Microsoft.AspNetCore.Mvc;
 4using Services.HangfireService;
 5using Services.SignalRServices;
 6
 7namespace OzonOrdersWeb.Controllers;
 8
 9public partial class OrdersController
 10{
 11    public async Task<IActionResult> MultiplayUpdateOrdersStatusV2(string ids, int page)
 12    {
 013        if (string.IsNullOrEmpty(ids))
 14        {
 015            TempData["ErorrResult"] = "Не переданы ID заказов для обновления";
 016            return RedirectToAction(nameof(Index), new { page });
 17        }
 18
 019        int[] idArray = ids.Split(',').Select(int.Parse).ToArray();
 20
 021        if (idArray.Length > 10)
 22        {
 023            BackgroundJobClient client = new BackgroundJobClient();
 024            client.Create(
 025                () => UpdateOrdersStatusInBackground(ids, page),
 026                new EnqueuedState(HangfireQueueNames.Main));
 27
 028            TempData["TransactionResult"] = "Обновление статусов обрабатывается в фоне (много заказов).";
 029            ClearSelectedIdsSession();
 030            return RedirectToAction(nameof(Index), new { page });
 31        }
 32
 033        var result = await _clientStatusSyncService.SyncOrderStatusesAsync(idArray);
 34
 035        if (result.Error != null)
 36        {
 037            TempData["ErorrResult"] = result.Error;
 38        }
 039        else if (result.Message != null)
 40        {
 041            TempData["TransactionResult"] = result.Message;
 042            await NotificationService.NotifyAllAsync(result.Message);
 43        }
 44
 045        ClearSelectedIdsSession();
 046        return RedirectToAction(nameof(Index), new { page });
 047    }
 48
 49    public async Task<string> UpdateOrdersStatusInBackground(string ids, int page)
 50    {
 051        int[] idArray = ids.Split(',').Select(int.Parse).ToArray();
 052        var result = await _clientStatusSyncService.SyncOrderStatusesAsync(idArray);
 053        string notificationMessage = result.Error ?? result.Message ?? "Нет изменений в статусах заказов";
 054        await NotificationService.NotifyAllAsync(notificationMessage);
 055        return notificationMessage;
 056    }
 57}

/home/runner/work/Studio2Prod/Studio2Prod/OzonOrdersWeb/Areas/Studio2/Controllers/OrdersController.CreateEdit.cs

#LineLine coverage
 1using Microsoft.AspNetCore.Mvc;
 2using Microsoft.AspNetCore.Mvc.Rendering;
 3using Microsoft.EntityFrameworkCore;
 4using Microsoft.Extensions.DependencyInjection;
 5using OzonDomains;
 6using OzonDomains.Models;
 7using OzonDomains.Models.AccessControl;
 8using OzonOrdersWeb.ViewModels.OrderViewModels;
 9using OzonRepositories.Context.Identity;
 10using Services.DataServices;
 11using Services.SignalRServices;
 12
 13namespace OzonOrdersWeb.Controllers;
 14
 15public partial class OrdersController
 16{
 17        public async Task<IActionResult> Details(int id)
 18
 19        {
 020            var order = await _orderServices.GetOrder(id);
 21
 022            if (order == null)
 23
 24            {
 025                return NotFound();
 26            }
 27
 28
 029            return View(new DetailsOrderViewModel()
 030
 031            {
 032                Order = order,
 033            });
 034        }
 35
 36
 37        public async Task<IActionResult> MultiplayEdit(string ids, int page)
 38
 39        {
 040            ViewBag.AppStatuses =
 041                new SelectList(
 42                    (await _appStatusServices.GetAppStatuses()).Where(s => s.Name != "Заказан поставщику")
 43                    .OrderBy(a => a.Name), "Id", "Name");
 44
 45
 046            if (string.IsNullOrWhiteSpace(ids))
 47            {
 048                TempData["ErrorMessage"] = "Не выбраны заказы для редактирования.";
 049                return RedirectToAction("Index");
 50            }
 51
 052            var idArray = ids
 053                .Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
 54                .Select(raw => int.TryParse(raw, out var parsed) ? (int?)parsed : null)
 55                .Where(parsed => parsed.HasValue)
 56                .Select(parsed => parsed!.Value)
 057                .Distinct()
 058                .ToArray();
 59
 060            if (idArray.Length == 0)
 61            {
 062                TempData["ErrorMessage"] = "Не удалось распознать идентификаторы заказов.";
 063                return RedirectToAction("Index");
 64            }
 65
 66
 067            idArray = await FilterOrderIdsByDataScopeAsync(idArray);
 068            var ordersToEdit = await _orderServices.GetOrdersNoTracking(idArray);
 69
 070            var user = await _userCacheService.GetCachedUserAsync(User);
 071            if (user == null)
 72            {
 073                return Redirect("/Identity/Account/Login");
 74            }
 75
 076            var pageViewModel = new MultiplayEditOrderViewModel()
 077
 078            {
 079                Orders = ordersToEdit,
 080
 081                RedirectPage = page,
 082
 083                User = user,
 084
 85                Suppliers = (await _supplierDataServices.GetSuppliers()).OrderBy(s => s.Name).ToList(),
 086
 087                RateEUR = await _currencyRateFetcher.GetEURRateAsync(),
 088
 089                RateUSD = await _currencyRateFetcher.GetUSDRateAsync(),
 090
 091                RateBYN = await _currencyRateFetcher.GetBYNRateAsync(),
 092            };
 93
 94
 095            if (pageViewModel.User.AccessProfileId != null)
 96
 97            {
 098                pageViewModel.User.AccessProfile = await _context.AccessProfiles.FindAsync(pageViewModel.User.AccessProf
 99            }
 0100            EnsureOrderColumnsVisibilityProfile(pageViewModel.User);
 101
 102
 0103            return View(pageViewModel);
 0104        }
 105
 106
 107        [HttpPost]
 108        public async Task<IActionResult> MultiplayEdit(List<Order> orders, int redirectPage)
 109
 110        {
 0111            if (orders == null)
 112
 113            {
 0114                TempData["ErorrResult"] =
 0115                    $"Не удалось изменить выбранные заказы.<br>Было передано слишком большое количество записей.";
 116
 0117                return RedirectToAction("Index");
 118            }
 119
 120            var allowedIds = (await FilterOrderIdsByDataScopeAsync(orders.Select(o => o.Id))).ToHashSet();
 0121            orders = orders.Where(o => allowedIds.Contains(o.Id)).ToList();
 122
 0123            var user = await _userCacheService.GetCachedUserAsync(User);
 0124            if (user == null)
 125            {
 0126                return Redirect("/Identity/Account/Login");
 127            }
 128
 0129            ViewBag.AppStatuses =
 0130                new SelectList(
 131                    (await _appStatusServices.GetAppStatuses()).Where(s => s.Name != "Заказан поставщику")
 132                    .OrderBy(a => a.Name), "Id", "Name");
 133
 134
 0135            var pageViewModel = new MultiplayEditOrderViewModel()
 0136
 0137            {
 0138                Orders = orders,
 0139
 0140                RedirectPage = redirectPage,
 0141
 0142                User = user,
 0143
 144                Suppliers = (await _supplierDataServices.GetSuppliers()).OrderBy(s => s.Name).ToList(),
 0145
 0146                RateEUR = await _currencyRateFetcher.GetEURRateAsync(),
 0147
 0148                RateUSD = await _currencyRateFetcher.GetUSDRateAsync(),
 0149
 0150                RateBYN = await _currencyRateFetcher.GetBYNRateAsync(),
 0151            };
 152
 153
 0154            if (pageViewModel.User.AccessProfileId != null)
 155
 156            {
 0157                pageViewModel.User.AccessProfile = await _context.AccessProfiles.FindAsync(pageViewModel.User.AccessProf
 158            }
 159
 160
 161            try
 162
 163            {
 0164                int editOrdersCount = await _orderServices.MultiplayEditOrder(orders, User.Identity?.Name ?? string.Empt
 165
 0166                await _cache.Update();
 167
 168
 0169                ClearSelectedIdsSession();
 170
 0171                return RedirectToAction(nameof(System.Index),
 0172                    new { sortOrder = GetSortStateCookie(), page = redirectPage });
 173            }
 174
 0175            catch (Exception ex)
 176            {
 0177                pageViewModel.ErrorMessage = ex.Message;
 178
 0179                return View(pageViewModel);
 180            }
 0181        }
 182
 183
 184        public async Task<IActionResult> MultiplayEditV2(string ids, int page)
 185
 186        {
 0187            var user = await _userCacheService.GetCachedUserAsync(User);
 188
 0189            AccessProfile? accessProfile = null;
 190
 191
 0192            if (user?.AccessProfileId.HasValue == true)
 193
 194            {
 0195                accessProfile = await _accessProfileDataServices.GetAccessProfileAsync(user.AccessProfileId.Value);
 196            }
 197
 198            // Load expense types for extra expenses functionality
 0199            var expenseTypes = await _context.ExpenseTypes.Where(et => et.IsActive).OrderBy(e => e.Name).ToListAsync();
 0200            ViewBag.ExpenseTypes = expenseTypes;
 201
 202
 203            // Фильтрация статусов по правам доступа
 204
 205            List<AppStatus> appStatuses;
 206
 0207            if (accessProfile != null)
 208
 209            {
 0210                var statusIds = accessProfile.ScopeItems
 211                    .Where(x => x.ScopeType == DataScopeType.AppStatus)
 212                    .Select(x => x.ReferenceId)
 0213                    .ToList();
 214
 215
 0216                var allAppStatuses = await _appStatusServices.GetAppStatuses();
 217
 0218                appStatuses = statusIds.Any()
 219                    ? allAppStatuses.Where(s => statusIds.Contains(s.Id)).OrderBy(a => a.Name).ToList()
 220                    : allAppStatuses.OrderBy(a => a.Name).ToList();
 0221            }
 222
 223            else
 224
 225            {
 226                appStatuses = (await _appStatusServices.GetAppStatuses()).OrderBy(a => a.Name).ToList();
 227            }
 228
 229
 230            var unassignedStatus = appStatuses.FirstOrDefault(s => s.Name == "Не указан");
 231
 0232            if (unassignedStatus != null)
 233
 234            {
 0235                appStatuses.Remove(unassignedStatus);
 236
 0237                appStatuses.Insert(0, unassignedStatus);
 238            }
 239
 240
 0241            ViewBag.AppStatuses = new SelectList(appStatuses, "Id", "Name");
 242
 243            ViewBag.StatusColors = appStatuses.ToDictionary(s => s.Id.ToString(), s => s.GetStatusColor());
 244
 245
 0246            int[] idArray = ids.Split(',').Select(int.Parse).ToArray();
 247
 248
 0249            idArray = await FilterOrderIdsByDataScopeAsync(idArray);
 0250            var ordersToEdit = await _orderServices.GetOrdersNoTracking(idArray);
 251
 252            // Load existing expenses for orders
 0253            var orderExtraExpenseDataServices = HttpContext.RequestServices.GetService<OrderExtraExpenseDataServices>();
 0254            if (orderExtraExpenseDataServices != null && ordersToEdit.Any())
 255            {
 256                var orderIdsForExpenses = ordersToEdit.Select(o => o.Id).ToList();
 0257                var expensesDict = await orderExtraExpenseDataServices.GetByOrderIds(orderIdsForExpenses);
 0258                ViewBag.ExistingExpenses = expensesDict;
 259            }
 260            else
 261            {
 0262                ViewBag.ExistingExpenses = new Dictionary<int, List<OrderExtraExpense>>();
 263            }
 264
 265
 266            // Фильтрация поставщиков по правам доступа
 267
 268            List<Supplier> suppliers;
 269
 0270            if (accessProfile != null)
 271
 272            {
 0273                var supplierIds = accessProfile.ScopeItems
 274                    .Where(x => x.ScopeType == DataScopeType.Supplier)
 275                    .Select(x => x.ReferenceId)
 0276                    .ToList();
 277
 278
 0279                var allSuppliers = await _supplierDataServices.GetSuppliers();
 280
 0281                suppliers = supplierIds.Any()
 282                    ? allSuppliers.Where(s => supplierIds.Contains(s.Id)).OrderBy(s => s.Name).ToList()
 283                    : allSuppliers.OrderBy(s => s.Name).ToList();
 0284            }
 285
 286            else
 287
 288            {
 289                suppliers = (await _supplierDataServices.GetSuppliers()).OrderBy(s => s.Name).ToList();
 290            }
 291
 292            appStatuses = await FilterByOrderScopeIdsAsync(appStatuses, s => s.Id, "AppStatusId");
 293            suppliers = await FilterByOrderScopeIdsAsync(suppliers, s => s.Id, "SupplierId");
 0294            ViewBag.AppStatuses = new SelectList(appStatuses, "Id", "Name");
 295            ViewBag.StatusColors = appStatuses.ToDictionary(s => s.Id.ToString(), s => s.GetStatusColor());
 296            ViewBag.AppStatusIsFinal = appStatuses.ToDictionary(s => s.Id.ToString(), s => s.IsFinal);
 297
 298
 299            // Загружаем склады для засылов
 0300            var warehouses = await _warehouseDataServices.GetWarehouses();
 301            warehouses = await FilterByOrderScopeIdsAsync(warehouses, w => w.Id, "ShipmentWarehouseId");
 302
 303            // Загружаем существующие значения засылов
 0304            var allZasyls = await _orderServices.GetAllZasylValuesAsync();
 0305            var allowedWarehouseNames = warehouses
 306                .Select(w => w.Name)
 307                .Where(n => !string.IsNullOrWhiteSpace(n))
 0308                .ToHashSet(StringComparer.OrdinalIgnoreCase);
 0309            var warehouseNames = warehouses
 310                .Select(w => w.Name)
 0311                .Union(allZasyls.Where(z => allowedWarehouseNames.Contains(z)))
 0312                .Distinct()
 313                .OrderBy(n => n)
 0314                .ToList();
 315
 0316            if (user == null)
 317            {
 0318                return Redirect("/Identity/Account/Login");
 319            }
 320
 0321            var pageViewModel = new MultiplayEditOrderViewModel()
 0322
 0323            {
 0324                Orders = ordersToEdit,
 0325
 0326                RedirectPage = page,
 0327
 0328                User = user,
 0329
 0330                Suppliers = suppliers,
 0331
 0332                RateEUR = await _currencyRateFetcher.GetEURRateAsync(),
 0333
 0334                RateUSD = await _currencyRateFetcher.GetUSDRateAsync(),
 0335
 0336                RateBYN = await _currencyRateFetcher.GetBYNRateAsync(),
 0337
 0338                UniqueArticles = await _orderServices.GetArticleCountsForOrdersAsync(ordersToEdit, false),
 0339
 0340                UniqueDeliveryCitys = await _orderServices.GetDeliveryCityCountsForOrdersAsync(ordersToEdit, false),
 0341
 0342                UniqueNumbers = await _orderServices.GetShipmentBaseCountsForOrdersAsync(ordersToEdit, false),
 0343
 344                Warehouses = warehouses.OrderBy(w => w.Name).ToList(),
 0345
 346                ZasylValues = warehouseNames.Where(n => n != null).Select(n => n!).ToList(),
 0347            };
 348
 349
 0350            if (user.AccessProfileId != null)
 351            {
 0352                pageViewModel.User.AccessProfile = accessProfile ?? await _context.AccessProfiles
 0353                    .Include(u => u.DataScopeSets)
 0354                    .ThenInclude(s => s.Items)
 0355                    .AsSplitQuery()
 0356                    .FirstOrDefaultAsync(u => u.Id == user.AccessProfileId.Value);
 357            }
 0358            EnsureOrderColumnsVisibilityProfile(pageViewModel.User);
 359
 360
 0361            return View(pageViewModel);
 0362        }
 363
 364
 365        [HttpPost]
 366        public async Task<IActionResult> MultiplayEditV2(List<Order> orders, int redirectPage,
 367            Dictionary<int, string?>? orderSupplierMap)
 368
 369        {
 0370            if (orders == null)
 371
 372            {
 0373                TempData["ErorrResult"] =
 0374                    $"Не удалось изменить выбранные заказы.<br>Было передано слишком большое количество записей.";
 375
 0376                return RedirectToAction("Index");
 377            }
 378
 379            var allowedIds = (await FilterOrderIdsByDataScopeAsync(orders.Select(o => o.Id))).ToHashSet();
 0380            orders = orders.Where(o => allowedIds.Contains(o.Id)).ToList();
 381
 382
 383            var orderIds = orders.Select(o => o.Id).Distinct().ToList();
 384
 0385            var fromStatusByOrderId = await _context.Orders.AsNoTracking()
 0386                .Where(o => orderIds.Contains(o.Id))
 0387                .Select(o => new { o.Id, FromStatusId = o.AppStatusId ?? 0 })
 388                .ToDictionaryAsync(x => x.Id, x => x.FromStatusId);
 389
 390
 391            try
 392
 393            {
 394                // Очищаем пустые дополнительные расходы перед сохранением
 395                // Это гарантирует, что пустые элементы не попадут в базу данных
 0396                foreach (var order in orders)
 397                {
 0398                    if (order.ExtraExpenses != null && order.ExtraExpenses.Any())
 399                    {
 400                        // Удаляем все расходы, где не заполнены оба поля (тип и сумма)
 0401                        order.ExtraExpenses = order.ExtraExpenses
 402                            .Where(e => e != null && e.ExpenseTypeId > 0 && e.Amount > 0)
 0403                            .ToList();
 404
 405                        // Если список стал пустым, устанавливаем null
 0406                        if (!order.ExtraExpenses.Any())
 407                        {
 0408                            order.ExtraExpenses = [];
 409                        }
 410                    }
 411                }
 412
 413                // Save the orders (without extra expenses)
 0414                await _orderServices.MultiplayEditOrder(orders, User.Identity?.Name ?? string.Empty);
 415
 416                // Save extra expenses and update cost price
 0417                await _orderServices.SaveExtraExpensesForOrders(orders);
 418
 419                // Сохраняем засылы если есть
 0420                if (orderSupplierMap != null && orderSupplierMap.Any())
 421                {
 0422                    await _orderServices.SetZasylAsync(orderSupplierMap);
 423                }
 424
 0425                var calculationSnapshots = HttpContext.RequestServices.GetService<OrderCalculationSnapshotService>();
 0426                if (calculationSnapshots != null)
 427                {
 0428                    await calculationSnapshots.CaptureAsync(
 429                        orders.Select(x => x.Id), Guid.NewGuid(), "ManualEdit",
 0430                        User.Identity?.Name, HttpContext.RequestAborted);
 431                }
 432
 433
 0434                var ordersWithStatusChanged = orders
 0435                    .Where(o => fromStatusByOrderId.TryGetValue(o.Id, out var fromId) &&
 0436                                (o.AppStatusId ?? o.AppStatus?.Id ?? 0) != fromId &&
 0437                                (o.AppStatusId ?? o.AppStatus?.Id ?? 0) != 0)
 0438                    .ToList();
 439
 0440                if (ordersWithStatusChanged.Any())
 441
 442                {
 0443                    var statusHistoryService =
 0444                        HttpContext.RequestServices.GetService<IStatusTransitionHistoryService>();
 445
 0446                    if (statusHistoryService != null)
 447
 448                    {
 0449                        var user = await _userCacheService.GetCachedUserAsync(User);
 450
 0451                        await statusHistoryService.RecordManualStatusChangeAsync(
 0452                            ordersWithStatusChanged,
 0453                            fromStatusByOrderId,
 0454                            user?.AccessProfileId,
 0455                            DateTime.Now);
 456                    }
 0457                }
 458
 459
 0460                await _cache.Update();
 461
 0462                ClearSelectedIdsSession();
 463
 0464                return RedirectToAction(nameof(Index), new { sortOrder = GetSortStateCookie(), page = redirectPage });
 465            }
 466
 0467            catch (Exception ex)
 468            {
 469                // Сохраняем засылы даже при ошибке
 0470                if (orderSupplierMap != null && orderSupplierMap.Any())
 471                {
 0472                    await _orderServices.SetZasylAsync(orderSupplierMap);
 473                }
 474
 0475                TempData["ErorrResult"] = $"Не удалось изменить выбранные заказы.<br>{ex.Message}";
 0476                await NotificationService.NotifyAllAsync($"Ошибка при изменении заказов: {ex.Message}");
 477
 0478                return RedirectToAction(nameof(Index), new { sortOrder = GetSortStateCookie(), page = redirectPage });
 479            }
 0480        }
 481        [HttpGet]
 482        public async Task<IActionResult> Create()
 483        {
 0484            var user = await _userCacheService.GetCachedUserAsync(User);
 0485            if (user == null)
 486            {
 0487                return Redirect("/Identity/Account/Login");
 488            }
 489
 0490            AccessProfile? accessProfile = null;
 0491            if (user.AccessProfileId != null)
 492            {
 0493                accessProfile = await _context.AccessProfiles
 0494                    .Include(u => u.DataScopeSets)
 0495                    .ThenInclude(s => s.Items)
 0496                    .AsSplitQuery()
 0497                    .FirstOrDefaultAsync(u => u.Id == user.AccessProfileId.Value);
 498            }
 499
 0500            var viewModel = await _orderCreateFormService.BuildForCreatePageAsync(user, accessProfile);
 0501            return View(viewModel);
 0502        }
 503
 504
 505        [HttpPost]
 506        [ValidateAntiForgeryToken]
 507        public async Task<IActionResult> Create(CreateOrderViewModel viewModel)
 508
 509        {
 0510            var currentUser = await _userCacheService.GetCachedUserAsync(User);
 0511            if (currentUser == null)
 512            {
 0513                return Redirect("/Identity/Account/Login");
 514            }
 515
 516            // Удаляем из ModelState служебные поля, которые не должны валидироваться
 517
 0518            ModelState.Remove("CartItemStatus");
 519
 0520            ModelState.Remove("Order.CartItemStatus");
 521
 0522            ModelState.Remove("User");
 523
 0524            ModelState.Remove("SuppliersSelectList");
 525
 0526            ModelState.Remove("CurrenciesSelectList");
 527
 0528            ModelState.Remove("DeliveriesSelectList");
 529
 0530            ModelState.Remove("WarehousesSelectList");
 531
 0532            ModelState.Remove("AppStatusesSelectList");
 533
 0534            ModelState.Remove("EtProducersSelectList");
 535
 0536            ModelState.Remove("OzonClientsSelectList");
 537
 0538            ModelState.Remove("Suppliers");
 539
 0540            ModelState.Remove("Currencies");
 541
 0542            ModelState.Remove("Deliveries");
 543
 0544            ModelState.Remove("Warehouses");
 545
 0546            ModelState.Remove("AppStatuses");
 547
 0548            ModelState.Remove("EtProducers");
 549
 0550            ModelState.Remove("OzonClients");
 551
 0552            ModelState.Remove("RateUSD");
 553
 0554            ModelState.Remove("RateEUR");
 555
 0556            ModelState.Remove("RateBYN");
 557
 558
 0559            if (!ModelState.IsValid)
 560
 561            {
 562                // Собираем все ошибки валидации (исключая служебные поля ViewModel)
 563
 0564                var excludedFields = new HashSet<string>
 0565
 0566                {
 0567                    "User", "SuppliersSelectList", "CurrenciesSelectList", "DeliveriesSelectList",
 0568
 0569                    "WarehousesSelectList", "AppStatusesSelectList", "EtProducersSelectList", "CartItemStatus",
 0570
 0571                    "OzonClientsSelectList", "CartItemStatus", "Suppliers", "Currencies", "Deliveries",
 0572
 0573                    "Warehouses", "AppStatuses", "EtProducers", "OzonClients", "RateUSD", "RateEUR", "RateBYN"
 0574                };
 575
 576
 0577                var errors = new List<string>();
 578
 0579                foreach (var error in ModelState)
 580
 581                {
 582                    // Пропускаем служебные поля ViewModel
 583
 0584                    if (excludedFields.Contains(error.Key))
 585
 586                    {
 587                        continue;
 588                    }
 589
 590
 591                    // Пропускаем поля, которые начинаются с названий коллекций (например, "Order.ProductInfo.Volumetric
 592
 0593                    var fieldKey = error.Key;
 594
 0595                    if (fieldKey.Contains("Suppliers") || fieldKey.Contains("Currencies") ||
 0596                        fieldKey.Contains("Deliveries") || fieldKey.Contains("Warehouses") ||
 0597                        fieldKey.Contains("AppStatuses") || fieldKey.Contains("EtProducers") ||
 0598                        fieldKey.Contains("OzonClients") || fieldKey.Contains("SelectList"))
 599
 600                    {
 601                        continue;
 602                    }
 603
 604
 0605                    if (error.Value.Errors.Count > 0)
 606
 607                    {
 0608                        foreach (var errorMessage in error.Value.Errors)
 609
 610                        {
 0611                            var fieldName = error.Key;
 612
 613                            // Упрощаем название поля для пользователя
 614
 0615                            if (fieldName.Contains("."))
 616
 617                            {
 0618                                var parts = fieldName.Split('.');
 619
 0620                                fieldName = parts[parts.Length - 1];
 621                            }
 622
 623
 624                            // Переводим технические названия полей на русский
 625
 0626                            var fieldDisplayName = fieldName switch
 0627
 0628                            {
 0629                                "ShipmentNumber" => "Номер отправления",
 0630
 0631                                "OzonClientId" => "Клиент",
 0632
 0633                                "ProcessingDate" => "Дата принятия",
 0634
 0635                                "ShippingDate" => "Дата отгрузки",
 0636
 0637                                "EtProducerId" => "Производитель",
 0638
 0639                                "EtProducerName" => "Производитель",
 0640
 0641                                "Article" => "Код товара",
 0642
 0643                                "Price" => "Цена",
 0644
 0645                                "Quantity" => "Количество",
 0646
 0647                                "ShipmentWarehouseId" => "Склад отгрузки",
 0648
 0649                                _ => fieldName
 0650                            };
 651
 652
 0653                            var message = errorMessage.ErrorMessage;
 654
 0655                            if (string.IsNullOrEmpty(message))
 656
 657                            {
 0658                                message = "Поле обязательно для заполнения";
 659                            }
 660
 661
 0662                            errors.Add($"<li><strong>{fieldDisplayName}:</strong> {message}</li>");
 663                        }
 664                    }
 665                }
 666
 667
 668                // Формируем сообщение об ошибке в HTML формате
 669
 0670                if (errors.Count > 0)
 671
 672                {
 0673                    viewModel.ErrorMessage =
 0674                        "<strong>Обнаружены ошибки валидации формы:</strong><ul class=\"mb-0 mt-2\">" +
 0675                        string.Join("", errors) + "</ul>";
 676                }
 677
 678
 0679                viewModel.User = currentUser;
 0680                return await ReturnCreateViewWithReloadedListsAsync(viewModel);
 681            }
 682
 683
 684            try
 685
 686            {
 687                // Устанавливаем статус по умолчанию, если не указан
 688
 0689                if (!viewModel.Order.AppStatusId.HasValue)
 690
 691                {
 0692                    var defaultStatus = await _context.AppStatuses
 0693                        .FirstOrDefaultAsync(s => s.Name == "Не указан");
 694
 0695                    if (defaultStatus != null)
 696
 697                    {
 0698                        viewModel.Order.AppStatusId = defaultStatus.Id;
 699                    }
 700                }
 701
 702
 703                // Обработка производителя: если EtProducerId не указан, пытаемся найти по имени
 704
 0705                if (!viewModel.Order.EtProducerId.HasValue)
 706
 707                {
 0708                    var producerName = Request.Form["Order.EtProducerName"].ToString().Trim();
 709
 0710                    if (!string.IsNullOrEmpty(producerName))
 711
 712                    {
 0713                        var allEtProducers = await _etProducerDataServices.GetEtProducers();
 714
 0715                        var foundProducer = allEtProducers
 0716                            .FirstOrDefault(e => e.Id == e.RealId &&
 0717                                                 !string.IsNullOrEmpty(e.MarketPrefix) &&
 0718                                                 e.Name.Equals(producerName, StringComparison.OrdinalIgnoreCase));
 719
 720
 0721                        if (foundProducer != null)
 722
 723                        {
 0724                            viewModel.Order.EtProducerId = foundProducer.Id;
 725                        }
 726                    }
 0727                }
 728
 729
 730                // Автоматическое формирование ProductKey: артикул=код_производителя
 731
 0732                if (!string.IsNullOrEmpty(viewModel.Order.Article) && viewModel.Order.EtProducerId.HasValue)
 733
 734                {
 0735                    var producer = await _etProducerDataServices.GetEtProducerAsync(viewModel.Order.EtProducerId.Value);
 736
 0737                    if (producer != null && !string.IsNullOrEmpty(producer.MarketPrefix))
 738
 739                    {
 0740                        viewModel.Order.ProductKey = $"{viewModel.Order.Article}={producer.MarketPrefix}";
 741                    }
 742                }
 743
 744
 745                // Заполнение ProductInfo из справочника продуктов по ProductKey и Article
 746
 0747                if (viewModel.Order.ProductInfo == null)
 748
 749                {
 0750                    viewModel.Order.ProductInfo = new Product();
 751                }
 752
 753
 754                // Ищем продукт в справочнике по Article (извлекаем из ProductKey или используем напрямую Article)
 755
 0756                string? searchArticle = null;
 757
 0758                if (!string.IsNullOrEmpty(viewModel.Order.ProductKey) && viewModel.Order.ProductKey.Contains('='))
 759
 760                {
 761                    // Извлекаем артикул из ProductKey (часть до знака =)
 762
 0763                    searchArticle = viewModel.Order.ProductKey.Split('=')[0].Trim();
 764                }
 765
 0766                else if (!string.IsNullOrEmpty(viewModel.Order.Article))
 767
 768                {
 769                    // Используем Article напрямую
 770
 0771                    searchArticle = viewModel.Order.Article.Trim();
 772                }
 773
 774
 0775                if (!string.IsNullOrEmpty(searchArticle))
 776
 777                {
 778                    // Ищем продукт по нормализованному артикулу
 779
 0780                    var existingProduct = await _context.Products
 0781                        .AsNoTracking()
 0782                        .FirstOrDefaultAsync(p => p.Article != null && p.Article == searchArticle);
 783
 784
 0785                    if (existingProduct != null)
 786
 787                    {
 788                        // Копируем данные из найденного продукта
 789
 0790                        viewModel.Order.ProductInfo.Id = existingProduct.Id;
 791
 0792                        viewModel.Order.ProductInfo.Article = existingProduct.Article ?? viewModel.Order.Article;
 793
 0794                        viewModel.Order.ProductInfo.OzonProductId = existingProduct.OzonProductId;
 795
 0796                        viewModel.Order.ProductInfo.FboOzonSkuId = existingProduct.FboOzonSkuId;
 797
 0798                        viewModel.Order.ProductInfo.FbsOzonSkuId = existingProduct.FbsOzonSkuId;
 799
 0800                        viewModel.Order.ProductInfo.CommercialCategory = existingProduct.CommercialCategory ??
 0801                                                                         viewModel.Order.ProductInfo.CommercialCategory;
 802
 0803                        viewModel.Order.ProductInfo.Barcode = existingProduct.Barcode;
 804
 0805                        viewModel.Order.ProductInfo.Volume = existingProduct.Volume;
 806
 0807                        viewModel.Order.ProductInfo.VolumetricWeight = existingProduct.VolumetricWeight ??
 0808                                                                       viewModel.Order.ProductInfo.VolumetricWeight;
 809
 0810                        viewModel.Order.ProductInfo.Weight = existingProduct.Weight;
 811
 0812                        viewModel.Order.ProductInfo.CurrentPriceWithDiscount =
 0813                            existingProduct.CurrentPriceWithDiscount ??
 0814                            viewModel.Order.ProductInfo.CurrentPriceWithDiscount;
 815                    }
 816
 817                    else
 818
 819                    {
 820                        // Если продукт не найден, устанавливаем Article
 821
 0822                        viewModel.Order.ProductInfo.Article = viewModel.Order.Article;
 823                    }
 824                }
 825
 826                else
 827
 828                {
 829                    // Если нет данных для поиска, устанавливаем Article если есть
 830
 0831                    if (!string.IsNullOrEmpty(viewModel.Order.Article))
 832
 833                    {
 0834                        viewModel.Order.ProductInfo.Article = viewModel.Order.Article;
 835                    }
 836                }
 837
 838
 0839                if (viewModel.Order.Supplier == null)
 840                {
 0841                    viewModel.Order.Supplier = await _supplierDataServices.FindOrCreateAsync("Не указан");
 842                }
 843
 844
 845                // Устанавливаем UpdatedBy
 846
 0847                viewModel.Order.UpdatedBy = User.Identity?.Name ?? "System";
 848
 849
 850                // Создаем список с одним заказом для метода AddOrders
 851
 0852                var ordersList = new List<Order> { viewModel.Order };
 853
 854
 855                // Используем метод AddOrders для создания заказа
 856
 0857                var result = await _orderServices.AddOrders(ordersList);
 858
 0859                var addCount = result[0];
 860
 0861                var updateCount = result[1];
 862
 863
 0864                await _cache.Update();
 865
 866
 0867                if (addCount > 0)
 868
 869                {
 0870                    TempData["TransactionResult"] =
 0871                        $"Заказ успешно создан. Добавлено: {addCount}, Обновлено: {updateCount}";
 872
 0873                    return RedirectToAction(nameof(Index));
 874                }
 875
 876                else
 877
 878                {
 0879                    viewModel.ErrorMessage = "Не удалось создать заказ. Возможно, такой заказ уже существует.";
 0880                    viewModel.User = currentUser;
 0881                    return await ReturnCreateViewWithReloadedListsAsync(viewModel);
 882                }
 883            }
 884
 0885            catch (Exception ex)
 886            {
 0887                viewModel.ErrorMessage = $"Ошибка при создании заказа: {ex.Message}";
 0888                viewModel.User = currentUser;
 0889                return await ReturnCreateViewWithReloadedListsAsync(viewModel);
 890            }
 0891        }
 892
 893        private async Task<IActionResult> ReturnCreateViewWithReloadedListsAsync(CreateOrderViewModel viewModel)
 894        {
 0895            await _orderCreateFormService.ReloadListsAsync(viewModel, null);
 0896            return View(viewModel);
 0897        }
 898}

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

#LineLine coverage
 1using System.Collections.Generic;
 2using Microsoft.AspNetCore.Authorization;
 3using Microsoft.AspNetCore.Identity;
 4using Microsoft.AspNetCore.Mvc;
 5using Microsoft.Extensions.Caching.Memory;
 6using Microsoft.Extensions.DependencyInjection;
 7using Microsoft.AspNetCore.Mvc.Rendering;
 8using Newtonsoft.Json;
 9using OServices.FiltersServices.FilterModels;
 10using OzonDomains;
 11using OzonDomains.Models;
 12using OzonDomains.Models.AccessControl;
 13using OzonOrdersWeb.WebServices;
 14using OzonOrdersWeb.WebServices.Currency;
 15using OzonOrdersWeb.WebServices.Cookies;
 16using OzonOrdersWeb.ViewModels.OrderViewModels;
 17using OzonRepositories.Context;
 18using OzonRepositories.Context.Identity;
 19using Services.ApiServices.DropBoxApi;
 20using Services.ApiServices.OzonApi;
 21using Services.ApiServices.YandexApi;
 22using Services.YandexServices;
 23using Services.CacheServices.Cache.OzonOrdersCache;
 24using Services.CacheServices.Cache.UniqueValuesCache;
 25using Services.CacheServices.Cache.UserCacheService;
 26using Services.DataServices;
 27using Services.DataServices.DTO;
 28using Services.FiltersServices.DataFilterManagers;
 29using Services.FiltersServices.SortModels;
 30using Services.ParserServices;
 31using Services.ParserServices.FielParsers;
 32using Services.ParserServices.HelpDictEnum;
 33using Services.PriceСlculationServices;
 34using Services.ReleasServices.ReleaseManager;
 35using Services.TransactionUtilsServices;
 36using Services.LabelServices;
 37using Services.CacheServices.Cache;
 38using System.IO.Compression;
 39using System.Net.Http;
 40using System.Security.Claims;
 41using System.Text;
 42using System.Text.Json;
 43using System.Diagnostics;
 44using Hangfire;
 45using Hangfire.States;
 46using Services.HangfireService;
 47using Microsoft.EntityFrameworkCore;
 48using OzonDomains.Models.OrderCarts.Cart;
 49using OzonRepositories.Data.Bitrix;
 50using Services.ApiServices._1CApi;
 51using Services.ApiServices._1CApi.Models;
 52using Services.CacheServices.Cache.CartCache;
 53using Services.SignalRServices;
 54using SlqStudio.Application.Services.EmailService;
 55using Services.ApiServices.OzonApi.Filters;
 56using OzonDomains.ViewModels;
 57using OzonOrdersWeb.Areas.Admin.OrderList;
 58using OzonOrdersWeb.Areas.Studio2.ViewHelpers;
 59using OzonOrdersWeb.Areas.Studio2.Controllers.Directories;
 60using OzonOrdersWeb.Authorization;
 61using OzonOrdersWeb.WebServices.Orders;
 62
 63
 64namespace OzonOrdersWeb.Controllers
 65
 66{
 67    [Authorize(Roles = "User,Admin")]
 68    [Area("Studio2")]
 69    public partial class OrdersController : Controller
 70
 71    {
 72        private const string ReturnsFilterCookieKey = "FilterDataReturns";
 73        private const string ReturnsSortCookieKey = "SortStateReturns";
 74
 75        /// <summary>
 76        /// Жёсткий потолок строк на страницу списка заказов. Выше — рендер раздувает HTML до 3+ МБ
 77        /// и DOM до ~14k узлов на 100 строк (row-шаблон ~36 КБ/строку). Больше данных — только
 78        /// страницами: номера страниц и кнопка «Показать ещё» работают через AJAX без full reload.
 79        /// </summary>
 80        public const int MaxOrdersPageSize = 100;
 81
 82        private const int DefaultOrdersPageSize = 15;
 83
 84        /// <summary>
 85        /// Определяет строк на страницу: <paramref name="overrideSize"/> (если задан в этом же запросе,
 86        /// когда cookie ещё не долетел до Request) иначе cookie PageSize. Всегда в [1..<see cref="MaxOrdersPageSize"/>]
 87        /// </summary>
 88        private int ResolveOrdersPageSize(int? overrideSize = null)
 89        {
 090            var size = overrideSize is > 0
 091                ? overrideSize.Value
 092                : (int.TryParse(Request.Cookies["PageSize"], out var s) && s > 0 ? s : DefaultOrdersPageSize);
 093            return Math.Clamp(size, 1, MaxOrdersPageSize);
 94        }
 95
 96        private readonly ApplicationDbContext _context;
 97
 98        private readonly IMemoryCache _memoryCache;
 99
 100        private readonly OrdersDataServices _orderServices;
 101
 102        private readonly AppStatusDataServices _appStatusServices;
 103
 104        private readonly ClientStatusDataServices _clientStatusServices;
 105
 106        private readonly SupplierDataServices _supplierDataServices;
 107
 108        private readonly WarehouseDataServices _warehouseDataServices;
 109
 110        private readonly OrderCaster _orderCaster;
 111
 112        private readonly OrderCache _cache;
 113
 114        private readonly OrderDataFilterManager _dataFilterManager;
 115
 116        private readonly CacheUpdater<Order> _cacheUpdater;
 117
 118        private readonly OzonJsonDataBuilder _jsonDataBuilder;
 119
 120        private readonly ReleaseManager _releaseManager;
 121
 122        private readonly TransactionDataServices _transactionDataServices;
 123
 124        private readonly TransactionCache _transactionCache;
 125
 126        private readonly DuplicateOrdersServices _duplicateOrdersServices;
 127
 128        private readonly OzonClientServices _ozonClientServices;
 129
 130        private readonly ExcelParser _excelParser;
 131
 132        private readonly ColumnMappingDataServices _columnMappingDataServices;
 133
 134        private readonly CurrencyRateFetcher _currencyRateFetcher;
 135
 136        private readonly ICurrencyRatesViewBagService _currencyRatesViewBagService;
 137
 138        private readonly DropboxApiClient _dropboxApiClient;
 139
 140        private readonly ILabelFileStorage _labelFileStorage;
 141
 142        private readonly ManufacturerDataService _manufacturerDataService;
 143
 144        private readonly ExcelExporter _excelExporter;
 145
 146        private readonly AccessProfileDataServices _accessProfileDataServices;
 147
 148        private readonly YandexDataManager _yandexDataManager;
 149        private readonly IYandexOrderStatusSyncService _yandexOrderStatusSyncService;
 150
 151        private readonly IUserCacheService _userCacheService;
 152
 153        private readonly OrderCartServices _orderCartServices;
 154
 155        private readonly CartCache _cartCache;
 156
 157        private readonly EtProducerDataServices _etProducerDataServices;
 158
 159        private readonly DeliveryDataServices _deliveryDataServices;
 160
 161        private static string SanitizeFileName(string name)
 162        {
 0163            if (string.IsNullOrWhiteSpace(name))
 0164                return "Client";
 165
 0166            var invalidChars = Path.GetInvalidFileNameChars();
 0167            var cleaned = new string(name.Select(ch => invalidChars.Contains(ch) ? '_' : ch).ToArray());
 0168            return string.IsNullOrWhiteSpace(cleaned) ? "Client" : cleaned;
 169        }
 170
 171        private readonly OzonApiDataManager _ozonApiDataManager;
 172
 173        private readonly OzonRepositories.Authorization.IUserScopeProvider _userScopeProvider;
 174
 175        private readonly Services.Authorization.Services.IPermissionService _permissionService;
 176
 0177        private readonly OzonLabelSessionStore _ozonLabelSessionStore = new();
 178
 179        private readonly IOzonLabelPrintService _ozonLabelPrintService;
 180
 181        private readonly ILabelExistenceService _labelExistenceService;
 182
 183        private readonly ILabelPrintLogService _labelPrintLogService;
 184
 185        private readonly IOrderFilterListsService _orderFilterListsService;
 186
 187        private readonly IOrdersIndexQueryService _ordersIndexQueryService;
 188
 189        private readonly IOrderDataScopeQueryApplier _dataScopeApplier;
 190
 191        private readonly IOrdersIndexPageDetailsService _ordersIndexPageDetailsService;
 192
 193        private readonly IOrderReturnInfoService _orderReturnInfoService;
 194
 195        private readonly IOrdersClientStatusSyncService _clientStatusSyncService;
 196
 197        private readonly IOrdersIndexClientStatusRefreshService _indexClientStatusRefreshService;
 198
 199        private readonly IOrdersIndexViewModelEnricher _ordersIndexViewModelEnricher;
 200
 201        private readonly IOrdersIndexPageViewDataService _ordersIndexPageViewDataService;
 202
 203        private readonly IOrderDataScopeIdFilterService _dataScopeIdFilterService;
 204
 205        private readonly IOrdersOzonShipService _ozonShipService;
 206
 207        private readonly IOzonExemplarService _ozonExemplarService;
 208
 209        private readonly IOrderCreateFormService _orderCreateFormService;
 210
 211        private readonly Services.OzonHandoverServices.IOzonHandoverImportService _handoverImportService;
 212
 213
 0214        public OrdersController(ApplicationDbContext context,
 0215            IMemoryCache memoryCache,
 0216            OrdersDataServices orderRepository,
 0217            AppStatusDataServices appStatusDataServices,
 0218            ClientStatusDataServices clientStatusDataServices,
 0219            SupplierDataServices supplierDataServices,
 0220            WarehouseDataServices warehouseDataServices,
 0221            OrderCaster orderCaster,
 0222            OrderCache orderCache,
 0223            OrderDataFilterManager dataFilterManager,
 0224            CacheUpdater<Order> cacheUpdater,
 0225            OzonJsonDataBuilder jsonDataBuilder,
 0226            ReleaseManager releaseManager,
 0227            TransactionDataServices transactionDataServices,
 0228            TransactionCache transactionCache,
 0229            DuplicateOrdersServices duplicateOrdersServices,
 0230            OzonClientServices ozonClientServices,
 0231            ExcelParser excelParser,
 0232            ColumnMappingDataServices columnMappingDataServices,
 0233            CurrencyRateFetcher currencyRateFetcher,
 0234            ICurrencyRatesViewBagService currencyRatesViewBagService,
 0235            DropboxApiClient dropboxApiClient,
 0236            ILabelFileStorage labelFileStorage,
 0237            ManufacturerDataService manufacturerDataService,
 0238            ExcelExporter excelExporter,
 0239            AccessProfileDataServices accessProfileDataServices,
 0240            YandexDataManager yandexDataManager,
 0241            IYandexOrderStatusSyncService yandexOrderStatusSyncService,
 0242            IUserCacheService userCacheService,
 0243            OrderCartServices orderCartServices,
 0244            CartCache cartCache,
 0245            EtProducerDataServices etProducerDataServices,
 0246            DeliveryDataServices deliveryDataServices,
 0247            OzonApiDataManager ozonApiDataManager,
 0248            OzonRepositories.Authorization.IUserScopeProvider userScopeProvider,
 0249            Services.Authorization.Services.IPermissionService permissionService,
 0250            IOzonLabelPrintService ozonLabelPrintService,
 0251            ILabelExistenceService labelExistenceService,
 0252            ILabelPrintLogService labelPrintLogService,
 0253            IOrderFilterListsService orderFilterListsService,
 0254            IOrdersIndexQueryService ordersIndexQueryService,
 0255            IOrderDataScopeQueryApplier dataScopeApplier,
 0256            IOrdersIndexPageDetailsService ordersIndexPageDetailsService,
 0257            IOrderReturnInfoService orderReturnInfoService,
 0258            IOrdersClientStatusSyncService clientStatusSyncService,
 0259            IOrdersIndexClientStatusRefreshService indexClientStatusRefreshService,
 0260            IOrdersIndexViewModelEnricher ordersIndexViewModelEnricher,
 0261            IOrdersIndexPageViewDataService ordersIndexPageViewDataService,
 0262            IOrderDataScopeIdFilterService dataScopeIdFilterService,
 0263            IOrdersOzonShipService ozonShipService,
 0264            IOzonExemplarService ozonExemplarService,
 0265            IOrderCreateFormService orderCreateFormService,
 0266            Services.OzonHandoverServices.IOzonHandoverImportService handoverImportService)
 267
 268        {
 0269            _context = context;
 0270            _memoryCache = memoryCache;
 271
 0272            _orderServices = orderRepository;
 273
 0274            _appStatusServices = appStatusDataServices;
 275
 0276            _clientStatusServices = clientStatusDataServices;
 277
 0278            _supplierDataServices = supplierDataServices;
 279
 0280            _warehouseDataServices = warehouseDataServices;
 281
 0282            _orderCaster = orderCaster;
 283
 0284            _cache = orderCache;
 285
 0286            _dataFilterManager = dataFilterManager;
 287
 0288            _cacheUpdater = cacheUpdater;
 289
 0290            _jsonDataBuilder = jsonDataBuilder;
 291
 0292            _releaseManager = releaseManager;
 293
 0294            _transactionDataServices = transactionDataServices;
 295
 0296            _transactionCache = transactionCache;
 297
 0298            _duplicateOrdersServices = duplicateOrdersServices;
 299
 0300            _ozonClientServices = ozonClientServices;
 301
 0302            _excelParser = excelParser;
 303
 0304            _columnMappingDataServices = columnMappingDataServices;
 305
 0306            _currencyRateFetcher = currencyRateFetcher;
 307
 0308            _currencyRatesViewBagService = currencyRatesViewBagService;
 309
 0310            _dropboxApiClient = dropboxApiClient;
 311
 0312            _labelFileStorage = labelFileStorage;
 313
 0314            _manufacturerDataService = manufacturerDataService;
 315
 0316            _excelExporter = excelExporter;
 317
 0318            _accessProfileDataServices = accessProfileDataServices;
 319
 0320            _yandexDataManager = yandexDataManager;
 0321            _yandexOrderStatusSyncService = yandexOrderStatusSyncService;
 322
 0323            _userCacheService = userCacheService;
 324
 0325            _orderCartServices = orderCartServices;
 326
 0327            _cartCache = cartCache;
 328
 0329            _etProducerDataServices = etProducerDataServices;
 330
 0331            _deliveryDataServices = deliveryDataServices;
 332
 0333            _warehouseDataServices = warehouseDataServices;
 334
 0335            _ozonApiDataManager = ozonApiDataManager;
 336
 0337            _userScopeProvider = userScopeProvider;
 338
 0339            _permissionService = permissionService;
 340
 0341            _ozonLabelPrintService = ozonLabelPrintService;
 0342            _labelExistenceService = labelExistenceService;
 0343            _labelPrintLogService = labelPrintLogService;
 0344            _orderFilterListsService = orderFilterListsService;
 0345            _ordersIndexQueryService = ordersIndexQueryService;
 0346            _dataScopeApplier = dataScopeApplier;
 0347            _ordersIndexPageDetailsService = ordersIndexPageDetailsService;
 0348            _orderReturnInfoService = orderReturnInfoService;
 0349            _clientStatusSyncService = clientStatusSyncService;
 0350            _indexClientStatusRefreshService = indexClientStatusRefreshService;
 0351            _ordersIndexViewModelEnricher = ordersIndexViewModelEnricher;
 0352            _ordersIndexPageViewDataService = ordersIndexPageViewDataService;
 0353            _dataScopeIdFilterService = dataScopeIdFilterService;
 0354            _ozonShipService = ozonShipService;
 0355            _ozonExemplarService = ozonExemplarService;
 0356            _orderCreateFormService = orderCreateFormService;
 0357            _handoverImportService = handoverImportService;
 0358        }
 359
 360        private async Task<IReadOnlyDictionary<string, bool>> BuildOrderListFieldReadMapAsync(string userId, Cancellatio
 361        {
 0362            return await OrderListFieldReadAccessBuilder.FromUserIdAsync(_permissionService, userId, ct);
 0363        }
 364
 365        private static IReadOnlyDictionary<string, bool> BuildOrderListFieldReadMapFromBundle(
 366            Services.Authorization.Dtos.UserPermissionsBundle bundle)
 367        {
 0368            return OrderListFieldReadAccessBuilder.FromBundle(bundle);
 369        }
 370
 371
 372        // Без ResponseCache: HTML персонализирован (RLS, cookies фильтров, этикетки Dropbox).
 373        public async Task<IActionResult> Returns(OrderSortState sortOrder = OrderSortState.StandardState, int page = 1)
 374
 375        {
 0376            var user = await _userCacheService.GetCachedUserAsync(User);
 0377            NormalizeSyntheticAccessProfile(user);
 378
 0379            AccessProfile? accessProfile = null;
 380
 0381            if (user?.AccessProfileId != null)
 382
 383            {
 0384                accessProfile = await _accessProfileDataServices.GetAccessProfileAsync(user.AccessProfileId.Value);
 385            }
 386
 387
 0388            var isAdmin = User.IsInRole("Admin");
 0389            ViewBag.IsAdmin = isAdmin;
 390
 0391            SaveSortStateCookie(sortOrder, ReturnsSortCookieKey);
 0392            SetInfoMessage();
 393
 0394            string? filterDataString = HttpContext.Request.Cookies[ReturnsFilterCookieKey];
 395
 0396            var filterData = new OrderFilterModel
 0397            {
 0398                OnlyWithReturns = true
 0399            };
 400
 0401            bool hasFilters = false;
 402
 0403            if (!string.IsNullOrEmpty(filterDataString))
 404
 405            {
 0406                filterData = JsonConvert.DeserializeObject<OrderFilterModel>(filterDataString) ?? new OrderFilterModel()
 0407                filterData.OnlyWithReturns = true;
 408
 0409                hasFilters = !_dataFilterManager.AreAllPropertiesNull(filterData);
 410
 0411                if (!string.IsNullOrEmpty(filterData.Barcode))
 412
 413                {
 0414                    ViewData["SearchByBarcode"] = true;
 415
 0416                    ViewData["BarcodeValue"] = filterData.Barcode;
 417                }
 418            }
 419
 0420            int pageSize = ResolveOrdersPageSize();
 421
 0422            if (page < 1)
 423
 0424                page = 1;
 425
 0426            bool hasUserAccessRestrictions = (accessProfile != null && accessProfile.ScopeItems.Any())
 0427                                             || !string.IsNullOrEmpty(_userScopeProvider.GetFilterExpression("Order"));
 428
 0429            if (sortOrder == OrderSortState.StandardState && Request.Cookies.ContainsKey(ReturnsSortCookieKey))
 430            {
 0431                sortOrder = GetSortStateCookie(ReturnsSortCookieKey);
 432            }
 433
 0434            var indexQueryResult = await LoadOrdersIndexPageAsync(
 0435                filterData, page, pageSize, sortOrder, accessProfile, hasFilters, hasUserAccessRestrictions,
 0436                isReturnsListPage: true);
 0437            var totalOrdersCount = indexQueryResult.TotalCount;
 438
 0439            var filterListsByData = HttpContext.Request.Cookies["FilterListsByData"] == "true";
 440
 0441            await ApplyFilterListsToViewAsync(
 0442                filterListsByData ? indexQueryResult.Orders : null,
 0443                accessProfile,
 0444                filterListsByData);
 445
 0446            SetSortOrderViewData(sortOrder);
 447
 0448            var (distinctOrders, ordersForPage) = ShapeOrdersAfterLoad(
 0449                indexQueryResult.Orders,
 0450                indexQueryResult.RequiresInMemorySort,
 0451                sortOrder,
 0452                filterData,
 0453                pageSize);
 454
 0455            if (filterData.SyncClientStatusesOnLoad && ordersForPage.Count > 0)
 456            {
 0457                await _indexClientStatusRefreshService.RefreshOnPageLoadAsync(
 0458                    distinctOrders,
 459                    ordersForPage.Select(o => o.Id).Distinct().ToArray(),
 0460                    HttpContext.RequestAborted);
 461            }
 462
 0463            await _ordersIndexPageDetailsService.LoadOrderDetailsForPageAsync(
 0464                ordersForPage, accessProfile, HttpContext.RequestAborted);
 465
 466            const int viewModelPage = 1;
 0467            var pageViewModel = new OrderPageViewModel<Order, OrderFilterModel>(
 0468                distinctOrders,
 0469                viewModelPage, pageSize, filterData, returnableCount: 0);
 470
 0471            _ordersIndexPageDetailsService.ApplyDbPagination(pageViewModel, totalOrdersCount, page, pageSize);
 472
 0473            await _ordersIndexPageDetailsService.PopulateUniqueCountsAsync(
 0474                pageViewModel, ordersForPage, filterData.IncludeArchived, HttpContext.RequestAborted);
 475
 0476            if (user == null)
 477            {
 0478                return Redirect("/Identity/Account/Login");
 479            }
 480
 0481            pageViewModel.User = user;
 482
 0483            var enrichResult = await _ordersIndexViewModelEnricher.EnrichAsync(new OrdersIndexViewModelEnrichRequest
 0484            {
 0485                ViewModel = pageViewModel,
 0486                User = user,
 0487                AccessProfile = accessProfile,
 0488                OrdersForPage = ordersForPage,
 0489                IsAdmin = isAdmin,
 0490                CancellationToken = HttpContext.RequestAborted
 0491            });
 492
 0493            await ApplyOrdersIndexPageViewDataAsync(
 0494                user.Id, isAdmin, HttpContext.RequestAborted, enrichResult.PermissionBundle);
 0495            ViewBag.ReturnVisualStatusOptions = OzonStatus.ReturnVisualStatuses;
 0496            ViewBag.ReturnWarehouseOptions = await _orderReturnInfoService.GetWarehouseOptionsAsync(HttpContext.RequestA
 497
 0498            ViewData["IndexAction"] = "Returns";
 0499            ViewData["PaginationAction"] = "Returns";
 0500            ViewData["PaginationController"] = "Orders";
 501
 0502            ViewData["LabelExistsByShipmentNumber"] = pageViewModel.LabelExistsByShipmentNumber;
 503
 0504            return View("~/Areas/Studio2/Views/Orders/ReturnsIndex.cshtml", pageViewModel);
 0505        }
 506
 507        [HttpPost]
 508        public async Task<IActionResult> Returns(OrderFilterModel filterData, int page = 1, string buttonState = "",
 509            string clearSort = "")
 510
 511        {
 0512            await Task.CompletedTask;
 0513            if (!string.IsNullOrEmpty(clearSort) && clearSort.ToLower() == "true")
 514
 515            {
 0516                Response.Cookies.Delete(ReturnsSortCookieKey);
 0517                Response.Cookies.Append(ReturnsSortCookieKey, OrderSortState.StandardState.ToString());
 518            }
 519
 0520            filterData.OnlyWithReturns = true;
 521
 0522            var serializedFilterData = JsonConvert.SerializeObject(filterData);
 0523            HttpContext.Response.Cookies.Append(ReturnsFilterCookieKey, serializedFilterData);
 524
 0525            return RedirectToAction(nameof(Returns), new { page = page });
 0526        }
 527
 528        public async Task<IActionResult> Index(OrderSortState sortOrder = OrderSortState.StandardState, int page = 1,
 529            string? search = null, string? shipmentNumber = null)
 530
 531        {
 0532            var user = await _userCacheService.GetCachedUserAsync(User);
 0533            NormalizeSyntheticAccessProfile(user);
 534
 0535            AccessProfile? accessProfile = null;
 536
 0537            if (user?.AccessProfileId != null)
 538
 539            {
 0540                accessProfile = await _accessProfileDataServices.GetAccessProfileAsync(user.AccessProfileId.Value);
 541            }
 542
 543
 544            // Проверяем, является ли пользователь админом
 545
 0546            var isAdmin = User.IsInRole("Admin");
 547
 548
 549            // Передаём информацию о том, является ли пользователь админом
 550
 0551            ViewBag.IsAdmin = isAdmin;
 552
 553            // Права пользователя грузятся параллельно с конвейером заказов: PermissionService
 554            // работает через собственный IDbContextFactory + memcache (не делит scoped ApplicationDbContext),
 555            // поэтому запуск задачи здесь безопасен. На cache-hit — мгновенно, на cold path перекрывает
 556            // ~8 запросов загрузки заказов.
 0557            Task<Services.Authorization.Dtos.UserPermissionsBundle>? permissionBundleTask =
 0558                (user != null && !isAdmin)
 0559                    ? _permissionService.GetUserPermissionsAsync(user.Id, HttpContext.RequestAborted)
 0560                    : null;
 561
 562
 0563            SaveSortStateCookie(sortOrder);
 564
 565
 0566            SetInfoMessage();
 567
 568
 0569            string? filterDataString = HttpContext.Request.Cookies["FilterData"];
 570
 0571            var filterData = new OrderFilterModel();
 572
 0573            bool hasFilters = false;
 574
 0575            var globalSearch = string.IsNullOrWhiteSpace(search) ? null : search.Trim();
 0576            var shipmentNumberFilter = string.IsNullOrWhiteSpace(shipmentNumber) ? null : shipmentNumber.Trim();
 0577            if (!string.IsNullOrEmpty(globalSearch))
 578            {
 0579                page = 1;
 0580                filterData = new OrderFilterModel
 0581                {
 0582                    Search = globalSearch,
 0583                    GroupByShipment = true
 0584                };
 0585                hasFilters = true;
 0586                HttpContext.Response.Cookies.Append("FilterData", JsonConvert.SerializeObject(filterData));
 0587                ViewData["GlobalSearchQuery"] = globalSearch;
 588            }
 0589            else if (!string.IsNullOrEmpty(shipmentNumberFilter))
 590            {
 0591                page = 1;
 0592                filterData = new OrderFilterModel
 0593                {
 0594                    ShipmentNumber = shipmentNumberFilter,
 0595                    GroupByShipment = true
 0596                };
 0597                hasFilters = true;
 0598                HttpContext.Response.Cookies.Append("FilterData", JsonConvert.SerializeObject(filterData));
 599            }
 0600            else if (!string.IsNullOrEmpty(filterDataString))
 601
 602            {
 0603                filterData = JsonConvert.DeserializeObject<OrderFilterModel>(filterDataString) ?? new OrderFilterModel()
 604
 0605                hasFilters = !_dataFilterManager.AreAllPropertiesNull(filterData);
 606
 607
 608                // Проверяем, был ли поиск по штрихкоду (EAN)
 609
 0610                if (!string.IsNullOrEmpty(filterData.Barcode))
 611
 612                {
 0613                    ViewData["SearchByBarcode"] = true;
 614
 0615                    ViewData["BarcodeValue"] = filterData.Barcode;
 616                }
 617            }
 618
 0619            int pageSize = ResolveOrdersPageSize();
 0620            bool disablePagination = false;
 621
 622            // Поиск по нескольким номерам заказов: раньше грузили всё одной страницей (pageSize=1000).
 623            // Теперь — обычными страницами по MaxOrdersPageSize; счётчик «найдено X из Y» остаётся,
 624            // пользователь листает результат номерами страниц / «Показать ещё».
 0625            if (!string.IsNullOrWhiteSpace(filterData.ShipmentNumber) &&
 0626                OrderDataFilterManager.HasMultipleShipmentNumbers(filterData.ShipmentNumber))
 627            {
 0628                page = page < 1 ? 1 : page;
 629            }
 630
 631            // Защита от некорректных значений page (должно быть >= 1)
 632
 0633            if (page < 1)
 634
 0635                page = 1;
 636
 637
 0638            bool hasUserAccessRestrictions = (accessProfile != null && accessProfile.ScopeItems.Any())
 0639                                             || !string.IsNullOrEmpty(_userScopeProvider.GetFilterExpression("Order"));
 640
 641            // Определяем sortOrder заранее для использования в DB-level сортировке
 0642            if (sortOrder == OrderSortState.StandardState && Request.Cookies.ContainsKey("SortState"))
 643            {
 0644                sortOrder = GetSortStateCookie();
 645            }
 646
 0647            var indexQueryResult = await LoadOrdersIndexPageAsync(
 0648                filterData, page, pageSize, sortOrder, accessProfile, hasFilters, hasUserAccessRestrictions);
 0649            var totalOrdersCount = indexQueryResult.TotalCount;
 650
 651
 652            // Формируем списки для фильтров (проверяем настройку фильтрации)
 653
 0654            var filterListsByData = HttpContext.Request.Cookies["FilterListsByData"] == "true";
 655
 0656            await ApplyFilterListsToViewAsync(
 0657                filterListsByData ? indexQueryResult.Orders : null,
 0658                accessProfile,
 0659                filterListsByData);
 660
 661            // Подрезаем dropdown-списки под DataScope-ы текущего пользователя (для новой системы авторизации)
 662
 663
 0664            SetSortOrderViewData(sortOrder);
 665
 0666            var (distinctOrders, ordersForPage) = ShapeOrdersAfterLoad(
 0667                indexQueryResult.Orders,
 0668                indexQueryResult.RequiresInMemorySort,
 0669                sortOrder,
 0670                filterData,
 0671                pageSize);
 672
 673            // Авто-синхронизация статусов клиента только для заказов на текущей странице,
 674            // чтобы не перегружать внешние API. Выполняется только если включён флаг.
 0675            if (filterData.SyncClientStatusesOnLoad && ordersForPage.Count > 0)
 676            {
 0677                await _indexClientStatusRefreshService.RefreshOnPageLoadAsync(
 0678                    distinctOrders,
 679                    ordersForPage.Select(o => o.Id).Distinct().ToArray(),
 0680                    HttpContext.RequestAborted);
 681            }
 682
 0683            await _ordersIndexPageDetailsService.LoadOrderDetailsForPageAsync(
 0684                ordersForPage, accessProfile, HttpContext.RequestAborted);
 685
 686            const int viewModelPage = 1;
 0687            var pageViewModel = new OrderPageViewModel<Order, OrderFilterModel>(
 0688                distinctOrders,
 0689                viewModelPage, pageSize, filterData, returnableCount: 0);
 690
 0691            _ordersIndexPageDetailsService.ApplyDbPagination(
 0692                pageViewModel, totalOrdersCount, page, pageSize, disablePagination);
 693
 0694            await _ordersIndexPageDetailsService.PopulateUniqueCountsAsync(
 0695                pageViewModel, ordersForPage, filterData.IncludeArchived, HttpContext.RequestAborted);
 696
 0697            if (user == null)
 698
 699            {
 0700                return Redirect("/Identity/Account/Login");
 701            }
 702
 0703            pageViewModel.User = user;
 704
 0705            Services.Authorization.Dtos.UserPermissionsBundle? permissionBundle =
 0706                permissionBundleTask != null ? await permissionBundleTask : null;
 707
 0708            var enrichResult = await _ordersIndexViewModelEnricher.EnrichAsync(new OrdersIndexViewModelEnrichRequest
 0709            {
 0710                ViewModel = pageViewModel,
 0711                User = user,
 0712                AccessProfile = accessProfile,
 0713                OrdersForPage = ordersForPage,
 0714                IsAdmin = isAdmin,
 0715                PrefetchedPermissionBundle = permissionBundle,
 0716                CancellationToken = HttpContext.RequestAborted
 0717            });
 718
 0719            await ApplyOrdersIndexPageViewDataAsync(
 0720                user.Id, isAdmin, HttpContext.RequestAborted, enrichResult.PermissionBundle ?? permissionBundle);
 0721            ViewBag.ReturnVisualStatusOptions = OzonStatus.ReturnVisualStatuses;
 722
 0723            ViewData["LabelExistsByShipmentNumber"] = pageViewModel.LabelExistsByShipmentNumber;
 0724            ViewData["UseOrdersIndexAjax"] = true;
 725
 0726            return View(pageViewModel);
 0727        }
 728
 729
 730        [HttpPost]
 731        public async Task<IActionResult> Index(OrderFilterModel filterData, int page = 1, string buttonState = "",
 732            string clearSort = "")
 733
 734        {
 0735            filterData ??= new OrderFilterModel();
 0736            var user = await _userCacheService.GetCachedUserAsync(User);
 0737            NormalizeSyntheticAccessProfile(user);
 738
 0739            AccessProfile? accessProfile = null;
 740
 0741            if (user?.AccessProfileId != null)
 742
 743            {
 0744                accessProfile = await _accessProfileDataServices.GetAccessProfileAsync(user.AccessProfileId.Value);
 745            }
 746
 747
 748            // Очищаем сортировку если передан параметр clearSort
 749
 0750            if (!string.IsNullOrEmpty(clearSort) && clearSort.ToLower() == "true")
 751
 752            {
 0753                Response.Cookies.Delete("SortState");
 754
 0755                Response.Cookies.Append("SortState", OrderSortState.StandardState.ToString());
 756            }
 757
 758
 759            // Проверяем, является ли пользователь админом
 760
 0761            var isAdmin = User.IsInRole("Admin");
 762
 763
 764            // Передаём информацию о том, является ли пользователь админом
 765
 0766            ViewBag.IsAdmin = isAdmin;
 767
 768
 0769            ViewData["ButtonState"] = buttonState;
 770
 771
 772            // Проверяем наличие фильтров
 773
 0774            bool hasFilters = !_dataFilterManager.AreAllPropertiesNull(filterData);
 775
 0776            bool hasUserAccessRestrictions = (accessProfile != null && accessProfile.ScopeItems.Any())
 0777                                             || !string.IsNullOrEmpty(_userScopeProvider.GetFilterExpression("Order"));
 778
 779            // Определяем sortOrder для использования в DB-level сортировке
 0780            var sortOrder = GetSortStateCookie();
 781
 782
 0783            int pageSize = ResolveOrdersPageSize();
 784
 785            // Поиск по нескольким номерам заказов — обычными страницами по MaxOrdersPageSize
 786            // (раньше грузили всё одной страницей pageSize=1000).
 0787            if (!string.IsNullOrWhiteSpace(filterData.ShipmentNumber) &&
 0788                OrderDataFilterManager.HasMultipleShipmentNumbers(filterData.ShipmentNumber))
 789            {
 0790                page = page < 1 ? 1 : page;
 791            }
 792
 793            // Защита от некорректных значений page (должно быть >= 1)
 794
 0795            if (page < 1)
 796
 0797                page = 1;
 798
 799
 0800            var indexQueryResult = await LoadOrdersIndexPageAsync(
 0801                filterData, page, pageSize, sortOrder, accessProfile, hasFilters, hasUserAccessRestrictions);
 0802            var totalOrdersCount = indexQueryResult.TotalCount;
 803
 804
 805            // Формируем списки для фильтров (проверяем настройку фильтрации)
 806
 0807            var filterListsByData = HttpContext.Request.Cookies["FilterListsByData"] == "true";
 808
 0809            await ApplyFilterListsToViewAsync(
 0810                filterListsByData ? indexQueryResult.Orders : null,
 0811                accessProfile,
 0812                filterListsByData);
 813
 814            // Подрезаем dropdown-списки под DataScope-ы текущего пользователя
 815
 816
 0817            var serializedFilterData = JsonConvert.SerializeObject(filterData);
 818
 0819            HttpContext.Response.Cookies.Append("FilterData", serializedFilterData);
 820
 821
 0822            SetSortOrderViewData(sortOrder);
 823
 0824            var (distinctOrders, ordersForPage) = ShapeOrdersAfterLoad(
 0825                indexQueryResult.Orders,
 0826                indexQueryResult.RequiresInMemorySort,
 0827                sortOrder,
 0828                filterData,
 0829                pageSize);
 830
 0831            await _ordersIndexPageDetailsService.LoadOrderDetailsForPageAsync(
 0832                ordersForPage, accessProfile, HttpContext.RequestAborted);
 833
 834            const int viewModelPage = 1;
 0835            var pageViewModel = new OrderPageViewModel<Order, OrderFilterModel>(
 0836                distinctOrders,
 0837                viewModelPage, pageSize, filterData!, returnableCount: 0);
 838
 0839            _ordersIndexPageDetailsService.ApplyDbPagination(pageViewModel, totalOrdersCount, page, pageSize);
 840
 841
 842            // Проверяем, был ли поиск по штрихкоду (EAN) и найдены ли заказы
 843
 0844            if (!string.IsNullOrEmpty(filterData!.Barcode) && distinctOrders.Any())
 845
 846            {
 0847                TempData["SearchByBarcode"] = true;
 848
 0849                TempData["BarcodeValue"] = filterData.Barcode;
 850
 0851                TempData["FoundOrdersCount"] = distinctOrders.Count;
 852            }
 853
 854            // Проверяем, фильтруется ли по нескольким номерам заказов
 0855            if (!string.IsNullOrWhiteSpace(filterData?.ShipmentNumber) &&
 0856                OrderDataFilterManager.HasMultipleShipmentNumbers(filterData.ShipmentNumber))
 857            {
 0858                var searchedNumbers = OrderDataFilterManager.GetShipmentNumbersFromFilter(filterData.ShipmentNumber);
 0859                var foundList = searchedNumbers
 0860                    .Where(n => distinctOrders.Any(o => OrderDataFilterManager.ShipmentNumberMatchesSearch(o.ShipmentNum
 0861                    .ToList();
 0862                var notFoundList = searchedNumbers
 0863                    .Where(n => distinctOrders.All(o => !OrderDataFilterManager.ShipmentNumberMatchesSearch(o.ShipmentNu
 0864                    .ToList();
 0865                TempData["MultipleShipmentNumberSearch"] = true;
 0866                TempData["FoundOrdersCountForShipmentNumbers"] = foundList.Count;
 0867                TempData["ShipmentNumbersFound"] = string.Join(",", foundList);
 0868                TempData["ShipmentNumbersSearchedCount"] = searchedNumbers.Count;
 0869                if (notFoundList.Count > 0)
 0870                    TempData["ShipmentNumbersNotFound"] = string.Join(",", notFoundList);
 871            }
 872
 873
 0874            await _ordersIndexPageDetailsService.PopulateUniqueCountsAsync(
 0875                pageViewModel, distinctOrders, filterData!.IncludeArchived, HttpContext.RequestAborted);
 876
 0877            if (user == null)
 878
 879            {
 0880                return Redirect("/Identity/Account/Login");
 881            }
 882
 0883            pageViewModel.User = user;
 884
 0885            if (!string.IsNullOrWhiteSpace(filterData?.ShipmentNumber) &&
 0886                OrderDataFilterManager.HasMultipleShipmentNumbers(filterData.ShipmentNumber))
 887            {
 0888                await _ordersIndexViewModelEnricher.EnrichAsync(new OrdersIndexViewModelEnrichRequest
 0889                {
 0890                    ViewModel = pageViewModel,
 0891                    User = user,
 0892                    AccessProfile = accessProfile,
 0893                    OrdersForPage = ordersForPage,
 0894                    IsAdmin = isAdmin,
 0895                    LoadFieldReadAccess = false,
 0896                    CancellationToken = HttpContext.RequestAborted
 0897                });
 0898                ViewData["LabelExistsByShipmentNumber"] = pageViewModel.LabelExistsByShipmentNumber;
 0899                await ApplyOrdersIndexPageViewDataAsync(user.Id, isAdmin, HttpContext.RequestAborted);
 0900                return View("~/Areas/Studio2/Views/Orders/Index.cshtml", pageViewModel);
 901            }
 902
 903
 904            // Используем POST-Redirect-GET паттерн для предотвращения предупреждения о повторной отправке формы
 905
 0906            return RedirectToAction(nameof(Index), new { page = page });
 0907        }
 908
 909
 910        public async Task<IActionResult> ViewOrders(int[] ids)
 911        {
 0912            List<Order> ordereToView = [];
 0913            ids = await FilterOrderIdsByDataScopeAsync(ids);
 914
 0915            var returnableCount = 0; // не используется в UI; COUNT по всей таблице убран для скорости Index
 916
 0917            SetInfoMessage();
 918
 919
 0920            var user = await _userCacheService.GetCachedUserAsync(User);
 921
 0922            var isAdmin = User.IsInRole("Admin");
 923
 0924            ViewBag.IsAdmin = isAdmin;
 925
 0926            AccessProfile? accessProfile = null;
 927
 0928            if (user?.AccessProfileId != null)
 929
 930            {
 0931                accessProfile = await _accessProfileDataServices.GetAccessProfileAsync(user.AccessProfileId.Value);
 932            }
 933
 934
 0935            var orders = await _orderServices.GetOrdersNoTracking(ids);
 0936            ordereToView.AddRange(orders);
 937
 938
 0939            string? filterDataString = HttpContext.Request.Cookies["FilterData"];
 940
 0941            var filterData = new OrderFilterModel();
 942
 0943            if (!string.IsNullOrEmpty(filterDataString))
 944
 945            {
 0946                filterData = JsonConvert.DeserializeObject<OrderFilterModel>(filterDataString) ?? new OrderFilterModel()
 947            }
 948
 949
 0950            var pageViewModel = new OrderPageViewModel<Order, OrderFilterModel>(ordereToView, 1, ordereToView.Count(),
 0951                filterData, returnableCount)
 0952
 0953            {
 0954                UniqueArticles = await _orderServices.GetArticleCountsForOrdersAsync(ordereToView, filterData.IncludeArc
 0955
 0956                UniqueDeliveryCitys = await _orderServices.GetDeliveryCityCountsForOrdersAsync(ordereToView, filterData.
 0957
 0958                UniqueNumbers = await _orderServices.GetShipmentBaseCountsForOrdersAsync(ordereToView, filterData.Includ
 0959            };
 960
 0961            if (user == null)
 962            {
 0963                return Redirect("/Identity/Account/Login");
 964            }
 965
 0966            pageViewModel.User = user;
 967
 0968            if (user != null)
 969            {
 0970                pageViewModel.OrderListFieldReadAccess = await BuildOrderListFieldReadMapAsync(user.Id, HttpContext.Requ
 971            }
 972
 0973            if (pageViewModel.User != null && pageViewModel.User.AccessProfileId != null)
 974
 975            {
 0976                pageViewModel.User.AccessProfile = accessProfile ?? await _context.AccessProfiles.FindAsync(pageViewMode
 977            }
 978
 979
 980
 981            // Формируем списки для фильтров (проверяем настройку фильтрации)
 982
 0983            var filterListsByData = HttpContext.Request.Cookies["FilterListsByData"] == "true";
 984
 0985            await ApplyFilterListsToViewAsync(
 0986                filterListsByData ? ordereToView : null,
 0987                accessProfile,
 0988                filterListsByData);
 989
 990            // Подрезаем dropdown-списки под DataScope-ы текущего пользователя
 991
 992
 0993            await ApplyOrdersIndexPageViewDataAsync(user!.Id, isAdmin, HttpContext.RequestAborted);
 994
 0995            return View("~/Areas/Studio2/Views/Orders/Index.cshtml", pageViewModel);
 0996        }
 997
 998
 999
 1000        [HttpPost]
 1001        public async Task<IActionResult> DelSortStateCookie()
 1002
 1003        {
 01004            await Task.CompletedTask;
 01005            Response.Cookies.Delete("SortState");
 1006
 01007            Response.Cookies.Append("SortState", OrderSortState.StandardState.ToString());
 1008
 01009            return RedirectToAction(nameof(System.Index));
 01010        }
 1011
 1012
 1013        public async Task<IActionResult> DelSortStateCookieForV2()
 1014
 1015        {
 01016            await Task.CompletedTask;
 01017            Response.Cookies.Delete("SortState");
 1018
 01019            Response.Cookies.Append("SortState", OrderSortState.StandardState.ToString());
 1020
 01021            return RedirectToAction(nameof(Index));
 01022        }
 1023
 1024
 1025        [HttpPost]
 1026        public async Task<IActionResult> SetPageSize(int size)
 1027
 1028        {
 01029            await Task.CompletedTask;
 01030            size = Math.Clamp(size, 1, MaxOrdersPageSize);
 1031
 01032            Response.Cookies.Append("PageSize", size.ToString());
 1033
 01034            return RedirectToAction(nameof(System.Index));
 01035        }
 1036
 1037
 1038        [HttpPost]
 1039        public async Task<IActionResult> SetPageSizeForV2(int size)
 1040
 1041        {
 01042            await Task.CompletedTask;
 01043            size = Math.Clamp(size, 1, MaxOrdersPageSize);
 1044
 01045            Response.Cookies.Append("PageSize", size.ToString());
 1046
 01047            return RedirectToAction(nameof(Index));
 01048        }
 1049
 1050
 1051        public async Task<IActionResult> Update()
 1052
 1053        {
 01054            await _cacheUpdater.Update(_cache);
 1055
 01056            return RedirectToAction(nameof(System.Index));
 01057        }
 1058
 1059
 1060        public async Task<IActionResult> UpdateV2()
 1061
 1062        {
 01063            await _cacheUpdater.Update(_cache);
 1064
 01065            return RedirectToAction(nameof(Index));
 01066        }
 1067
 1068
 1069
 1070
 1071
 1072
 1073        public async Task<IActionResult> Delete(int id, int page)
 1074
 1075        {
 1076            try
 1077
 1078            {
 01079                int result = await _orderServices.DeleteOrder(id);
 1080
 1081
 01082                if (result == 0)
 1083
 1084                {
 01085                    TempData["ErorrResult"] = $"Для удаления доступны только заказы со статусом '<b>Не указан</b>'.";
 1086                }
 1087
 01088                else if (result == 1)
 1089
 1090                {
 01091                    await _cache.RemoveOrderFromCache(id);
 1092
 01093                    ClearSelectedIdsSession();
 1094
 01095                    await NotificationService.NotifyAllAsync($"Заказаid={id} успешно удален.");
 1096
 01097                    TempData["TransactionResult"] = $"Заказ успешно удален.";
 1098                }
 1099
 1100                else
 1101
 1102                {
 01103                    TempData["ErorrResult"] = $"Ошибка при удалении заказа.";
 1104                }
 01105            }
 1106
 01107            catch
 1108
 1109            {
 01110                TempData["ErorrResult"] = $"Ошибка при удалении заказа.";
 01111            }
 1112
 1113
 01114            return RedirectToAction(nameof(System.Index), new { sortOrder = GetSortStateCookie(), page = page });
 01115        }
 1116
 1117
 1118        public async Task<IActionResult> DeleteV2(int id, int page)
 1119
 1120        {
 1121            try
 1122
 1123            {
 01124                int result = await _orderServices.DeleteOrder(id);
 1125
 1126
 01127                if (result == 0)
 1128
 1129                {
 01130                    TempData["ErorrResult"] = $"Для удаления доступны только заказы со статусом '<b>Не указан</b>'.";
 1131                }
 1132
 01133                else if (result == 1)
 1134
 1135                {
 01136                    await _cache.RemoveOrderFromCache(id);
 1137
 01138                    ClearSelectedIdsSession();
 1139
 01140                    await NotificationService.NotifyAllAsync($"Заказ id={id} успешно удален.");
 1141
 01142                    TempData["TransactionResult"] = $"Заказ успешно удален.";
 1143                }
 1144
 1145                else
 1146
 1147                {
 01148                    TempData["ErorrResult"] = $"Ошибка при удалении заказа.";
 1149                }
 01150            }
 1151
 01152            catch
 1153
 1154            {
 01155                TempData["ErorrResult"] = $"Ошибка при удалении заказа.";
 01156            }
 1157
 1158
 01159            return RedirectToAction(nameof(Index), new { sortOrder = GetSortStateCookie(), page = page });
 01160        }
 1161
 1162        /// <summary>
 1163        /// Преобразует техническое сообщение об ошибке Ozon API в понятное для пользователя.
 1164        /// </summary>
 1165        private static string GetFriendlySplitErrorMessage(Exception ex)
 1166        {
 01167            var msg = ex.Message;
 01168            if (msg.Contains("invalid status", StringComparison.OrdinalIgnoreCase) ||
 01169                msg.Contains("expected status 300", StringComparison.OrdinalIgnoreCase))
 1170            {
 01171                return "Разделение на Ozon недоступно: отправление в неподходящем статусе. Разделять можно только заказы
 1172            }
 01173            if (msg.Contains("not all exemplars", StringComparison.OrdinalIgnoreCase))
 1174            {
 01175                return "Разделение на Ozon недоступно: отправление содержит другие товары или количество не совпадает с 
 1176            }
 01177            if (msg.Contains("not enough in parent posting", StringComparison.OrdinalIgnoreCase))
 1178            {
 01179                return "Разделение на Ozon недоступно: в отправлении недостаточно товара для указанного разделения. Коли
 1180            }
 01181            if (msg.Contains("Ozon API вернул пустые номера", StringComparison.OrdinalIgnoreCase))
 1182            {
 01183                return msg;
 1184            }
 01185            return "Ошибка при разделении заказа: " + msg;
 1186        }
 1187
 1188        /// <summary>
 1189        /// Выполняет разделение заказа с вызовом Ozon API для заказов Ozon-клиентов (quantity &gt; 1).
 1190        /// Перед разделением проверяет количество в Ozon через /v3/posting/fbs/get.
 1191        /// Если в Ozon достаточно — разделяет на Ozon. Если меньше — разделяет локально до уровня Ozon с пропорцией.
 1192        /// При onlyInSystem=true Ozon API не вызывается, разделение только в системе.
 1193        /// </summary>
 1194        /// <returns>Новый заказ, флаги разделения и сведения о частях для пайплайна этикеток.</returns>
 1195        private async Task<(Order newOrder, bool wasOzonSplit, bool wasOzonQuantityAdjusted, SplitOrderPartsResult parts
 1196        {
 01197            var order = await _context.Orders
 01198                .Include(o => o.ProductInfo)
 01199                .Include(o => o.OzonClient)
 01200                .FirstOrDefaultAsync(o => o.Id == orderId);
 1201
 01202            if (order == null)
 01203                throw new InvalidOperationException($"Заказ {orderId} не найден");
 1204
 01205            (string posting1, string posting2)? ozonPostingNumbers = null;
 01206            var wasOzonSplit = false;
 01207            var wasOzonQuantityAdjusted = false;
 01208            var actualPart1 = part1;
 01209            var actualPart2 = part2;
 01210            int? totalQuantityOverride = null;
 1211
 01212            if (!onlyInSystem && order.OzonClientId != null
 01213                && order.Quantity > 1
 01214                && !string.IsNullOrEmpty(order.ShipmentNumber))
 1215            {
 01216                var ozonClient = await _ozonClientServices.GetOzonClientAsync(order.OzonClientId.Value);
 01217                if (ozonClient != null
 01218                    && !string.IsNullOrWhiteSpace(ozonClient.DecryptClientId)
 01219                    && !string.IsNullOrWhiteSpace(ozonClient.DecryptApiKey))
 1220                {
 01221                    _ozonApiDataManager.SetClient(ozonClient.DecryptClientId, ozonClient.DecryptApiKey);
 1222
 01223                    var postingNumber = OzonPostingNumberHelper.GetOzonApiPostingNumber(order.ShipmentNumber);
 1224
 01225                    var productId = long.Parse(order.ProductInfo!.FbsOzonSkuId!);
 01226                    var ozonQuantity = await _ozonApiDataManager.GetPostingProductQuantityAsync(postingNumber, productId
 01227                    var ozonSku = await _ozonApiDataManager.GetPostingProductSkuAsync(postingNumber, productId)
 01228                        ?? throw new InvalidOperationException("Ozon API не вернул SKU товара для разделения отправления
 1229
 01230                    if (ozonQuantity > 0 && ozonQuantity < part1 + part2)
 1231                    {
 01232                        wasOzonQuantityAdjusted = true;
 01233                        if (ozonQuantity < 2)
 1234                        {
 01235                            actualPart1 = part1;
 01236                            actualPart2 = part2;
 1237                        }
 1238                        else
 1239                        {
 01240                            var total = part1 + part2;
 01241                            actualPart1 = (int)Math.Round(ozonQuantity * (double)part1 / total);
 01242                            if (actualPart1 < 1) actualPart1 = 1;
 01243                            actualPart2 = ozonQuantity - actualPart1;
 01244                            if (actualPart2 < 1)
 1245                            {
 01246                                actualPart2 = 1;
 01247                                actualPart1 = ozonQuantity - 1;
 1248                            }
 01249                            totalQuantityOverride = ozonQuantity;
 1250                        }
 1251                    }
 01252                    else if (ozonQuantity >= part1 + part2)
 1253                    {
 01254                        var postings = new List<FbsSplitPosting>
 01255                        {
 01256                            new FbsSplitPosting
 01257                            {
 01258                                Products = new List<FbsSplitProduct>
 01259                                {
 01260                                    new FbsSplitProduct { ProductId = ozonSku, Quantity = part1 }
 01261                                }
 01262                            },
 01263                            new FbsSplitPosting
 01264                            {
 01265                                Products = new List<FbsSplitProduct>
 01266                                {
 01267                                    new FbsSplitProduct { ProductId = ozonSku, Quantity = part2 }
 01268                                }
 01269                            }
 01270                        };
 1271
 01272                        var result = await _ozonApiDataManager.SplitFbsPosting(postingNumber, postings);
 01273                        var mapped = OzonSplitPostingMapper.Map(result, part1, part2);
 01274                        ozonPostingNumbers = mapped;
 01275                        wasOzonSplit = true;
 1276                    }
 01277                    else if (ozonQuantity == 0)
 1278                    {
 01279                        throw new InvalidOperationException("Товар не найден в отправлении Ozon или ошибка при получении
 1280                    }
 01281                }
 1282            }
 1283
 01284            var newOrder = await _orderServices.SplitOrder(orderId, actualPart1, actualPart2, ozonPostingNumbers, totalQ
 01285            if (newOrder == null)
 1286            {
 01287                throw new InvalidOperationException($"Не удалось разделить заказ {orderId}");
 1288            }
 1289
 01290            var originalAfterSplit = await _context.Orders.AsNoTracking()
 01291                .FirstOrDefaultAsync(o => o.Id == orderId);
 01292            if (originalAfterSplit == null)
 1293            {
 01294                throw new InvalidOperationException($"Заказ {orderId} не найден после разделения.");
 1295            }
 1296
 01297            var parts = new SplitOrderPartsResult
 01298            {
 01299                OriginalOrderId = orderId,
 01300                NewOrderId = newOrder.Id,
 01301                ShipmentNumbers = new[]
 01302                {
 01303                    originalAfterSplit.ShipmentNumber ?? string.Empty,
 01304                    newOrder.ShipmentNumber ?? string.Empty
 01305                },
 01306                IsOzonSplit = wasOzonSplit
 01307            };
 1308
 01309            return (newOrder, wasOzonSplit, wasOzonQuantityAdjusted, parts);
 01310        }
 1311
 1312        public async Task<IActionResult> SplitOrder(string ids, int page, string splitOption, bool onlyInSystem = false)
 1313
 1314        {
 1315            try
 1316            {
 01317                int[] idArray = ids.Split(',').Select(int.Parse).ToArray();
 1318
 01319                var parts = splitOption.Split(' ').Select(int.Parse).ToArray();
 1320
 01321                var anyOzonSplit = false;
 01322                var anyOzonQuantityAdjusted = false;
 01323                foreach (var id in idArray)
 1324
 1325                {
 01326                    var (_, wasOzonSplit, wasOzonQuantityAdjusted, _) =
 01327                        await PerformSplitOrderAsync(id, parts[0], parts[1], onlyInSystem);
 01328                    anyOzonSplit = anyOzonSplit || wasOzonSplit;
 01329                    anyOzonQuantityAdjusted = anyOzonQuantityAdjusted || wasOzonQuantityAdjusted;
 1330                }
 1331
 1332
 01333                await _cache.Update();
 1334
 01335                if (anyOzonSplit)
 1336                {
 01337                    TempData["TransactionResult"] = "Заказ успешно разделен на Ozon. Номера отправлений обновлены из отв
 1338                }
 01339                else if (anyOzonQuantityAdjusted)
 1340                {
 01341                    TempData["TransactionResult"] = "Заказ разделен локально: количество приведено к данным Ozon (в Ozon
 1342                }
 1343                else
 1344                {
 01345                    TempData["TransactionResult"] = "Заказ успешно разделен.";
 1346                }
 1347
 01348                return RedirectToAction(nameof(Index), new { sortOrder = GetSortStateCookie(), page = page });
 1349            }
 01350            catch (Exception ex)
 1351            {
 01352                TempData["ErorrResult"] = GetFriendlySplitErrorMessage(ex);
 01353                return RedirectToAction(nameof(Index), new { sortOrder = GetSortStateCookie(), page = page });
 1354            }
 01355        }
 1356
 1357
 1358        [HttpPost]
 1359
 1360
 1361        public async Task<IActionResult> MultiplayDeleteOrders(string ids, int page)
 1362
 1363        {
 01364            if (ids == null)
 1365
 1366            {
 01367                TempData["ErorrResult"] =
 01368                    $"Не удалось удалить выбранные заказы.<br>Было передано слишком большое количество записей.";
 1369
 01370                return RedirectToAction("Index");
 1371            }
 1372
 1373
 01374            int[] idArray = ids.Split(',').Select(int.Parse).ToArray();
 1375
 1376
 1377            try
 1378
 1379            {
 01380                int result = await _orderServices.MultiplayDeleteOrders(idArray);
 1381
 1382
 01383                if (result == 0)
 1384
 1385                {
 01386                    TempData["ErorrResult"] = $"Для удаления доступны только заказы со статусом '<b>Не указан</b>'.";
 1387                }
 1388
 01389                else if (result > 0)
 1390
 1391                {
 01392                    await _cache.RemoveOrdersFromCache(idArray);
 1393
 01394                    ClearSelectedIdsSession();
 1395
 01396                    await NotificationService.NotifyAllAsync($"Удалено <b>{result}</b> заказов.");
 1397
 01398                    TempData["TransactionResult"] = $"Удалено <b>{result}</b> заказов.";
 1399                }
 1400
 1401                else
 1402
 1403                {
 01404                    TempData["ErorrResult"] = $"Ошибка при удалении заказа.";
 1405                }
 01406            }
 1407
 01408            catch
 1409
 1410            {
 01411                TempData["ErorrResult"] = $"Ошибка при удалении заказа.";
 01412            }
 1413
 1414
 01415            return RedirectToAction(nameof(System.Index), new { sortOrder = GetSortStateCookie(), page = page });
 01416        }
 1417
 1418
 1419        public async Task<IActionResult> DeleteOrderPage(List<int> ordersIdsToDelete)
 1420
 1421        {
 01422            var ordersToDelete = ordersIdsToDelete != null && ordersIdsToDelete.Any()
 01423                ? await _orderServices.GetOrdersNoTracking(ordersIdsToDelete.ToArray())
 01424                : new List<Order>();
 1425
 1426
 01427            return View(new DeleteOrderViewModel()
 01428
 01429                { IdsToDelete = ordersIdsToDelete ?? [], OrdersToDelete = ordersToDelete });
 01430        }
 1431
 1432
 1433        public async Task<IActionResult> MultiplayDeleteOrdersV2(string ids, int page)
 1434
 1435        {
 01436            if (ids == null)
 1437
 1438            {
 01439                TempData["ErorrResult"] =
 01440                    $"Не удалось удалить выбранные заказы.<br>Было передано слишком большое количество записей.";
 1441
 01442                return RedirectToAction("Index");
 1443            }
 1444
 1445
 01446            int[] idArray = ids.Split(',').Select(int.Parse).ToArray();
 1447
 1448
 1449            try
 1450
 1451            {
 01452                int result = await _orderServices.MultiplayDeleteOrders(idArray);
 1453
 1454
 01455                if (result == 0)
 1456
 1457                {
 01458                    TempData["ErorrResult"] = $"Для удаления доступны только заказы со статусом '<b>Не указан</b>'.";
 1459                }
 1460
 01461                else if (result > 0)
 1462
 1463                {
 01464                    await _cache.RemoveOrdersFromCache(idArray);
 1465
 01466                    ClearSelectedIdsSession();
 1467
 01468                    await NotificationService.NotifyAllAsync($"Удалено <b>{result}</b> заказов.");
 1469
 01470                    TempData["TransactionResult"] = $"Удалено <b>{result}</b> заказов.";
 1471                }
 1472
 1473                else
 1474
 1475                {
 01476                    TempData["ErorrResult"] = $"Ошибка при удалении заказа.";
 1477                }
 01478            }
 1479
 01480            catch
 1481
 1482            {
 01483                TempData["ErorrResult"] = $"Ошибка при удалении заказа.";
 01484            }
 1485
 1486
 01487            return RedirectToAction(nameof(Index), new { sortOrder = GetSortStateCookie(), page = page });
 01488        }
 1489
 1490
 1491        public async Task<IActionResult> CancellationOrders(string ids, int page)
 1492
 1493        {
 01494            if (ids == null)
 1495
 1496            {
 01497                TempData["ErorrResult"] =
 01498                    $"Не удалось изменить выбранные заказы.<br>Было передано слишком большое количество записей.";
 1499
 01500                return RedirectToAction("Index");
 1501            }
 1502
 1503
 01504            int[] idArray = ids.Split(',').Select(int.Parse).ToArray();
 1505
 1506
 1507            try
 1508
 1509            {
 01510                int result = await _orderServices.CancellationOrders(idArray);
 1511
 1512
 01513                if (result == 0)
 1514
 1515                {
 01516                    TempData["ErorrResult"] = $"Для изменения доступны только заказы со статусом '<b>Не указан</b>'.";
 1517                }
 1518
 01519                else if (result > 0)
 1520
 1521                {
 01522                    await _cache.RemoveOrdersFromCache(idArray);
 1523
 01524                    ClearSelectedIdsSession();
 1525
 01526                    await NotificationService.NotifyAllAsync($"Изменено <b>{result}</b> заказов.");
 1527
 01528                    TempData["TransactionResult"] = $"Изменено <b>{result}</b> заказов.";
 1529                }
 1530
 1531                else
 1532
 1533                {
 01534                    TempData["ErorrResult"] = $"Ошибка при изменении заказа.";
 1535                }
 01536            }
 1537
 01538            catch
 1539
 1540            {
 01541                TempData["ErorrResult"] = $"Ошибка при изменении заказа.";
 01542            }
 1543
 1544
 01545            return RedirectToAction(nameof(Index), new { sortOrder = GetSortStateCookie(), page = page });
 01546        }
 1547
 1548
 1549        [HttpPost]
 1550        public async Task<IActionResult> Recover(int id, DateTime start, DateTime end)
 1551
 1552        {
 01553            await _cache.UpdateCacheIncrementally(id);
 1554
 01555            ClearSelectedIdsSession();
 1556
 01557            return RedirectToAction("Index");
 01558        }
 1559
 1560
 1561        public async Task<IActionResult> ConfirmAccepted(int id, string ids, int page)
 1562
 1563        {
 01564            if (ids == null || ids.Length == 0)
 1565
 1566            {
 01567                await _orderServices.ConfirmAccepted(id);
 1568
 01569                await _cache.UpdateCacheIncrementally(id);
 1570            }
 1571
 1572            else
 1573
 1574            {
 01575                int[] idArray = ids.Split(',').Select(int.Parse).ToArray();
 1576
 01577                foreach (var concretId in idArray)
 1578
 1579                {
 01580                    await _orderServices.ConfirmAccepted(concretId);
 1581
 01582                    await _cache.UpdateCacheIncrementally(concretId);
 1583                }
 1584
 1585
 01586                if (Request.Cookies["selectedIds"] != null)
 1587
 1588                {
 01589                    Response.Cookies.Delete("selectedIds");
 1590                }
 1591            }
 1592
 1593
 01594            return RedirectToAction(nameof(System.Index), new { sortOrder = GetSortStateCookie(), page = page });
 01595        }
 1596
 1597
 1598        public async Task<IActionResult> ConfirmAcceptedV2(int id, string ids, int page)
 1599
 1600        {
 01601            if (ids == null || ids.Length == 0)
 1602
 1603            {
 01604                await _orderServices.ConfirmAccepted(id);
 1605
 01606                await _cache.UpdateCacheIncrementally(id);
 1607            }
 1608
 1609            else
 1610
 1611            {
 01612                int[] idArray = ids.Split(',').Select(int.Parse).ToArray();
 1613
 01614                foreach (var concretId in idArray)
 1615
 1616                {
 01617                    await _orderServices.ConfirmAccepted(concretId);
 1618
 01619                    await _cache.UpdateCacheIncrementally(concretId);
 1620                }
 1621
 1622
 01623                if (Request.Cookies["selectedIds"] != null)
 1624
 1625                {
 01626                    Response.Cookies.Delete("selectedIds");
 1627                }
 1628            }
 1629
 1630
 01631            return RedirectToAction(nameof(Index), new { sortOrder = GetSortStateCookie(), page = page });
 01632        }
 1633
 1634
 1635        [HttpPost]
 1636        [ValidateAntiForgeryToken]
 1637        public async Task<IActionResult> SaveSelectedStockListToSession([FromBody] List<StockItem> stockItems)
 1638
 1639        {
 1640            try
 1641
 1642            {
 01643                var failedOrderIds = await _orderCartServices.CreateCartItem(stockItems);
 1644
 01645                await _cartCache.Update();
 1646
 01647                await NotificationService.NotifyAllAsync("Заказы добавлены в корзину.");
 1648
 01649                return Json(new { success = true, failedOrderIds = failedOrderIds });
 1650            }
 1651
 01652            catch (Exception e)
 1653
 1654            {
 01655                TempData["ErorrResult"] = "Ошибка при разборе списка  корзины.";
 1656
 01657                await NotificationService.NotifyAllAsync($"Ошибка при разборе списка корзины. {e.Message}");
 1658
 01659                return Json(new { success = false, message = "Ошибка на сервере" });
 1660            }
 01661        }
 1662
 1663
 1664        // Methods moved to OrderDetailsController:
 1665        // OrderDetailedInformation, OrderDetailedInformationForBitrix, OrderSubstitute, OrderStocks
 1666
 1667
 1668
 1669
 1670        [ServiceEndpoint]
 1671        [HttpPost]
 1672        public async Task<IActionResult> SplitOrders(string ids, string splitOption, bool onlyInSystem = false)
 1673
 1674        {
 1675            try
 1676
 1677            {
 01678                var splitConfig = OzonOrderSplitConfig.FromRequest(splitOption, onlyInSystem);
 01679                if (string.IsNullOrEmpty(splitConfig.SplitOption))
 1680
 1681                {
 01682                    return Json(new { success = false, message = "Не указан параметр разделения" });
 1683                }
 1684
 01685                int[] idArray = ids.Split(',').Select(int.Parse).ToArray();
 1686
 1687                int part1;
 1688                int part2;
 1689                try
 1690                {
 01691                    (part1, part2) = splitConfig.ParseParts();
 01692                }
 01693                catch (ArgumentException)
 1694                {
 01695                    return Json(new { success = false, message = "Неверный формат параметра разделения" });
 1696                }
 1697
 1698
 01699                var anyOzonSplit = false;
 01700                var anyOzonQuantityAdjusted = false;
 01701                var splitResults = new List<object>();
 01702                foreach (var id in idArray)
 1703
 1704                {
 01705                    var (_, wasOzonSplit, wasOzonQuantityAdjusted, splitParts) =
 01706                        await PerformSplitOrderAsync(id, part1, part2, splitConfig.OnlyInSystem);
 01707                    anyOzonSplit = anyOzonSplit || wasOzonSplit;
 01708                    anyOzonQuantityAdjusted = anyOzonQuantityAdjusted || wasOzonQuantityAdjusted;
 01709                    splitResults.Add(new
 01710                    {
 01711                        originalOrderId = splitParts.OriginalOrderId,
 01712                        orderIds = splitParts.OrderIds,
 01713                        shipmentNumbers = splitParts.ShipmentNumbers,
 01714                        isOzonSplit = splitParts.IsOzonSplit
 01715                    });
 1716                }
 1717
 1718
 01719                await _cache.Update();
 1720
 01721                var message = anyOzonSplit
 01722                    ? "Заказы успешно разделены на Ozon. Номера отправлений обновлены из ответа API."
 01723                    : anyOzonQuantityAdjusted
 01724                        ? "Заказы разделены локально: количество приведено к данным Ozon (в Ozon меньше, чем в системе).
 01725                        : $"Заказы успешно разделены ({part1}/{part2})";
 1726
 01727                return Json(new
 01728
 01729                {
 01730                    success = true,
 01731
 01732                    message = message,
 01733
 01734                    splitCount = idArray.Length,
 01735
 01736                    isOzonSplit = anyOzonSplit,
 01737
 01738                    splits = splitResults
 01739                });
 1740            }
 1741
 01742            catch (Exception ex)
 1743            {
 01744                return Json(new
 01745
 01746                {
 01747                    success = false,
 01748
 01749                    message = GetFriendlySplitErrorMessage(ex)
 01750                });
 1751            }
 01752        }
 1753
 1754
 1755        [Authorize(Roles = "Admin")]
 1756        public IActionResult DeleteDuplicateOrders()
 1757
 1758        {
 1759            int? duplicateCount;
 1760
 1761            int? deletedRowsCount;
 1762
 1763
 01764            (duplicateCount, deletedRowsCount) = _duplicateOrdersServices.DeleteDuplicateOrders();
 1765
 01766            ClearSelectedIdsSession();
 1767
 01768            return RedirectToAction(nameof(System.Index));
 1769        }
 1770
 1771
 1772        private async Task<bool> OrderExists(int id)
 1773
 1774        {
 01775            return await _orderServices.GetOrder(id) != null;
 01776        }
 1777
 1778
 1779        public void SetSortOrderViewData(OrderSortState sortOrder)
 1780        {
 01781            var sortData = _dataFilterManager.GetSortOrderViewData(sortOrder);
 01782            foreach (var item in sortData)
 1783            {
 01784                ViewData[item.Key] = item.Value;
 1785            }
 1786
 1787            // Передаем текущий sortOrder для отображения стрелки только на активном столбце
 01788            ViewData["CurrentSortOrder"] = sortOrder;
 01789        }
 1790
 1791
 1792        public IEnumerable<Order> ApplySortOrder(IEnumerable<Order> orders, OrderSortState sortOrder)
 1793        {
 01794            return _dataFilterManager.ApplySortOrder(orders, sortOrder);
 1795        }
 1796
 1797
 1798        private async Task<OrdersIndexQueryResult> LoadOrdersIndexPageAsync(
 1799            OrderFilterModel filterData,
 1800            int page,
 1801            int pageSize,
 1802            OrderSortState sortOrder,
 1803            AccessProfile? accessProfile,
 1804            bool hasFilters,
 1805            bool hasUserAccessRestrictions,
 1806            bool isReturnsListPage = false)
 1807        {
 01808            return await _ordersIndexQueryService.LoadPageAsync(new OrdersIndexQueryRequest
 01809            {
 01810                Filter = filterData,
 01811                Page = page,
 01812                PageSize = pageSize,
 01813                SortOrder = sortOrder,
 01814                AccessProfile = accessProfile,
 01815                HasFilters = hasFilters,
 01816                HasUserAccessRestrictions = hasUserAccessRestrictions,
 01817                IsReturnsListPage = isReturnsListPage,
 01818                UserId = User?.FindFirstValue(ClaimTypes.NameIdentifier),
 01819                CancellationToken = HttpContext.RequestAborted
 01820            });
 01821        }
 1822
 1823        private (List<Order> distinctOrders, List<Order> ordersForPage) ShapeOrdersAfterLoad(
 1824            List<Order> ordersFromCache,
 1825            bool requiresInMemorySort,
 1826            OrderSortState sortOrder,
 1827            OrderFilterModel filterData,
 1828            int pageSize)
 1829        {
 01830            if (requiresInMemorySort)
 1831            {
 01832                ordersFromCache = ApplySortOrder(ordersFromCache, sortOrder).ToList();
 1833            }
 1834
 01835            var distinctOrders = _ordersIndexQueryService.PrepareDistinctOrders(
 01836                ordersFromCache,
 01837                filterData.GroupByShipment);
 01838            distinctOrders = _ordersIndexQueryService.SelectOrdersForPage(
 01839                distinctOrders, 1, pageSize, out var ordersForPage);
 01840            return (distinctOrders, ordersForPage);
 1841        }
 1842
 1843
 1844        private async Task ApplyOrdersIndexPageViewDataAsync(
 1845            string userId,
 1846            bool isAdmin,
 1847            CancellationToken ct,
 1848            Services.Authorization.Dtos.UserPermissionsBundle? prefetchedBundle = null)
 1849        {
 01850            var data = await _ordersIndexPageViewDataService.BuildAsync(new OrdersIndexPageViewDataRequest
 01851            {
 01852                UserId = userId,
 01853                IsWebAdmin = isAdmin,
 01854                PrefetchedPermissionBundle = prefetchedBundle,
 01855                CancellationToken = ct
 01856            });
 01857            OrdersIndexPageViewDataApplicator.Apply(data, ViewBag);
 01858        }
 1859
 1860
 1861        private async Task ApplyFilterListsToViewAsync(
 1862            List<Order>? filteredData,
 1863            AccessProfile? accessProfile,
 1864            bool useFilteredData)
 1865        {
 01866            var userId = User?.FindFirstValue(ClaimTypes.NameIdentifier);
 01867            var scopeExpr = _userScopeProvider.GetFilterExpression("Order");
 1868
 01869            var data = await _orderFilterListsService.BuildAsync(
 01870                new OrderFilterListsBuildRequest
 01871                {
 01872                    FilteredData = filteredData,
 01873                    AccessProfile = accessProfile,
 01874                    UseFilteredData = useFilteredData,
 01875                    UserId = userId,
 01876                    OrderScopeExpression = scopeExpr
 01877                },
 01878                HttpContext.RequestAborted);
 1879
 01880            OrderFilterListsViewDataApplicator.Apply(data, ViewBag);
 01881        }
 1882
 1883
 1884        public async Task SetFilterLists(List<Order> filteredData, AccessProfile? accessProfile = null,
 1885            bool useFilteredData = false)
 1886        {
 01887            await ApplyFilterListsToViewAsync(
 01888                useFilteredData ? filteredData : null,
 01889                accessProfile,
 01890                useFilteredData);
 01891        }
 1892
 1893        private async Task<IQueryable<Order>> ApplyOrderDataScopeAsync(IQueryable<Order> ordersQuery)
 1894        {
 01895            var userId = User?.FindFirstValue(ClaimTypes.NameIdentifier);
 01896            return await _dataScopeApplier.ApplyAsync(ordersQuery, userId, HttpContext.RequestAborted);
 01897        }
 1898
 1899        private async Task<int[]> FilterOrderIdsByDataScopeAsync(IEnumerable<int> ids)
 1900        {
 01901            var userId = User?.FindFirstValue(ClaimTypes.NameIdentifier);
 01902            return await _dataScopeIdFilterService.FilterOrderIdsAsync(
 01903                userId, ids, HttpContext.RequestAborted);
 01904        }
 1905
 1906        private async Task<List<T>> FilterByOrderScopeIdsAsync<T>(
 1907            IEnumerable<T> source,
 1908            Func<T, int> idSelector,
 1909            string scopeFieldName)
 1910        {
 01911            var userId = User?.FindFirstValue(ClaimTypes.NameIdentifier);
 01912            return await _dataScopeIdFilterService.FilterItemsByScopeFieldAsync(
 01913                userId, source, idSelector, scopeFieldName, HttpContext.RequestAborted);
 01914        }
 1915
 1916        private static void EnsureOrderColumnsVisibilityProfile(CustomIdentityUser? user)
 1917        {
 01918            if (user == null)
 1919            {
 01920                return;
 1921            }
 1922
 01923            if (user.AccessProfileId != null && user.AccessProfile?.AvailableOrderColumns?.Any() == true)
 1924            {
 01925                return;
 1926            }
 1927
 01928            user.AccessProfileId ??= -1;
 01929            user.AccessProfile ??= new AccessProfile();
 01930            user.AccessProfile.AvailableOrderColumns = OrderListColumnCatalog.All
 01931                .Select(c => c.LegacyAccessKey)
 01932                .Where(k => !string.IsNullOrWhiteSpace(k))
 01933                .Distinct(StringComparer.OrdinalIgnoreCase)
 01934                .ToList()!;
 01935        }
 1936
 1937        private static void NormalizeSyntheticAccessProfile(CustomIdentityUser? user)
 1938        {
 01939            if (user?.AccessProfileId is <= 0)
 1940            {
 01941                user.AccessProfileId = null;
 1942            }
 01943        }
 1944
 1945        public void SaveSortStateCookie(OrderSortState sortOrder)
 1946
 1947        {
 01948            SaveSortStateCookie(sortOrder, "SortState");
 01949        }
 1950
 1951        public void SaveSortStateCookie(OrderSortState sortOrder, string cookieKey)
 1952
 1953        {
 01954            if (sortOrder != OrderSortState.StandardState)
 1955
 1956            {
 01957                Response.Cookies.Delete(cookieKey);
 1958
 01959                Response.Cookies.Append(cookieKey, sortOrder.ToString());
 1960            }
 01961        }
 1962
 1963
 1964        private OrderSortState GetSortStateCookie()
 1965
 1966        {
 01967            return GetSortStateCookie("SortState");
 1968        }
 1969
 1970        private OrderSortState GetSortStateCookie(string cookieKey)
 1971
 1972        {
 01973            var sortStateCookie = Request.Cookies[cookieKey];
 1974
 01975            if (!string.IsNullOrEmpty(sortStateCookie) &&
 01976                Enum.TryParse<OrderSortState>(sortStateCookie, out var savedSortState))
 1977
 1978            {
 01979                return savedSortState;
 1980            }
 1981
 1982
 01983            return OrderSortState.StandardState;
 1984        }
 1985
 1986
 1987        private void SetInfoMessage()
 1988
 1989        {
 01990            if (TempData.ContainsKey("UploadResult") && TempData["UploadResult"] != null)
 1991
 1992            {
 01993                int[] result = TempData["UploadResult"] as int[] ?? [];
 1994
 01995                string period = TempData["UploadResultPeriod"]?.ToString() ?? string.Empty;
 1996
 01997                ViewData["UploadResult"] = result;
 1998
 01999                ViewData["UploadResultPeriod"] = period;
 2000            }
 2001
 2002
 02003            if (TempData.ContainsKey("TransactionResult") && TempData["TransactionResult"] != null)
 2004
 2005            {
 02006                string result = TempData["TransactionResult"]?.ToString() ?? string.Empty;
 2007
 02008                ViewData["TransactionResult"] = result;
 2009            }
 2010
 2011
 02012            if (TempData.ContainsKey("ErorrResult") && TempData["ErorrResult"] != null)
 2013
 2014            {
 02015                string result = TempData["ErorrResult"]?.ToString() ?? string.Empty;
 2016
 02017                ViewData["ErorrResult"] = result;
 2018            }
 2019
 02020            if (TempData.ContainsKey("TransactionErrorCode") && TempData["TransactionErrorCode"] != null)
 2021            {
 02022                ViewData["TransactionErrorCode"] = TempData["TransactionErrorCode"]?.ToString();
 2023            }
 2024
 2025
 02026            if (TempData.ContainsKey("OrdersNotFoundInOzone") && TempData["OrdersNotFoundInOzone"] != null)
 2027
 2028            {
 02029                string result = TempData["OrdersNotFoundInOzone"]?.ToString() ?? string.Empty;
 2030
 02031                ViewData["OrdersNotFoundInOzone"] = result;
 2032            }
 2033
 2034
 02035            if (TempData.ContainsKey("SearchByBarcode") && TempData["SearchByBarcode"] is true)
 2036
 2037            {
 02038                ViewData["SearchByBarcode"] = true;
 2039
 02040                ViewData["BarcodeValue"] = TempData["BarcodeValue"]?.ToString();
 2041
 02042                ViewData["FoundOrdersCount"] = TempData["FoundOrdersCount"]?.ToString();
 2043            }
 02044        }
 2045
 2046
 2047        private List<int> RetrieveSelectedIdsFromSession()
 2048        {
 02049            var selectedIds = HttpContext.Session.GetString("selectedIds");
 02050            if (string.IsNullOrEmpty(selectedIds))
 02051                return new List<int>();
 02052            return JsonConvert.DeserializeObject<List<int>>(selectedIds) ?? new List<int>();
 2053        }
 2054
 2055        private void ClearSelectedIdsSession()
 2056        {
 02057            HttpContext.Session.SetString("selectedIds", JsonConvert.SerializeObject(new List<int>()));
 02058        }
 2059
 2060
 2061        /// <summary>
 2062        /// Обновляет кэш для указанной этикетки
 2063        /// </summary>
 2064        private void UpdateLabelCache(string shipmentNumber, bool exists) =>
 02065            _labelExistenceService.SetShipmentExists(shipmentNumber, exists);
 2066
 2067
 2068        [HttpGet]
 2069        // Methods moved to OrderLookupController:
 2070        // GetClientStatusesByClient, GetClientStatusesByClientType, GetCurrencyByCode,
 2071        // SearchEtProducers, GetEtProducerMarketPrefix, GetEtProducersByArticle,
 2072        // CheckOrderUnique, GetProductByArticle, SearchArticleByEAN
 2073
 2074        /// <summary>
 2075        /// Возвращает все заказы с указанным ProductKey для модального окна.
 2076        /// </summary>
 2077        [HttpGet]
 2078        public async Task<IActionResult> GetOrdersByProductKey(string key, bool includeArchived = false)
 2079        {
 02080            if (string.IsNullOrEmpty(key)) return Json(Array.Empty<object>());
 2081
 02082            var query = _orderServices.GetOrdersQueryable(includeArchived)
 02083                .Where(o => o.ProductKey == key);
 2084
 02085            query = await ApplyOrderDataScopeAsync(query);
 2086
 02087            var orders = await query
 02088                .Select(o => new
 02089                {
 02090                    o.Id,
 02091                    o.ShipmentNumber,
 02092                    ProcessingDate = o.ProcessingDate != null
 02093                        ? o.ProcessingDate.Value.ToString("dd.MM.yyyy")
 02094                        : "",
 02095                    Status = o.Status ?? "",
 02096                    o.Article,
 02097                    o.ProductName,
 02098                    Price = o.Price ?? 0m
 02099                })
 02100                .OrderByDescending(o => o.ProcessingDate)
 02101                .Take(500)
 02102                .ToListAsync();
 2103
 02104            return Json(orders);
 02105        }
 2106    }
 2107}

/home/runner/work/Studio2Prod/Studio2Prod/OzonOrdersWeb/Areas/Studio2/Controllers/OrdersController.ExcelExport.cs

#LineLine coverage
 1using System.IO.Compression;
 2using Microsoft.AspNetCore.Mvc;
 3using Microsoft.EntityFrameworkCore;
 4using OServices.FiltersServices.FilterModels;
 5using OzonDomains.Models;
 6using OzonDomains.Models.AccessControl;
 7using OzonOrdersWeb.WebServices.Orders.Grid;
 8using OzonRepositories.Context.Identity;
 9using Services.FiltersServices.DataFilterManagers;
 10
 11namespace OzonOrdersWeb.Controllers;
 12
 13public partial class OrdersController
 14{
 015    private static readonly IReadOnlyDictionary<string, string[]> ExcelColumnsByGridField =
 016        new Dictionary<string, string[]>(StringComparer.OrdinalIgnoreCase)
 017        {
 018            ["ShipmentNumber"] = ["Номер заказа"],
 019            ["OzonClient"] = ["Клиент"],
 020            ["ProcessingDate"] = ["Принят в обработку"],
 021            ["ShippingDate"] = ["Дата отгрузки"],
 022            ["DeliveryPeriod"] = ["Срок доставки"],
 023            ["ClientStatus"] = ["Статус клиента"],
 024            ["AppStatus"] = ["Статус"],
 025            ["ProductName"] = ["Наименование товара"],
 026            ["Article"] = ["Артикул", "ЧистыйАртикул"],
 027            ["Manufacturer"] = ["Производитель"],
 028            ["ShipmentWarehouse"] = ["Склад отгрузки"],
 029            ["Supplier"] = ["Поставщик"],
 030            ["SupplierOrderNumber"] = ["Номер заказа поставщику"],
 031            ["SitePrice"] = ["Цена сайта"],
 032            ["Price"] = ["Цена"],
 033            ["Quantity"] = ["Количество"],
 034            ["ShipmentTotal"] = ["Сумма отправления"],
 035            ["Category"] = ["Категория"],
 036            ["VolumeWeight"] = ["Объемный вес"],
 037            ["PurchasePrice"] = ["Цена закупки", "Оригинальная цена"],
 038            ["Cost"] = ["Себестоимость"],
 039            ["OzonCommission"] = ["Комиссия ОЗОН (мин.)", "Комиссия ОЗОН (макс.)"],
 040            ["Profit"] = ["Прибыль (мин.)", "Прибыль (макс.)"],
 041            ["MarkupPercent"] = ["Наценка % (мин.)", "Наценка % (макс.)"],
 042            ["DeliveryCity"] = ["Город доставки"]
 043        };
 44
 45    private async Task<List<string>?> ResolveExcelColumnsAsync(CustomIdentityUser? user, CancellationToken cancellationT
 46    {
 047        NormalizeSyntheticAccessProfile(user);
 048        var standardOrder = _excelExporter.ColumnMappings.Keys.ToList();
 049        if (user?.AccessProfileId is int profileId)
 50        {
 051            var profile = await _accessProfileDataServices.GetAccessProfileAsync(profileId);
 052            if (profile == null) return null;
 053            var profileColumns = profile.AvailableOrderColumns ?? [];
 054            return standardOrder.Where(profileColumns.Contains).ToList();
 55        }
 56
 057        if (user == null) return null;
 058        var readMap = await BuildOrderListFieldReadMapAsync(user.Id, cancellationToken);
 059        var allowedGridFields = OrdersGridColumnAccess.Compute(user, readMap);
 060        var allowedExcelColumns = allowedGridFields
 061            .Where(ExcelColumnsByGridField.ContainsKey)
 62            .SelectMany(field => ExcelColumnsByGridField[field])
 063            .ToHashSet(StringComparer.OrdinalIgnoreCase);
 064        return standardOrder.Where(allowedExcelColumns.Contains).ToList();
 065    }
 66
 67        public async Task<IActionResult> PrintOrdersExcel(string ids, int page)
 68
 69        {
 070            int[] idArray = ids.Split(',').Select(int.Parse).ToArray();
 71
 072            var ordersToExcel = await _orderServices.GetOrdersNoTracking(idArray);
 73
 74
 75            try
 76
 77            {
 078                var user = await _userCacheService.GetCachedUserAsync(User);
 079                if (user?.AccessProfileId == null)
 80                {
 081                    TempData["ErorrResult"] = $"Не удалось распечатать выделенные заказы<br/>";
 082                    return RedirectToAction(nameof(System.Index), new { sortOrder = GetSortStateCookie(), page = page })
 83                }
 84
 085                var userColumns = await _accessProfileDataServices.GetAccessProfileAsync(user.AccessProfileId.Value);
 086                if (userColumns == null)
 87                {
 088                    TempData["ErorrResult"] = $"Не удалось распечатать выделенные заказы<br/>";
 089                    return RedirectToAction(nameof(System.Index), new { sortOrder = GetSortStateCookie(), page = page })
 90                }
 91
 92
 093                byte[] excelFile = _excelExporter.ExportToExcel(ordersToExcel, userColumns.AvailableOrderColumns ?? []);
 94
 095                ClearSelectedIdsSession();
 96
 97
 098                return File(excelFile, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
 099                    $"Orders_{DateTime.Now:dd.MM.yyyy}.xlsx");
 100            }
 101
 0102            catch (Exception)
 103
 104            {
 0105                TempData["ErorrResult"] = $"Не удалось распечатать выделенные заказы<br/>";
 106
 0107                return RedirectToAction(nameof(System.Index), new { sortOrder = GetSortStateCookie(), page = page });
 108            }
 0109        }
 110
 111
 112        public async Task<IActionResult> PrintOrdersExcelV2(string ids, int page)
 113        {
 114            // Проверка на null или пустую строку
 0115            if (string.IsNullOrEmpty(ids) || string.IsNullOrWhiteSpace(ids))
 116            {
 0117                TempData["ErorrResult"] = "Не выбран ни один заказ для экспорта";
 0118                return RedirectToAction(nameof(System.Index), new { sortOrder = GetSortStateCookie(), page = page });
 119            }
 120
 121            try
 122            {
 123                // Разделение строки и преобразование в массив ID
 0124                int[] idArray = ids.Split(',', StringSplitOptions.RemoveEmptyEntries)
 125                    .Select(id => int.Parse(id.Trim()))
 126                    .Where(id => id > 0) // Дополнительная проверка на положительные ID
 0127                    .Distinct()
 0128                    .ToArray();
 129
 130                // Проверка, что после обработки есть ID
 0131                if (idArray.Length == 0)
 132                {
 0133                    TempData["ErorrResult"] = "Не выбран ни один заказ для экспорта";
 0134                    return RedirectToAction(nameof(System.Index),
 0135                        new { sortOrder = GetSortStateCookie(), page = page });
 136                }
 137
 0138                idArray = await FilterOrderIdsByDataScopeAsync(idArray);
 0139                if (idArray.Length == 0)
 140                {
 0141                    TempData["ErorrResult"] = "Нет доступа к выбранным заказам";
 0142                    return RedirectToAction(nameof(System.Index),
 0143                        new { sortOrder = GetSortStateCookie(), page = page });
 144                }
 145
 146                // Загружаем все заказы одним запросом, сразу с нужными навигационными свойствами
 147                // (включая поставщика и засылы), чтобы в Excel были корректные данные
 0148                var ordersToExcel = await _context.Orders
 0149                    .Where(o => idArray.Contains(o.Id))
 0150                    .Include(o => o.Supplier)
 0151                    .Include(o => o.OzonClient)
 0152                    .Include(o => o.ProductInfo)
 0153                    .Include(o => o.ShipmentWarehouse)
 0154                    .Include(o => o.AppStatus)
 0155                    .Include(o => o.OrderExtensions)
 0156                    .Include(o => o.EtProducer)
 0157                    .ToListAsync();
 158
 159                // Проверка, что удалось получить хотя бы один заказ
 0160                if (ordersToExcel.Count == 0)
 161                {
 0162                    TempData["ErorrResult"] = "Не удалось получить данные по выбранным заказам";
 0163                    return RedirectToAction(nameof(System.Index),
 0164                        new { sortOrder = GetSortStateCookie(), page = page });
 165                }
 166
 0167                var user = await _userCacheService.GetCachedUserAsync(User);
 0168                var orderedColumns = await ResolveExcelColumnsAsync(user, HttpContext.RequestAborted);
 0169                if (orderedColumns == null)
 170                {
 0171                    TempData["ErorrResult"] = "Не удалось получить профиль доступа пользователя";
 0172                    return RedirectToAction(nameof(System.Index),
 0173                        new { sortOrder = GetSortStateCookie(), page = page });
 174                }
 175
 0176                byte[] excelFile = _excelExporter.ExportToExcel(ordersToExcel, orderedColumns);
 0177                ClearSelectedIdsSession();
 178
 0179                return File(excelFile, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
 0180                    $"Orders_{DateTime.Now:dd.MM.yyyy}.xlsx");
 181            }
 0182            catch (FormatException)
 183            {
 0184                TempData["ErorrResult"] = "Некорректный формат идентификаторов заказов";
 0185                return RedirectToAction(nameof(System.Index), new { sortOrder = GetSortStateCookie(), page = page });
 186            }
 0187            catch (Exception)
 188            {
 0189                TempData["ErorrResult"] = $"Не удалось распечатать выделенные заказы<br/>";
 0190                return RedirectToAction(nameof(System.Index), new { sortOrder = GetSortStateCookie(), page = page });
 191            }
 0192        }
 193
 194
 195        /// <summary>
 196        /// Печать / экспорт для клиента по фильтрам, а не по выделенным заказам.
 197        /// Использует OrderFilterModel как на главной странице + кастомные фильтры:
 198        /// - множественный выбор статусов клиента;
 199        /// - множественный выбор статусов приложения.
 200        /// </summary>
 201        [HttpGet]
 202        public async Task<IActionResult> PrintOrdersExcelForClient(
 203            OrderFilterModel filterData,
 204            List<string>? selectedClients,
 205            List<string>? selectedStatuses,
 206            List<string>? selectedAppStatuses,
 207            List<string>? selectedShipmentWarehouses,
 208            bool includePayoutExplanation = false,
 209            decimal? payoutPercent = null,
 210            decimal? payoutLowThreshold = null,
 211            decimal? payoutMidThreshold = null,
 212            decimal? payoutMidFixed = null,
 213            decimal? payoutHighFixed = null)
 214        {
 0215            var user = await _userCacheService.GetCachedUserAsync(User);
 216
 0217            AccessProfile? accessProfile = null;
 218
 0219            if (user?.AccessProfileId != null)
 220            {
 0221                accessProfile = await _accessProfileDataServices.GetAccessProfileAsync(user.AccessProfileId.Value);
 222            }
 223
 224            // Для клиентского экспорта отключаем группировку по отправлениям,
 225            // чтобы каждая строка заказа попадала в Excel только один раз.
 0226            filterData.GroupByShipment = false;
 227
 228            // Базовый запрос: все заказы (с учётом архива)
 0229            IQueryable<Order> ordersQuery = _orderServices.GetOrdersQueryable(filterData.IncludeArchived);
 0230            ordersQuery = _dataFilterManager.FilterByFilterDataQueryable(ordersQuery, filterData, accessProfile);
 231
 232            // Дополнительная фильтрация по периодам дат, специфичная для клиентского экспорта.
 233            // Используем ShippingDateFrom/To и LastStatusChangeDateFrom/To, чтобы не ломать общую логику.
 234
 0235            if (filterData.ShippingDateFrom.HasValue)
 236            {
 0237                var from = filterData.ShippingDateFrom.Value.Date;
 0238                ordersQuery = ordersQuery.Where(o => o.ShippingDate >= from);
 239            }
 240
 0241            if (filterData.ShippingDateTo.HasValue)
 242            {
 0243                var to = filterData.ShippingDateTo.Value.Date.AddDays(1).AddTicks(-1);
 0244                ordersQuery = ordersQuery.Where(o => o.ShippingDate <= to);
 245            }
 246
 0247            if (filterData.LastStatusChangeDateFrom.HasValue)
 248            {
 0249                var from = filterData.LastStatusChangeDateFrom.Value.Date;
 0250                ordersQuery = ordersQuery.Where(o => o.LastStatusChangeDate >= from);
 251            }
 252
 0253            if (filterData.LastStatusChangeDateTo.HasValue)
 254            {
 0255                var to = filterData.LastStatusChangeDateTo.Value.Date.AddDays(1).AddTicks(-1);
 0256                ordersQuery = ordersQuery.Where(o => o.LastStatusChangeDate <= to);
 257            }
 258
 259            // Дополнительная кастомная фильтрация:
 260            // 1) множественный выбор клиентов (по имени)
 0261            if (selectedClients != null && selectedClients.Any())
 262            {
 0263                ordersQuery = ordersQuery.Where(o =>
 0264                    o.OzonClient != null &&
 0265                    selectedClients.Contains(o.OzonClient.Name));
 266            }
 267
 268            // 2) множественный выбор статусов клиента
 0269            if (selectedStatuses != null && selectedStatuses.Any())
 270            {
 0271                ordersQuery = ordersQuery.Where(o =>
 0272                    o.ClientStatus != null &&
 0273                    selectedStatuses.Contains(o.ClientStatus!.Name!));
 274            }
 275
 276            // 3) множественный выбор статусов приложения
 0277            if (selectedAppStatuses != null && selectedAppStatuses.Any())
 278            {
 0279                ordersQuery = ordersQuery.Where(o =>
 0280                    o.AppStatus != null &&
 0281                    selectedAppStatuses.Contains(o.AppStatus!.Name!));
 282            }
 283
 284            // 4) множественный выбор складов отгрузки
 0285            if (selectedShipmentWarehouses != null)
 286            {
 0287                selectedShipmentWarehouses = selectedShipmentWarehouses
 288                    .Where(x => !string.IsNullOrWhiteSpace(x))
 0289                    .Distinct()
 0290                    .ToList();
 291            }
 292
 0293            if (selectedShipmentWarehouses != null && selectedShipmentWarehouses.Any())
 294            {
 0295                ordersQuery = ordersQuery.Where(o =>
 0296                    o.ShipmentWarehouse != null &&
 0297                    selectedShipmentWarehouses.Contains(o.ShipmentWarehouse!.Name!));
 298            }
 299
 300            // Загружаем данные с нужными навигациями для корректного экспорта
 0301            var orders = await ordersQuery
 0302                .Include(o => o.Supplier)
 0303                .Include(o => o.OzonClient)
 0304                .Include(o => o.ProductInfo)
 0305                .Include(o => o.ShipmentWarehouse)
 0306                .Include(o => o.AppStatus)
 0307                .Include(o => o.OrderExtensions)
 0308                .ToListAsync();
 309
 310            // Гарантируем уникальность заказов по Id (на случай дубликатов после фильтрации)
 0311            orders = orders
 312                .GroupBy(o => o.Id)
 313                .Select(g => g.First())
 0314                .ToList();
 315
 0316            if (!orders.Any())
 317            {
 0318                TempData["ErorrResult"] = "По заданным фильтрам не найдено ни одного заказа для печати.";
 0319                return RedirectToAction(nameof(Index));
 320            }
 321
 322            // Набор колонок для клиента: до "Количество" и отдельная колонка выплаты.
 0323            var clientColumns = new List<string>
 0324            {
 0325                "Номер заказа",
 0326                "Принят в обработку",
 0327                "Клиент",
 0328                "Город доставки",
 0329                "Статус клиента",
 0330                "Статус",
 0331                "Производитель",
 0332                "Склад отгрузки",
 0333                "Поставщик",
 0334                "Цена",
 0335                "Количество",
 0336                "Выплата клиенту"
 0337            };
 338
 0339            if (includePayoutExplanation)
 340            {
 0341                clientColumns.Add("Комментарий выплаты клиенту");
 342            }
 343
 344            // Применяем пользовательские правила выплаты, если заданы
 0345            if (payoutPercent.HasValue) _excelExporter.PayoutPercent = payoutPercent.Value / 100m;
 0346            if (payoutLowThreshold.HasValue) _excelExporter.PayoutLowThreshold = payoutLowThreshold.Value;
 0347            if (payoutMidThreshold.HasValue) _excelExporter.PayoutMidThreshold = payoutMidThreshold.Value;
 0348            if (payoutMidFixed.HasValue) _excelExporter.PayoutMidFixed = payoutMidFixed.Value;
 0349            if (payoutHighFixed.HasValue) _excelExporter.PayoutHighFixed = payoutHighFixed.Value;
 350
 351            // Фильтруем по тем колонкам, которые реально поддерживаются маппингом экспортёра
 0352            var supportedColumns = _excelExporter.ColumnMappings.Keys.ToHashSet();
 0353            clientColumns = clientColumns.Where(c => supportedColumns.Contains(c)).ToList();
 354
 355            // Если выбрано несколько клиентов — формируем отдельный Excel-файл для каждого и упаковываем в ZIP
 0356            if (selectedClients != null && selectedClients.Count > 1)
 357            {
 0358                using var zipStream = new MemoryStream();
 0359                using (var archive = new ZipArchive(zipStream, ZipArchiveMode.Create, true))
 360                {
 0361                    foreach (var clientName in selectedClients.Distinct())
 362                    {
 0363                        var clientOrders = orders
 0364                            .Where(o => o.OzonClient != null && o.OzonClient.Name == clientName)
 0365                            .ToList();
 366
 0367                        if (!clientOrders.Any())
 368                            continue;
 369
 0370                        var entryName =
 0371                            $"Orders_ForClient_{SanitizeFileName(clientName)}_{DateTime.Now:dd.MM.yyyy}.xlsx";
 0372                        var entry = archive.CreateEntry(entryName, CompressionLevel.Optimal);
 0373                        using var entryStream = entry.Open();
 0374                        var bytes = _excelExporter.ExportToExcel(clientOrders, clientColumns);
 0375                        entryStream.Write(bytes, 0, bytes.Length);
 376                    }
 0377                }
 378
 0379                var zipBytes = zipStream.ToArray();
 0380                var zipFileName = $"Orders_ForClients_{DateTime.Now:dd.MM.yyyy}.zip";
 0381                return File(zipBytes, "application/zip", zipFileName);
 382            }
 383            else
 384            {
 385                // Один клиент или все клиенты в одном файле
 0386                byte[] excelFile = _excelExporter.ExportToExcel(orders, clientColumns);
 387
 0388                return File(excelFile,
 0389                    "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
 0390                    $"Orders_ForClient_{DateTime.Now:dd.MM.yyyy}.xlsx");
 391            }
 0392        }
 393}

/home/runner/work/Studio2Prod/Studio2Prod/OzonOrdersWeb/Areas/Studio2/Controllers/OrdersController.HandoverImport.cs

#LineLine coverage
 1using System.Text.Json;
 2using Microsoft.AspNetCore.Mvc;
 3using Services.OzonHandoverServices;
 4
 5namespace OzonOrdersWeb.Controllers;
 6
 7public partial class OrdersController
 8{
 9    /// <summary>
 10    /// Ручная загрузка JSON-документа выдачи/приёма-передачи Ozon (формат "Telegram Lite")
 11    /// со страницы /Studio2/Orders/Upload — тот же путь обработки, что и у машинного
 12    /// POST /api/ozon-handover/import, но через веб-форму с файлом и AJAX-результатом
 13    /// вместо X-Api-Key + редиректа.
 14    /// </summary>
 15    [HttpPost]
 16    public async Task<IActionResult> UploadHandoverJson(IFormFile file, CancellationToken ct)
 17    {
 018        if (file == null || file.Length == 0)
 019            return BadRequest(new { error = "Файл не выбран." });
 20
 21        OzonHandoverImportRequest? request;
 22        try
 23        {
 024            await using var stream = file.OpenReadStream();
 025            request = await JsonSerializer.DeserializeAsync<OzonHandoverImportRequest>(stream, cancellationToken: ct);
 026        }
 027        catch (JsonException ex)
 28        {
 029            return BadRequest(new { error = $"Файл не похож на документ выдачи Ozon: {ex.Message}" });
 30        }
 31
 032        if (request == null)
 033            return BadRequest(new { error = "Пустой JSON." });
 34
 035        if (!TryValidateModel(request))
 36        {
 037            var errors = ModelState
 38                .Where(kv => kv.Value?.Errors.Count > 0)
 39                .SelectMany(kv => kv.Value!.Errors.Select(e => e.ErrorMessage))
 040                .ToList();
 041            return BadRequest(new { error = "Документ не соответствует формату.", details = errors });
 42        }
 43
 044        var result = await _handoverImportService.ImportAsync(request, ct);
 045        return Ok(result);
 046    }
 47}

/home/runner/work/Studio2Prod/Studio2Prod/OzonOrdersWeb/Areas/Studio2/Controllers/OrdersController.IndexAjax.cs

#LineLine coverage
 1using Microsoft.AspNetCore.Mvc;
 2using Microsoft.EntityFrameworkCore;
 3using Microsoft.Extensions.Caching.Memory;
 4using OServices.FiltersServices.FilterModels;
 5using OzonDomains.Models;
 6using OzonDomains.Models.AccessControl;
 7using OzonDomains.Models.Authorization.Enums;
 8using OzonOrdersWeb.Areas.Studio2.ViewHelpers;
 9using OzonOrdersWeb.Authorization;
 10using OzonOrdersWeb.WebServices.Orders;
 11using OzonOrdersWeb.ViewModels.OrderViewModels;
 12using Services.FiltersServices.DataFilterManagers;
 13using Services.FiltersServices.SortModels;
 14
 15namespace OzonOrdersWeb.Controllers;
 16
 17public partial class OrdersController
 18{
 19    /// <summary>
 20    /// AJAX: только tbody таблицы + пагинация. Справочники фильтров не перезагружаются (остаются с первого GET).
 21    /// </summary>
 22    [HttpPost]
 23    [ValidateAntiForgeryToken]
 24    // Фильтрация — часть просмотра списка, а не операция изменения. Иначе auto-авторизация
 25    // требует CanEdit для POST и пользователи с правом просмотра получают 403.
 26    [PageAuthorize("Studio2.Orders.Index", AccessType.View)]
 27    public async Task<IActionResult> IndexTableAjax(
 28        OrderFilterModel filterData,
 29        int page = 1,
 30        OrderSortState sortOrder = OrderSortState.StandardState,
 31        string clearSort = "",
 32        int? size = null)
 33    {
 034        filterData ??= new OrderFilterModel();
 35
 036        if (!string.IsNullOrEmpty(clearSort) && clearSort.Equals("true", StringComparison.OrdinalIgnoreCase))
 37        {
 038            Response.Cookies.Delete("SortState");
 039            Response.Cookies.Append("SortState", OrderSortState.StandardState.ToString());
 040            sortOrder = OrderSortState.StandardState;
 41        }
 042        else if (sortOrder != OrderSortState.StandardState)
 43        {
 044            SaveSortStateCookie(sortOrder);
 45        }
 46        else
 47        {
 048            sortOrder = GetSortStateCookie();
 49        }
 50
 051        int? effectiveSize = null;
 052        if (size is > 0)
 53        {
 054            effectiveSize = Math.Clamp(size.Value, 1, MaxOrdersPageSize);
 055            Response.Cookies.Append("PageSize", effectiveSize.Value.ToString());
 056            page = 1;
 57        }
 58
 059        var buildResult = await BuildOrdersIndexTableAjaxViewModelAsync(filterData, page, sortOrder, effectiveSize);
 060        if (buildResult.LoginRedirect)
 61        {
 062            return Unauthorized();
 63        }
 64
 065        ViewData["UseOrdersIndexAjax"] = true;
 066        return PartialView("~/Areas/Studio2/Views/Partials/Orders/_IndexAjaxRegion.cshtml", buildResult.ViewModel);
 067    }
 68
 69    /// <summary>Данные календаря для фильтров дат на главной таблице.</summary>
 70    [HttpPost]
 71    [ValidateAntiForgeryToken]
 72    [PageAuthorize("Studio2.Orders.Index", AccessType.View)]
 73    public async Task<IActionResult> IndexDateCalendarAjax(
 74        OrderFilterModel filterData,
 75        string dateField,
 76        string? month,
 77        CancellationToken cancellationToken = default)
 78    {
 079        if (dateField is not ("ProcessingDate" or "ShippingDate" or "LastStatusChangeDate"))
 080            return BadRequest();
 81
 082        filterData ??= new OrderFilterModel();
 083        var selectedDate = dateField switch
 084        {
 085            "ProcessingDate" => filterData.ProcessingDate,
 086            "ShippingDate" => filterData.ShippingDate,
 087            _ => filterData.LastStatusChangeDate
 088        };
 089        var calendarMonth = DateTime.TryParseExact(month, "yyyy-MM", System.Globalization.CultureInfo.InvariantCulture,
 090            System.Globalization.DateTimeStyles.None, out var parsedMonth)
 091            ? parsedMonth.Date
 092            : selectedDate?.Date ?? DateTime.Today;
 093        calendarMonth = new DateTime(calendarMonth.Year, calendarMonth.Month, 1);
 094        var nextMonth = calendarMonth.AddMonths(1);
 95
 96        // Выбранная дата не должна скрывать другие доступные дни календаря.
 097        if (dateField == "ProcessingDate")
 098            filterData.ProcessingDate = null;
 099        else if (dateField == "ShippingDate")
 0100            filterData.ShippingDate = null;
 101        else
 0102            filterData.LastStatusChangeDate = null;
 103
 0104        var user = await _userCacheService.GetCachedUserAsync(User);
 0105        NormalizeSyntheticAccessProfile(user);
 0106        AccessProfile? accessProfile = null;
 0107        if (user?.AccessProfileId is int accessProfileId)
 0108            accessProfile = await _accessProfileDataServices.GetAccessProfileAsync(accessProfileId);
 109
 0110        var userId = User.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier)?.Value ?? "anonymous";
 0111        var cacheKey = $"orders-date-calendar:{userId}:{dateField}:{calendarMonth:yyyy-MM}:{Newtonsoft.Json.JsonConvert.
 0112        var days = await _memoryCache.GetOrCreateAsync<List<OrdersDateCalendarDay>>(cacheKey, async entry =>
 0113        {
 0114            entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromSeconds(30);
 0115            var ordersQuery = _orderServices.GetOrdersQueryable(filterData.IncludeArchived);
 0116            ordersQuery = _dataFilterManager.FilterByFilterDataQueryable(ordersQuery, filterData, accessProfile);
 0117            ordersQuery = await _dataFilterManager.NarrowByElasticsearchAsync(ordersQuery, filterData);
 0118            ordersQuery = await _dataScopeApplier.ApplyAsync(ordersQuery, userId, cancellationToken);
 0119
 0120            return dateField switch
 0121            {
 0122                "ProcessingDate" => await ordersQuery
 0123                    .Where(x => x.ProcessingDate >= calendarMonth && x.ProcessingDate < nextMonth)
 0124                    .GroupBy(x => x.ProcessingDate!.Value.Date)
 0125                    .Select(g => new OrdersDateCalendarDay { Date = g.Key, Count = g.Count() })
 0126                    .ToListAsync(cancellationToken),
 0127                "ShippingDate" => await ordersQuery
 0128                    .Where(x => x.ShippingDate >= calendarMonth && x.ShippingDate < nextMonth)
 0129                    .GroupBy(x => x.ShippingDate!.Value.Date)
 0130                    .Select(g => new OrdersDateCalendarDay { Date = g.Key, Count = g.Count() })
 0131                    .ToListAsync(cancellationToken),
 0132                _ => await ordersQuery
 0133                    .Where(x => x.LastStatusChangeDate >= calendarMonth && x.LastStatusChangeDate < nextMonth)
 0134                    .GroupBy(x => x.LastStatusChangeDate!.Value.Date)
 0135                    .Select(g => new OrdersDateCalendarDay { Date = g.Key, Count = g.Count() })
 0136                    .ToListAsync(cancellationToken)
 0137            };
 0138        }) ?? [];
 139
 0140        return Ok(new
 0141        {
 0142            month = calendarMonth.ToString("yyyy-MM"),
 143            days = days.Select(x => new { date = x.Date.ToString("yyyy-MM-dd"), count = x.Count })
 0144        });
 0145    }
 146
 147    private sealed class OrdersDateCalendarDay
 148    {
 0149        public DateTime Date { get; init; }
 0150        public int Count { get; init; }
 151    }
 152
 153    private sealed class OrdersIndexTableAjaxBuildResult
 154    {
 0155        public OrderPageViewModel<Order, OrderFilterModel> ViewModel { get; init; } = null!;
 0156        public bool LoginRedirect { get; init; }
 157    }
 158
 159    private async Task<OrdersIndexTableAjaxBuildResult> BuildOrdersIndexTableAjaxViewModelAsync(
 160        OrderFilterModel filterData,
 161        int page,
 162        OrderSortState sortOrder,
 163        int? pageSizeOverride = null)
 164    {
 0165        var user = await _userCacheService.GetCachedUserAsync(User);
 0166        NormalizeSyntheticAccessProfile(user);
 167
 0168        AccessProfile? accessProfile = null;
 0169        if (user?.AccessProfileId != null)
 170        {
 0171            accessProfile = await _accessProfileDataServices.GetAccessProfileAsync(user.AccessProfileId.Value);
 172        }
 173
 0174        var isAdmin = User.IsInRole("Admin");
 175
 0176        bool hasFilters = !_dataFilterManager.AreAllPropertiesNull(filterData);
 0177        bool hasUserAccessRestrictions = (accessProfile != null && accessProfile.ScopeItems.Any())
 0178                                         || !string.IsNullOrEmpty(_userScopeProvider.GetFilterExpression("Order"));
 179
 0180        int pageSize = ResolveOrdersPageSize(pageSizeOverride);
 181        const bool disablePagination = false;
 182
 0183        if (page < 1)
 184        {
 0185            page = 1;
 186        }
 187
 0188        var indexQueryResult = await LoadOrdersIndexPageAsync(
 0189            filterData, page, pageSize, sortOrder, accessProfile, hasFilters, hasUserAccessRestrictions);
 0190        var totalOrdersCount = indexQueryResult.TotalCount;
 191
 0192        HttpContext.Response.Cookies.Append("FilterData", Newtonsoft.Json.JsonConvert.SerializeObject(filterData));
 193
 0194        SetSortOrderViewData(sortOrder);
 0195        ViewData["SortAjaxLinks"] = OrderIndexSortAjaxMeta.Build(
 0196            sortOrder,
 0197            _dataFilterManager.GetSortOrderViewData(sortOrder));
 198
 0199        var (distinctOrders, ordersForPage) = ShapeOrdersAfterLoad(
 0200            indexQueryResult.Orders,
 0201            indexQueryResult.RequiresInMemorySort,
 0202            sortOrder,
 0203            filterData,
 0204            pageSize);
 205
 0206        await _ordersIndexPageDetailsService.LoadOrderDetailsForPageAsync(
 0207            ordersForPage, accessProfile, HttpContext.RequestAborted);
 208
 209        const int viewModelPage = 1;
 0210        var pageViewModel = new OrderPageViewModel<Order, OrderFilterModel>(
 0211            distinctOrders,
 0212            viewModelPage,
 0213            pageSize,
 0214            filterData,
 0215            returnableCount: 0);
 216
 0217        _ordersIndexPageDetailsService.ApplyDbPagination(
 0218            pageViewModel, totalOrdersCount, page, pageSize, disablePagination);
 219
 0220        await _ordersIndexPageDetailsService.PopulateUniqueCountsAsync(
 0221            pageViewModel, ordersForPage, filterData.IncludeArchived, HttpContext.RequestAborted);
 222
 0223        if (user == null)
 224        {
 0225            return new OrdersIndexTableAjaxBuildResult { LoginRedirect = true };
 226        }
 227
 0228        pageViewModel.User = user;
 229
 0230        Services.Authorization.Dtos.UserPermissionsBundle? permissionBundle = null;
 0231        if (!isAdmin)
 232        {
 0233            permissionBundle = await _permissionService.GetUserPermissionsAsync(
 0234                user.Id, HttpContext.RequestAborted);
 235        }
 236
 0237        await _ordersIndexViewModelEnricher.EnrichAsync(new OrdersIndexViewModelEnrichRequest
 0238        {
 0239            ViewModel = pageViewModel,
 0240            User = user,
 0241            AccessProfile = accessProfile,
 0242            OrdersForPage = ordersForPage,
 0243            IsAdmin = isAdmin,
 0244            PrefetchedPermissionBundle = permissionBundle,
 0245            CancellationToken = HttpContext.RequestAborted
 0246        });
 247
 0248        ViewData["LabelExistsByShipmentNumber"] = pageViewModel.LabelExistsByShipmentNumber;
 0249        ViewBag.GroupByShipment = filterData.GroupByShipment;
 250
 0251        return new OrdersIndexTableAjaxBuildResult { ViewModel = pageViewModel };
 0252    }
 253}

/home/runner/work/Studio2Prod/Studio2Prod/OzonOrdersWeb/Areas/Studio2/Controllers/OrdersController.OzonLabels.cs

#LineLine coverage
 1using System.IO.Compression;
 2using System.Security.Claims;
 3using iText.Kernel.Pdf;
 4using Microsoft.AspNetCore.Mvc;
 5using Newtonsoft.Json;
 6using OzonDomains;
 7using OzonDomains.Models;
 8using OzonOrdersWeb.Authorization;
 9using Services.ApiServices.DropBoxApi;
 10using Services.ApiServices.OzonApi;
 11using Services.ApiServices.YandexApi;
 12using Services.LabelServices;
 13using Services.ParserServices.HelpDictEnum;
 14using Services.SignalRServices;
 15
 16namespace OzonOrdersWeb.Controllers;
 17
 18public partial class OrdersController
 19{
 20        public async Task<IActionResult> PrintOrdersLable(string ids, int page)
 21
 22        {
 023            List<OzonClient> clients = await _ozonClientServices.GetOzonClients();
 24
 025            int[] idArray = ids.Split(',').Select(int.Parse).ToArray();
 26
 027            var ordersLables = await _orderServices.GetOrdersNoTracking(idArray);
 28
 029            Dictionary<string, byte[]> lables = new Dictionary<string, byte[]>();
 30
 031            int continueLablesCuont = 0;
 32
 033            var failedShipments = new List<string>();
 34
 35
 36            // Экран использует устаревший прямой путь без нового JS-пайплайна. Сгруппируем
 37            // заказы по клиенту: экземпляр OzonJsonDataBuilder переключается только между
 38            // группами, а запросы этикеток внутри одной группы независимы и идут параллельно.
 39            const int labelParallelism = 8;
 040            var resultLock = new object();
 41
 42            foreach (var clientGroup in ordersLables.GroupBy(order => order.OzonClient?.ClientId))
 43            {
 044                var client = clients.FirstOrDefault(c => c.ClientId == clientGroup.Key);
 045                if (client == null || string.IsNullOrEmpty(client.DecryptClientId) ||
 046                    string.IsNullOrEmpty(client.DecryptApiKey))
 47                {
 048                    lock (resultLock)
 49                    {
 050                        foreach (var order in clientGroup)
 51                        {
 052                            continueLablesCuont++;
 053                            failedShipments.Add($"{order.ShipmentNumber}: Ozon клиент не найден");
 54                        }
 055                    }
 56
 57                    continue;
 58                }
 59
 060                _jsonDataBuilder.SetClient(client.DecryptClientId, client.DecryptApiKey);
 061                using var semaphore = new SemaphoreSlim(labelParallelism);
 62
 063                await Task.WhenAll(clientGroup.Select(async order =>
 064                {
 065                    await semaphore.WaitAsync();
 066                    try
 067                    {
 068                        var isRussianPost = order.Delivery?.Provider?.Name?.Contains(
 069                            "Почта России", StringComparison.OrdinalIgnoreCase) == true;
 070                        var maxAttempts = isRussianPost ? 5 : 3;
 071                        var delayMs = isRussianPost ? 1500 : 500;
 072
 073                        byte[]? labelBytes = null;
 074                        Exception? lastError = null;
 075
 076                        for (var attempt = 1; attempt <= maxAttempts; attempt++)
 077                        {
 078                            try
 079                            {
 080                                labelBytes = await _jsonDataBuilder.GetOrderLabel(order.ShipmentNumber!);
 081                                if (labelBytes is { Length: > 0 })
 082                                    break;
 083                            }
 084                            catch (Exception ex)
 085                            {
 086                                lastError = ex;
 087                            }
 088
 089                            if (attempt < maxAttempts)
 090                                await Task.Delay(delayMs);
 091                        }
 092
 093                        lock (resultLock)
 094                        {
 095                            if (labelBytes is { Length: > 0 })
 096                            {
 097                                lables[order.ShipmentNumber!] = labelBytes;
 098                            }
 099                            else
 0100                            {
 0101                                continueLablesCuont++;
 0102                                var reason = lastError?.Message ?? "этикетка не сформирована (пустой ответ)";
 0103                                failedShipments.Add($"{order.ShipmentNumber}: {reason}");
 0104                            }
 0105                        }
 0106                    }
 0107                    catch (Exception ex)
 0108                    {
 0109                        lock (resultLock)
 0110                        {
 0111                            continueLablesCuont++;
 0112                            failedShipments.Add($"{order.ShipmentNumber}: {ex.Message}");
 0113                        }
 0114                    }
 0115                    finally
 0116                    {
 0117                        semaphore.Release();
 0118                    }
 0119                }));
 0120            }
 121
 122
 0123            var result = SendFilesToUser(lables);
 124
 0125            if (result == null)
 126
 127            {
 0128                if (Request.Cookies["selectedIds"] != null)
 129
 130                {
 0131                    Response.Cookies.Delete("selectedIds");
 132                }
 133
 134
 0135                TempData["ErorrResult"] =
 136                    $"Не удалось распечатать этикетку для заказов: {string.Join(", ", ordersLables.Select(o => o.Shipmen
 0137                    $"Печать этикетки возможна только для заказов со статусом <b>'Ожидает отгрузки'</b>";
 138
 139
 0140                var errorMsg =
 0141                    $"Не удалось распечатать этикетки. Печать возможна только для заказов со статусом 'Ожидает отгрузки'
 142
 0143                if (failedShipments.Any())
 144
 145                {
 0146                    errorMsg += $" Ошибки по заказам: {string.Join("; ", failedShipments.Take(5))}" +
 0147                                (failedShipments.Count > 5 ? " ..." : "");
 148                }
 149
 150
 0151                await NotificationService.NotifyAllAsync(errorMsg);
 152
 0153                return RedirectToAction(nameof(System.Index), new { sortOrder = GetSortStateCookie(), page = page });
 154            }
 155
 156
 0157            var successCount = lables.Count;
 158
 0159            if (successCount > 0)
 160
 161            {
 0162                var msg = $"<div class='alert alert-success'>" +
 0163                          $"<strong>Этикетки сформированы</strong><br/>" +
 0164                          $"Успешно: <b>{successCount}</b>, с ошибками: <b>{continueLablesCuont}</b>." +
 0165                          $"</div>";
 166
 0167                await NotificationService.NotifyAllAsync(msg);
 168
 169
 0170                if (continueLablesCuont > 0 && failedShipments.Any())
 171
 172                {
 0173                    var warn = $"<div class='alert alert-warning'>" +
 0174                               $"<strong>Часть этикеток не сформирована</strong><br/>" +
 0175                               $"Ошибки по заказам: {string.Join("; ", failedShipments.Take(5))}" +
 0176                               (failedShipments.Count > 5 ? " ..." : "") +
 0177                               $"</div>";
 178
 0179                    await NotificationService.NotifyAllAsync(warn);
 180                }
 181            }
 182
 183
 0184            ClearSelectedIdsSession();
 185
 0186            return result;
 0187        }
 188
 189
 190        [ServiceEndpoint]
 191        [HttpGet]
 192        public async Task<IActionResult> GetOzonLabelEligibleOrders(string ids)
 193        {
 0194            if (string.IsNullOrWhiteSpace(ids))
 195            {
 0196                return Json(new { eligibleOrderIds = Array.Empty<int>(), skippedOrders = Array.Empty<object>() });
 197            }
 198
 0199            var orderIds = ids.Split(',', StringSplitOptions.RemoveEmptyEntries)
 200                .Select(value => int.TryParse(value.Trim(), out var id) ? id : (int?)null)
 0201                .OfType<int>()
 0202                .Distinct()
 0203                .ToArray();
 204
 0205            if (orderIds.Length == 0)
 206            {
 0207                return Json(new { eligibleOrderIds = Array.Empty<int>(), skippedOrders = Array.Empty<object>() });
 208            }
 209
 0210            var orders = await _orderServices.GetOrdersNoTracking(orderIds);
 0211            var eligibleOrderIds = orders
 212                .Where(order => OzonLabelOrderEligibility.IsOzonOrder(
 213                    order.OzonClient?.ClientType,
 214                    order.ShipmentNumber,
 215                    order.OzonClient?.Id))
 216                .Select(order => order.Id)
 0217                .ToArray();
 0218            var skippedOrders = orders
 0219                .Where(order => !eligibleOrderIds.Contains(order.Id))
 220                .Select(order => new
 221                {
 222                    orderId = order.Id,
 223                    shipmentNumber = order.ShipmentNumber,
 224                    clientType = order.OzonClient?.ClientType.ToString(),
 225                    clientName = order.OzonClient?.Name
 226                })
 0227                .ToArray();
 228
 0229            return Json(new { eligibleOrderIds, skippedOrders });
 0230        }
 231
 232        [ServiceEndpoint]
 233        [HttpGet]
 234        public async Task<IActionResult> GetOrderProducts(string ids)
 235
 236        {
 237            try
 238
 239            {
 0240                if (string.IsNullOrWhiteSpace(ids))
 241
 0242                    return Json(new { orderProducts = Array.Empty<object>() });
 243
 244
 0245                var idArray = ids.Split(',', StringSplitOptions.RemoveEmptyEntries)
 246                    .Select(s => int.TryParse(s.Trim(), out var id) ? id : (int?)null)
 247                    .Where(id => id.HasValue)
 248                    .Select(id => id!.Value)
 0249                    .ToArray();
 250
 251
 0252                if (idArray.Length == 0)
 253
 0254                    return Json(new { orderProducts = Array.Empty<object>() });
 255
 256
 0257                var clients = await _ozonClientServices.GetOzonClients();
 258
 0259                var result = new List<object>();
 260
 261                // Строки одного отправления (несколько товаров в posting) — один запрос к Ozon.
 0262                var loadedPostings = new HashSet<string>(StringComparer.Ordinal);
 263
 0264                foreach (var orderId in idArray)
 265                {
 0266                    var order = await _orderServices.GetOrder(orderId);
 0267                    if (order?.OzonClient == null || string.IsNullOrEmpty(order.ShipmentNumber))
 268                        continue;
 269
 0270                    var apiPosting = OzonPostingNumberHelper.GetOzonApiPostingNumber(order.ShipmentNumber);
 0271                    if (!loadedPostings.Add(apiPosting))
 272                        continue;
 273
 274                    try
 275                    {
 0276                        var client = clients.FirstOrDefault(c => c.ClientId == order.OzonClient.ClientId);
 0277                        if (client == null || string.IsNullOrEmpty(client.DecryptClientId) ||
 0278                            string.IsNullOrEmpty(client.DecryptApiKey))
 0279                            continue;
 280
 0281                        _jsonDataBuilder.SetClient(client.DecryptClientId, client.DecryptApiKey);
 0282                        var posting = await _jsonDataBuilder.GetPostingProductsWithDeliveryAsync(apiPosting);
 0283                        var skus = posting.Products
 284                            .Select(p => p["sku"]?.ToObject<long?>() ?? 0)
 285                            .Where(sku => sku > 0)
 0286                            .Distinct()
 0287                            .ToArray();
 0288                        var images = await _jsonDataBuilder.GetPrimaryImagesBySkuAsync(skus);
 289
 0290                        result.Add(new
 0291                        {
 0292                            orderId = order.Id,
 0293                            shipmentNumber = apiPosting,
 0294                            deliveryMethod = posting.DeliveryMethodName,
 0295                            isRussianPost = OzonDeliveryMethodHelper.IsRussianPost(
 0296                                posting.DeliveryMethodName, posting.TplProvider),
 0297                            products = posting.Products.Select(p => new
 0298                            {
 0299                                offer_id = p["offer_id"]?.ToString(),
 0300                                name = p["name"]?.ToString(),
 0301                                sku = p["sku"]?.ToObject<long?>(),
 0302                                imageUrl = ResolvePostingProductImage(p, images, order),
 0303                                quantity = p["quantity"]?.ToObject<int?>(),
 0304                                price = p["price"]?.ToString(),
 0305                                currency_code = p["currency_code"]?.ToString()
 0306                            }).ToList()
 0307                        });
 0308                    }
 0309                    catch
 310                    {
 311                        // Пропускаем заказ при ошибке
 0312                    }
 0313                }
 314
 0315                return Json(new { orderProducts = result });
 316            }
 317
 0318            catch
 319
 320            {
 0321                return Json(new { orderProducts = Array.Empty<object>() });
 322            }
 0323        }
 324
 325        /// <summary>
 326        /// Картинка товара отправления: главная картинка Ozon по SKU/offer_id, иначе — картинка
 327        /// карточки товара заказа Studio (как в основной таблице), если это тот же SKU.
 328        /// </summary>
 329        private static string? ResolvePostingProductImage(
 330            Newtonsoft.Json.Linq.JObject product,
 331            IReadOnlyDictionary<string, string> images,
 332            Order order)
 333        {
 0334            var sku = product["sku"]?.ToString();
 0335            if (!string.IsNullOrEmpty(sku) && images.TryGetValue(sku, out var bySku))
 0336                return bySku;
 337
 0338            var offerId = product["offer_id"]?.ToString();
 0339            if (!string.IsNullOrEmpty(offerId) && images.TryGetValue(offerId, out var byOffer))
 0340                return byOffer;
 341
 0342            return !string.IsNullOrEmpty(sku) && order.ProductInfo?.FbsOzonSkuId == sku
 0343                ? order.ProductInfo.ImageUrl
 0344                : null;
 345        }
 346
 347
 348        [ServiceEndpoint]
 349        [HttpPost]
 350        public async Task<IActionResult> GetOzonCountryRequirements(string ids)
 351        {
 0352            var orderIds = ParseOrderIds(ids);
 0353            if (orderIds.Length == 0)
 0354                return Json(new { success = true, requirements = Array.Empty<object>() });
 355
 0356            var clients = await _ozonClientServices.GetOzonClients();
 0357            var orders = await _orderServices.GetOrdersNoTracking(orderIds);
 0358            var requirements = new List<object>();
 359
 0360            foreach (var order in orders.Where(order =>
 361                         OzonLabelOrderEligibility.IsOzonOrder(order.OzonClient?.ClientType, order.ShipmentNumber,
 362                             order.OzonClient?.Id) && !string.IsNullOrWhiteSpace(order.ShipmentNumber)))
 363            {
 0364                var client = clients.FirstOrDefault(item => item.ClientId == order.OzonClient?.ClientId);
 0365                if (client == null || string.IsNullOrWhiteSpace(client.DecryptClientId) ||
 0366                    string.IsNullOrWhiteSpace(client.DecryptApiKey))
 367                {
 368                    continue;
 369                }
 370
 371                try
 372                {
 0373                    _ozonApiDataManager.SetClient(client.DecryptClientId, client.DecryptApiKey);
 0374                    var postingNumber = OzonPostingNumberHelper.GetOzonApiPostingNumber(order.ShipmentNumber!);
 0375                    var products = await _ozonApiDataManager.GetFbsProductsRequiringCountryAsync(postingNumber);
 0376                    requirements.AddRange(products.Select(product => new
 0377                    {
 0378                        orderId = order.Id,
 0379                        shipmentNumber = order.ShipmentNumber,
 0380                        productId = product["sku"]?.ToObject<long>() ?? 0,
 0381                        productName = product["name"]?.ToObject<string>() ?? product["offer_id"]?.ToObject<string>() ?? 
 0382                        offerId = product["offer_id"]?.ToObject<string>() ?? string.Empty
 383                    }).Where(item => item.productId > 0));
 0384                }
 0385                catch
 386                {
 387                    // Ошибка Ozon по одному отправлению не должна ронять проверку остальных заказов
 388                    // и всю печать — недостающую страну пользователь увидит уже как ошибку отгрузки.
 0389                }
 0390            }
 391
 0392            return Json(new { success = true, requirements });
 0393        }
 394
 395        /// <summary>
 396        /// Требования Ozon к данным экземпляров (Честный ЗНАК, ГТД, РНПТ) перед сборкой отправления.
 397        /// Без них /v4/posting/fbs/ship отвечает EXEMPLAR_INFO_NOT_FILLED_COMPLETELY.
 398        /// </summary>
 399        [ServiceEndpoint]
 400        [HttpPost]
 401        public async Task<IActionResult> GetOzonExemplarRequirements(string ids)
 402        {
 0403            var orderIds = ParseOrderIds(ids);
 0404            if (orderIds.Length == 0)
 0405                return Json(new { success = true, requirements = Array.Empty<object>() });
 406
 0407            var clients = await _ozonClientServices.GetOzonClients();
 0408            var orders = await _orderServices.GetOrdersNoTracking(orderIds);
 0409            var requirements = new List<object>();
 410
 411            foreach (var ordersByClient in orders.GroupBy(order => order.OzonClient?.ClientId))
 412            {
 0413                var client = clients.FirstOrDefault(item => item.ClientId == ordersByClient.Key);
 0414                if (client == null || string.IsNullOrWhiteSpace(client.DecryptClientId) ||
 0415                    string.IsNullOrWhiteSpace(client.DecryptApiKey))
 416                {
 417                    continue;
 418                }
 419
 0420                _ozonExemplarService.SetClient(client.DecryptClientId, client.DecryptApiKey);
 0421                var clientRequirements = await _ozonExemplarService.GetRequirementsAsync(ordersByClient);
 422
 423                requirements.AddRange(clientRequirements.Select(item => new
 424                {
 425                    orderId = item.OrderId,
 426                    shipmentNumber = item.ShipmentNumber,
 427                    productId = item.Requirement.ProductId,
 428                    productName = item.ProductName,
 429                    offerId = item.OfferId,
 430                    exemplarId = item.Requirement.ExemplarId,
 431                    index = item.Requirement.Index,
 432                    quantity = item.Requirement.Quantity,
 433                    needsMandatoryMark = item.Requirement.NeedsMandatoryMark,
 434                    needsGtd = item.Requirement.NeedsGtd,
 435                    needsRnpt = item.Requirement.NeedsRnpt,
 436                    unsupported = item.Requirement.Unsupported
 437                }));
 438            }
 439
 0440            return Json(new { success = true, requirements });
 0441        }
 442
 443        /// <summary>
 444        /// Сохраняет данные экземпляров в Ozon и ждёт вердикта о готовности отправления к сборке.
 445        /// Печать продолжается только при ship_available.
 446        /// </summary>
 447        [ServiceEndpoint]
 448        [HttpPost]
 449        public async Task<IActionResult> SetOzonExemplars([FromBody] List<OzonExemplarSubmission>? submissions)
 450        {
 0451            if (submissions == null || submissions.Count == 0)
 452            {
 0453                return BadRequest(new { success = false, message = "Не переданы данные экземпляров" });
 454            }
 455
 456            var orderIds = submissions.Select(item => item.OrderId).Distinct().ToArray();
 0457            var orders = await _orderServices.GetOrdersNoTracking(orderIds);
 0458            var clients = await _ozonClientServices.GetOzonClients();
 459            var ordersById = orders.ToDictionary(order => order.Id);
 460
 0461            var reasons = new List<string>();
 462
 0463            foreach (var submission in submissions)
 464            {
 0465                if (!ordersById.TryGetValue(submission.OrderId, out var order) ||
 0466                    string.IsNullOrWhiteSpace(order.ShipmentNumber))
 467                {
 0468                    return BadRequest(new { success = false, message = "Заказ для сохранения экземпляров не найден" });
 469                }
 470
 0471                var client = clients.FirstOrDefault(item => item.ClientId == order.OzonClient?.ClientId);
 0472                if (client == null || string.IsNullOrWhiteSpace(client.DecryptClientId) ||
 0473                    string.IsNullOrWhiteSpace(client.DecryptApiKey))
 474                {
 0475                    return BadRequest(new { success = false, message = $"{order.ShipmentNumber}: не найден клиент Ozon" 
 476                }
 477
 0478                _ozonExemplarService.SetClient(client.DecryptClientId, client.DecryptApiKey);
 479
 480                OzonExemplarSaveResult result;
 481                try
 482                {
 0483                    result = await _ozonExemplarService.SaveAsync(order.ShipmentNumber, submission.Exemplars);
 0484                }
 0485                catch (Exception ex)
 486                {
 0487                    return BadRequest(new
 0488                    {
 0489                        success = false,
 0490                        message = $"{order.ShipmentNumber}: Ozon отклонил данные экземпляров. {ex.Message}"
 0491                    });
 492                }
 493
 0494                if (!result.ShipAvailable)
 495                {
 0496                    reasons.AddRange(result.Reasons.Select(reason => $"{order.ShipmentNumber}: {reason}"));
 497                }
 0498            }
 499
 0500            if (reasons.Count > 0)
 501            {
 0502                return Json(new
 0503                {
 0504                    success = false,
 0505                    message = "Ozon не подтвердил готовность отправления к сборке.",
 0506                    reasons
 0507                });
 508            }
 509
 0510            return Json(new { success = true });
 0511        }
 512
 513        [ServiceEndpoint]
 514        [HttpGet]
 515        public async Task<IActionResult> GetOzonProductCountries(int orderId, string? search = null)
 516        {
 0517            var order = await _orderServices.GetOrder(orderId);
 0518            var client = (await _ozonClientServices.GetOzonClients())
 0519                .FirstOrDefault(item => item.ClientId == order?.OzonClient?.ClientId);
 0520            if (client == null || string.IsNullOrWhiteSpace(client.DecryptClientId) ||
 0521                string.IsNullOrWhiteSpace(client.DecryptApiKey))
 522            {
 0523                return BadRequest(new { success = false, message = "Не найден клиент Ozon для заказа" });
 524            }
 525
 0526            _ozonApiDataManager.SetClient(client.DecryptClientId, client.DecryptApiKey);
 0527            var countries = await _ozonApiDataManager.GetFbsProductCountriesAsync(search);
 0528            return Json(new
 0529            {
 0530                success = true,
 531                countries = countries.Select(country => new
 532                {
 533                    name = country["name"]?.ToObject<string>() ?? string.Empty,
 534                    countryIsoCode = country["country_iso_code"]?.ToObject<string>() ?? string.Empty
 535                }).Where(country => !string.IsNullOrWhiteSpace(country.countryIsoCode))
 536                .OrderBy(country => country.name, StringComparer.Create(
 0537                    System.Globalization.CultureInfo.GetCultureInfo("ru-RU"), ignoreCase: true))
 0538            });
 0539        }
 540
 541        [ServiceEndpoint]
 542        [HttpPost]
 543        public async Task<IActionResult> SetOzonProductCountries([FromBody] List<OzonProductCountrySelection>? selection
 544        {
 0545            if (selections == null || selections.Count == 0 || selections.Any(selection =>
 546                    selection.OrderId <= 0 || selection.ProductId <= 0 || string.IsNullOrWhiteSpace(selection.CountryIso
 547            {
 0548                return BadRequest(new { success = false, message = "Не заполнены страны-изготовители" });
 549            }
 550
 551            var orders = await _orderServices.GetOrdersNoTracking(selections.Select(selection => selection.OrderId)
 0552                .Distinct().ToArray());
 0553            var clients = await _ozonClientServices.GetOzonClients();
 554            var ordersById = orders.ToDictionary(order => order.Id);
 555
 0556            foreach (var selection in selections)
 557            {
 0558                if (!ordersById.TryGetValue(selection.OrderId, out var order) ||
 0559                    string.IsNullOrWhiteSpace(order.ShipmentNumber))
 560                {
 0561                    return BadRequest(new { success = false, message = "Заказ для сохранения страны не найден" });
 562                }
 563
 0564                var client = clients.FirstOrDefault(item => item.ClientId == order.OzonClient?.ClientId);
 0565                if (client == null || string.IsNullOrWhiteSpace(client.DecryptClientId) ||
 0566                    string.IsNullOrWhiteSpace(client.DecryptApiKey))
 567                {
 0568                    return BadRequest(new { success = false, message = $"{order.ShipmentNumber}: не найден клиент Ozon" 
 569                }
 570
 0571                _ozonApiDataManager.SetClient(client.DecryptClientId, client.DecryptApiKey);
 0572                await _ozonApiDataManager.SetFbsProductCountryAsync(
 0573                    OzonPostingNumberHelper.GetOzonApiPostingNumber(order.ShipmentNumber), selection.ProductId,
 0574                    selection.CountryIsoCode);
 575            }
 576
 0577            return Json(new { success = true });
 0578        }
 579
 580        private static int[] ParseOrderIds(string? ids)
 581        {
 0582            return (ids ?? string.Empty).Split(',', StringSplitOptions.RemoveEmptyEntries)
 0583                .Select(value => int.TryParse(value.Trim(), out var id) ? id : (int?)null)
 0584                .OfType<int>()
 0585                .Distinct()
 0586                .ToArray();
 587        }
 588
 589        [ServiceEndpoint]
 590        [HttpPost]
 591        public async Task<IActionResult> CheckLabelsInDropbox(string ids, string? sessionKey = null)
 592
 593        {
 594            try
 595
 596            {
 0597                int[] idArray = ids.Split(',').Select(int.Parse).ToArray();
 598
 0599                var ordersLables = await _orderServices.GetOrdersNoTracking(idArray);
 600
 0601                Dictionary<string, byte[]> labels = new Dictionary<string, byte[]>();
 602
 0603                var failedShipments = new List<string>();
 604
 605
 606                // Проверяем наличие этикеток в хранилище
 607
 0608                bool allLabelsExist = true;
 609
 0610                foreach (var order in ordersLables)
 611
 612                {
 613                    try
 614
 615                    {
 0616                        var matchedFileName = await _labelFileStorage.FindExistingFileNameAsync(
 0617                            OzonPostingNumberHelper.GetLabelFileNameCandidates(order.ShipmentNumber));
 618
 0619                        if (matchedFileName != null)
 620
 621                        {
 622                            // Загружаем файл из хранилища
 623
 624                            try
 625
 626                            {
 0627                                var labelBytes = await _labelFileStorage.TryDownloadAsync(matchedFileName);
 628
 0629                                if (labelBytes != null && labelBytes.Length > 0)
 630
 631                                {
 0632                                    labels.Add(order.ShipmentNumber!, labelBytes);
 633                                }
 634                                else
 635                                {
 0636                                    allLabelsExist = false;
 637                                }
 0638                            }
 639
 0640                            catch (Exception storageEx)
 641
 642                            {
 0643                                allLabelsExist = false;
 644
 0645                                failedShipments.Add(
 0646                                    $"{order.ShipmentNumber}: Не удалось загрузить этикетку - {storageEx.Message}");
 0647                            }
 648                        }
 649
 650                        else
 651
 652                        {
 0653                            allLabelsExist = false;
 654                        }
 0655                    }
 656
 0657                    catch (Exception ex)
 658                    {
 0659                        allLabelsExist = false;
 660
 661                        System.Diagnostics.Debug.WriteLine(
 662                            $"Ошибка при проверке этикетки {order.ShipmentNumber} в хранилище: {ex.Message}");
 0663                    }
 0664                }
 665
 666
 667                // Если все этикетки существуют в хранилище, сохраняем их в сессию и возвращаем sessionKey
 668
 0669                if (allLabelsExist && labels.Count > 0)
 670
 671                {
 0672                    var finalSessionKey = _ozonLabelSessionStore.SaveLabels(HttpContext.Session, sessionKey, labels, app
 673
 674
 675                    // Обновляем кэш для всех найденных этикеток
 676
 0677                    foreach (var shipmentNumber in labels.Keys)
 678
 679                    {
 0680                        UpdateLabelCache(shipmentNumber, true);
 681                    }
 682
 683
 0684                    return Json(new
 0685
 0686                    {
 0687                        success = true,
 0688
 0689                        message = $"Этикетки для всех заказов найдены в системе ({labels.Count} шт.)",
 0690
 0691                        sessionKey = finalSessionKey,
 0692
 0693                        generatedLabels = labels.Keys.ToList(),
 0694
 0695                        failedShipments = failedShipments,
 0696
 0697                        totalCount = labels.Count,
 0698
 0699                        labelsAlreadyExist = true,
 0700
 0701                        skipFurtherSteps = true // Флаг для фронтенда, чтобы пропустить дальнейшие шаги
 0702                    });
 703                }
 704
 705
 0706                string? accumulatedSessionKey = null;
 0707                if (labels.Count > 0 && !string.IsNullOrEmpty(sessionKey))
 708                {
 0709                    accumulatedSessionKey = _ozonLabelSessionStore.SaveLabels(HttpContext.Session, sessionKey, labels, a
 710                }
 711
 712                // Если этикетки не найдены или не все найдены, возвращаем информацию для продолжения обычного процесса
 713
 0714                return Json(new
 0715
 0716                {
 0717                    success = true,
 0718
 0719                    message = labels.Count > 0
 0720                        ? $"Найдено {labels.Count} из {ordersLables.Count} этикеток в системе. Продолжаем генерацию оста
 0721                        : "Этикетки не найдены в системе. Продолжаем генерацию.",
 0722
 0723                    labelsFound = labels.Count,
 0724
 0725                    totalOrders = ordersLables.Count,
 0726
 0727                    skipFurtherSteps = false,
 0728
 0729                    foundLabels = labels.Keys.ToList(),
 0730
 0731                    sessionKey = accumulatedSessionKey
 0732                });
 733            }
 734
 0735            catch (Exception ex)
 736            {
 0737                return Json(new
 0738
 0739                {
 0740                    success = false,
 0741
 0742                    message = $"Ошибка при проверке этикеток в системе: {ex.Message}",
 0743
 0744                    skipFurtherSteps = false
 0745                });
 746            }
 0747        }
 748
 749
 750        /// <summary>
 751        /// Батч-запрос статусов Ozon для всех отправлений пачки: группируем по клиенту Ozon
 752        /// (у каждого свои креды) и опрашиваем отправления группы параллельно. Ошибку запроса
 753        /// не подменяем пустым статусом — такое отправление просто не попадает в словарь и
 754        /// вызывающий код запрашивает статус для него обычным путём.
 755        /// </summary>
 756        private async Task<Dictionary<string, string>> LoadOzonStatusesAsync(
 757            IEnumerable<Order> orders,
 758            List<OzonClient> clients)
 759        {
 0760            var result = new Dictionary<string, string>(StringComparer.Ordinal);
 761
 762            foreach (var clientGroup in orders.GroupBy(o => o.OzonClient?.ClientId))
 763            {
 0764                var client = clients.FirstOrDefault(c => c.ClientId == clientGroup.Key);
 0765                if (client == null || string.IsNullOrEmpty(client.DecryptClientId) ||
 0766                    string.IsNullOrEmpty(client.DecryptApiKey))
 767                {
 768                    continue;
 769                }
 770
 0771                var postings = clientGroup
 772                    .Where(o => !string.IsNullOrWhiteSpace(o.ShipmentNumber))
 773                    .Select(o => OzonPostingNumberHelper.GetOzonApiPostingNumber(o.ShipmentNumber))
 0774                    .Distinct(StringComparer.Ordinal)
 0775                    .ToList();
 776
 0777                if (postings.Count == 0)
 778                {
 779                    continue;
 780                }
 781
 0782                _jsonDataBuilder.SetClient(client.DecryptClientId, client.DecryptApiKey);
 783
 784                try
 785                {
 0786                    var statuses = await _jsonDataBuilder.GetProductStatusBatch(postings);
 0787                    foreach (var (posting, value) in statuses)
 788                    {
 0789                        if (value.Success && !string.IsNullOrEmpty(value.Status))
 790                        {
 0791                            result[posting] = value.Status;
 792                        }
 793                    }
 0794                }
 0795                catch
 796                {
 797                    // Статусы — оптимизация, не обязательное условие: при сбое каждый заказ
 798                    // запросит свой статус сам.
 0799                }
 800            }
 801
 0802            return result;
 0803        }
 804
 805        [ServiceEndpoint]
 806        public async Task<IActionResult> UpdateOrdersStatus(string ids, string? splitOption = null)
 807
 808        {
 809            try
 810
 811            {
 0812                List<OzonClient> clients = await _ozonClientServices.GetOzonClients();
 813
 0814                int[] idArray = ids.Split(',').Select(int.Parse).ToArray();
 815
 0816                var ordersLables = await _orderServices.GetOrdersNoTracking(idArray);
 817
 0818                var failedShipments = new List<string>();
 819
 0820                string updatedOrdersResult = "";
 821
 0822                var studioParentGroups = OzonPostingNumberHelper.GroupStudioLocalSplitSiblings(ordersLables);
 823
 0824                var shippedStudioParents = new HashSet<string>(StringComparer.Ordinal);
 825
 826                // Отгрузка собирает отправление целиком (все товары), поэтому остальные строки того же
 827                // отправления (несколько разных товаров в одном posting) повторно не отгружаем — иначе они
 828                // пошли бы со статусом из батча ниже, уже устаревшим после первой отгрузки.
 0829                var processedPostings = new HashSet<string>(StringComparer.Ordinal);
 830
 831                // Статусы всех отправлений — заранее и параллельно, по одному батчу на клиента Ozon.
 832                // Иначе каждый заказ в цикле ниже начинался бы с собственного последовательного
 833                // запроса статуса (~0.4 c на заказ впустую, пока Ozon уже всё отдал).
 0834                var knownStatuses = await LoadOzonStatusesAsync(ordersLables, clients);
 835
 0836                foreach (var order in ordersLables)
 837
 838                {
 839                    try
 840
 841                    {
 0842                        var client = clients.FirstOrDefault(c => c.ClientId == order.OzonClient?.ClientId);
 0843                        if (client == null || string.IsNullOrEmpty(client.DecryptClientId) ||
 0844                            string.IsNullOrEmpty(client.DecryptApiKey))
 845                        {
 0846                            failedShipments.Add($"{order.ShipmentNumber}: Ozon клиент не найден");
 0847                            continue;
 848                        }
 849
 0850                        _jsonDataBuilder.SetClient(client.DecryptClientId, client.DecryptApiKey);
 851                        // _ozonShipService держит собственный (DI, transient) OzonJsonDataBuilder —
 852                        // SetClient выше на него не влияет, клиента нужно выставлять отдельно здесь же.
 0853                        _ozonShipService.SetClient(client.DecryptClientId, client.DecryptApiKey);
 854
 0855                        var apiPosting = OzonPostingNumberHelper.GetOzonApiPostingNumber(order.ShipmentNumber);
 0856                        if (studioParentGroups.TryGetValue(apiPosting, out var siblings))
 857                        {
 0858                            if (shippedStudioParents.Contains(apiPosting))
 859                            {
 0860                                updatedOrdersResult =
 0861                                    $"Отгрузка для {order.ShipmentNumber} выполнена в составе группы {apiPosting}.";
 0862                                continue;
 863                            }
 864
 0865                            updatedOrdersResult = await _ozonLabelPrintService.ProcessStudioLocalSplitGroupShipAsync(
 0866                                siblings,
 0867                                apiPosting,
 0868                                _ozonShipService.ProcessOrderStatusAsync);
 0869                            shippedStudioParents.Add(apiPosting);
 0870                            continue;
 871                        }
 872
 0873                        if (!processedPostings.Add(apiPosting))
 874                        {
 0875                            updatedOrdersResult =
 0876                                $"Отгрузка для {order.ShipmentNumber} выполнена в составе отправления {apiPosting}.";
 0877                            continue;
 878                        }
 879
 0880                        updatedOrdersResult = knownStatuses.TryGetValue(apiPosting, out var knownStatus)
 0881                            ? await _ozonShipService.ProcessOrderStatusWithKnownStatusAsync(
 0882                                order, splitOption, knownStatus)
 0883                            : await _ozonShipService.ProcessOrderStatusAsync(order, splitOption);
 0884                    }
 885
 0886                    catch (Exception ex)
 887                    {
 0888                        await _ozonShipService.RecordFailedShipmentAsync(order, ex, failedShipments);
 889                    }
 0890                }
 891
 892
 0893                await _clientStatusSyncService.SyncOrderStatusesAsync(idArray);
 894
 0895                var labelBlockReasons = new List<string>();
 0896                string? primaryOzonStatus = null;
 0897                foreach (var order in ordersLables)
 898                {
 0899                    if (failedShipments.Exists(f => f.StartsWith(order.ShipmentNumber + ":", StringComparison.Ordinal)))
 900                    {
 901                        continue;
 902                    }
 903
 0904                    if (order.OzonClient == null)
 905                    {
 906                        continue;
 907                    }
 908
 0909                    var client = clients.FirstOrDefault(c => c.ClientId == order.OzonClient.ClientId);
 0910                    if (client == null || string.IsNullOrEmpty(client.DecryptClientId) ||
 0911                        string.IsNullOrEmpty(client.DecryptApiKey))
 912                    {
 913                        continue;
 914                    }
 915
 0916                    _jsonDataBuilder.SetClient(client.DecryptClientId, client.DecryptApiKey);
 0917                    var ozonStatus = await _jsonDataBuilder.GetProductSatatus(
 0918                        OzonPostingNumberHelper.GetOzonApiPostingNumber(order.ShipmentNumber!));
 0919                    primaryOzonStatus ??= ozonStatus;
 920
 0921                    if (!OzonLabelStatusHelper.IsOzonStatusLabelGenerationBlocked(ozonStatus))
 922                    {
 923                        continue;
 924                    }
 925
 0926                    var statusName = OzonLabelStatusHelper.GetOzonStatusDisplayName(ozonStatus);
 0927                    labelBlockReasons.Add(
 0928                        $"{order.ShipmentNumber}: статус «{statusName}» — Ozon API не выдаёт этикетки для завершённых от
 0929                }
 930
 0931                var allFailed = ordersLables.Count > 0 && failedShipments.Count >= ordersLables.Count;
 0932                var anyFailed = failedShipments.Any();
 0933                var labelGenerationBlocked = labelBlockReasons.Count > 0;
 934
 0935                if (anyFailed)
 936                {
 937                }
 938
 0939                var resultMessage = anyFailed
 0940                    ? string.Join("; ", failedShipments)
 0941                    : updatedOrdersResult;
 942
 0943                if (!anyFailed && !string.IsNullOrWhiteSpace(updatedOrdersResult))
 944                {
 0945                    resultMessage = updatedOrdersResult;
 946                }
 947
 0948                if (labelGenerationBlocked)
 949                {
 0950                    resultMessage = string.Join("; ", labelBlockReasons);
 951                }
 952
 0953                var result = new
 0954
 0955                {
 0956                    success = !allFailed && !labelGenerationBlocked,
 0957
 0958                    message = resultMessage,
 0959
 0960                    updatedOrders = ordersLables.Count > 0 ? ordersLables[0].ShipmentNumber : "",
 0961
 0962                    failedShipments = failedShipments,
 0963
 0964                    totalOrders = ordersLables.Count,
 0965
 0966                    ozonStatus = primaryOzonStatus,
 0967
 0968                    labelGenerationBlocked,
 0969
 0970                    labelBlockReasons,
 0971
 0972                    step = allFailed || labelGenerationBlocked ? "UpdateOrdersStatus" : null
 0973                };
 974
 975
 0976                return Json(result);
 977            }
 978
 0979            catch (Exception ex)
 980
 981            {
 0982                return Json(new
 0983
 0984                {
 0985                    success = false,
 0986
 0987                    message = $"Ошибка при обновлении статусов: {ex.Message}",
 0988
 0989                    step = "UpdateOrdersStatus"
 0990                });
 991            }
 0992        }
 993
 994
 995        [ServiceEndpoint]
 996        [HttpPost]
 997        public async Task<IActionResult> GenerateLabels(string ids, string? sessionKey = null)
 998
 999        {
 1000            try
 1001
 1002            {
 01003                List<OzonClient> clients = await _ozonClientServices.GetOzonClients();
 1004
 01005                int[] idArray = ids.Split(',').Select(int.Parse).ToArray();
 1006
 01007                var ordersLables = await _orderServices.GetOrdersNoTracking(idArray);
 1008
 01009                Dictionary<string, byte[]> labels = new Dictionary<string, byte[]>();
 1010
 01011                var failedShipments = new List<string>();
 1012
 01013                var labelsFromDropbox = new List<string>();
 1014
 01015                var labelsGenerated = new List<string>();
 1016
 01017                var ordersNeedingGeneration = new List<Order>();
 1018
 01019                var autoShippedShipments = new List<string>();
 1020
 01021                var labelsStored = new List<string>();
 1022
 01023                foreach (var order in ordersLables)
 1024
 1025                {
 1026                    try
 1027
 1028                    {
 01029                        var matchedFileName = await _labelFileStorage.FindExistingFileNameAsync(
 01030                            OzonPostingNumberHelper.GetLabelFileNameCandidates(order.ShipmentNumber));
 1031
 1032
 01033                        if (matchedFileName != null)
 1034
 1035                        {
 1036                            try
 1037
 1038                            {
 01039                                var labelBytes = await _labelFileStorage.TryDownloadAsync(matchedFileName);
 1040
 01041                                if (labelBytes != null && labelBytes.Length > 0)
 1042
 1043                                {
 01044                                    labels.Add(order.ShipmentNumber!, labelBytes);
 1045
 01046                                    labelsFromDropbox.Add(order.ShipmentNumber!);
 1047                                }
 1048                                else
 1049                                {
 01050                                    ordersNeedingGeneration.Add(order);
 1051                                }
 01052                            }
 1053
 01054                            catch (Exception storageEx)
 1055
 1056                            {
 01057                                ordersNeedingGeneration.Add(order);
 01058                                failedShipments.Add(
 01059                                    $"{order.ShipmentNumber}: Не удалось загрузить этикетку - {storageEx.Message}");
 01060                            }
 1061                        }
 1062
 1063                        else
 1064
 1065                        {
 01066                            ordersNeedingGeneration.Add(order);
 1067                        }
 01068                    }
 1069
 01070                    catch (Exception ex)
 1071                    {
 01072                        failedShipments.Add($"{order.ShipmentNumber}: {ex.Message}");
 01073                    }
 01074                }
 1075
 01076                await _ozonLabelPrintService.GenerateOzonLabelsForOrdersAsync(
 01077                    ordersNeedingGeneration,
 01078                    clients,
 01079                    labels,
 01080                    labelsGenerated,
 01081                    failedShipments,
 01082                    autoShippedShipments,
 01083                    labelsStored);
 1084
 01085                if (labelsStored.Count > 0)
 1086                {
 01087                    var stored = labelsStored.ToHashSet(StringComparer.OrdinalIgnoreCase);
 01088                    await _labelPrintLogService.RecordAsync(ordersLables
 01089                        .Where(order => order.ShipmentNumber is { } shipment && stored.Contains(shipment))
 01090                        .Select(order => CreateStudioLabelLog($"{order.ShipmentNumber}.pdf", order.Id)));
 1091                }
 1092
 1093                // ScanIt связывает этикетку с заказом: если его ещё нет в базе, берём у Ozon при печати.
 1094                try
 1095                {
 01096                    await _ozonLabelPrintService.EnsureScanitAsync(ordersLables.ToList(), clients);
 01097                }
 01098                catch
 1099                {
 1100                    // Не мешаем печати: ScanIt подтянется при следующей печати или загрузке заказа.
 01101                }
 1102
 01103                if (labels.Count == 0)
 1104
 1105                {
 1106
 01107                    return Json(new
 01108
 01109                    {
 01110                        success = false,
 01111
 01112                        message = "Не удалось получить или сгенерировать ни одной этикетки",
 01113
 01114                        failedShipments = failedShipments,
 01115
 01116                        autoShippedShipments = autoShippedShipments,
 01117
 01118                        step = "GenerateLabels"
 01119                    });
 1120                }
 1121
 1122
 01123                var finalSessionKey = _ozonLabelSessionStore.SaveLabels(HttpContext.Session, sessionKey, labels, append:
 1124
 1125
 01126                var message = $"Обработано {labels.Count} этикеток";
 1127
 01128                if (labelsFromDropbox.Count > 0 && labelsGenerated.Count > 0)
 1129
 1130                {
 01131                    message +=
 01132                        $" ({labelsFromDropbox.Count} загружено из системы, {labelsGenerated.Count} сгенерировано и сохр
 1133                }
 1134
 01135                else if (labelsFromDropbox.Count > 0)
 1136
 1137                {
 01138                    message += $" (все загружены из системы)";
 1139                }
 1140
 01141                else if (labelsGenerated.Count > 0)
 1142
 1143                {
 01144                    message += $" (все сгенерированы и успешно сохранены в системе)";
 1145                }
 1146
 01147                if (autoShippedShipments.Count > 0)
 1148
 1149                {
 01150                    message += $". Статус {autoShippedShipments.Count} заказ(ов) автоматически обновлён (отгружен) перед
 1151                }
 1152
 1153                // Обновляем кэш для всех успешно обработанных этикеток
 1154
 01155                foreach (var shipmentNumber in labels.Keys)
 1156
 1157                {
 01158                    UpdateLabelCache(shipmentNumber, true);
 1159                }
 1160
 1161
 01162                return Json(new
 01163
 01164                {
 01165                    success = true,
 01166
 01167                    message = message,
 01168
 01169                    sessionKey = finalSessionKey,
 01170
 01171                    generatedLabels = labels.Keys.ToList(),
 01172
 01173                    failedShipments = failedShipments,
 01174
 01175                    totalCount = labels.Count,
 01176
 01177                    fromDropbox = labelsFromDropbox,
 01178
 01179                    autoShippedShipments = autoShippedShipments,
 01180
 01181                    newlyGenerated = labelsGenerated,
 01182
 01183                    labelsFromDropbox = labelsFromDropbox
 01184                });
 1185            }
 1186
 01187            catch (Exception ex)
 1188
 1189            {
 01190                return Json(new
 01191
 01192                {
 01193                    success = false,
 01194
 01195                    message = $"Ошибка при генерации этикеток: {ex.Message}",
 01196
 01197                    step = "GenerateLabels"
 01198                });
 1199            }
 01200        }
 1201
 1202
 1203        [HttpPost]
 1204        /// <summary>
 1205        /// Этикетки Яндекс.Маркета для выбранных заказов.
 1206        /// <paramref name="force"/> = перепечать: игнорирует сохранённый PDF и запрашивает у API
 1207        /// новый в формате <paramref name="format"/>, перезаписывая файл в хранилище.
 1208        /// </summary>
 1209        public async Task<IActionResult> GenerateYandexLabels(string ids, string format = "A7", bool force = false)
 1210
 1211        {
 1212            try
 1213
 1214            {
 01215                var labels = new Dictionary<string, byte[]>();
 1216
 01217                var failedShipments = new List<string>();
 1218
 01219                var labelsFromDropbox = new List<string>();
 1220
 01221                var labelsGenerated = new List<string>();
 01222                var storedLabelLogs = new List<LabelPrintLogEntry>();
 1223
 1224
 01225                var f = (format ?? "A7").Trim().ToUpperInvariant();
 1226
 01227                if (f != "A9_HORIZONTALLY" && f != "A9" && f != "A7" && f != "A4")
 1228
 01229                    f = "A7";
 1230
 1231
 01232                var clients = await _ozonClientServices.GetOzonClients();
 1233
 01234                var idArray = ids.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries)
 1235                    .Select(s => int.TryParse(s.Trim(), out var n) ? n : 0).Where(i => i > 0).ToArray();
 1236
 01237                var ordersToProcess = new List<Order>();
 1238
 1239
 01240                foreach (var orderId in idArray)
 1241
 1242                {
 01243                    var order = await _orderServices.GetOrder(orderId);
 1244
 01245                    if (order == null) continue;
 1246
 01247                    if (order.OzonClient?.ClientType != ClientType.YANDEX)
 1248
 1249                    {
 01250                        failedShipments.Add($"{order.ShipmentNumber}: не заказ Яндекс.Маркета");
 1251
 01252                        continue;
 1253                    }
 1254
 1255
 01256                    ordersToProcess.Add(order);
 1257                }
 1258
 1259
 1260                // YandexDataManager хранит текущего клиента. Поэтому клиентов обрабатываем
 1261                // последовательно, а независимые заказы одного клиента — параллельно.
 1262                // Внутренний rate limiter YandexDataManager по-прежнему ограничивает API до 100 rpm.
 1263                // Partner API допускает не более 4 параллельных запросов на кампанию/бизнес (иначе HTTP 420).
 1264                const int labelParallelism = 4;
 01265                var resultLock = new object();
 1266
 1267                foreach (var clientGroup in ordersToProcess.GroupBy(order => order.OzonClient?.ClientId))
 1268                {
 01269                    var client = clients.FirstOrDefault(c => c.ClientId == clientGroup.Key);
 01270                    if (client == null || string.IsNullOrEmpty(client.DecryptClientId) ||
 01271                        string.IsNullOrEmpty(client.DecryptApiKey))
 1272                    {
 01273                        lock (resultLock)
 1274                        {
 01275                            failedShipments.AddRange(clientGroup.Select(order =>
 1276                                $"{order.ShipmentNumber}: нет данных клиента Яндекс"));
 01277                        }
 1278
 1279                        continue;
 1280                    }
 1281
 01282                    _yandexDataManager.SetClient(client.DecryptClientId, client.DecryptApiKey);
 01283                    using var semaphore = new SemaphoreSlim(labelParallelism);
 1284
 01285                    await Task.WhenAll(clientGroup.Select(async order =>
 01286                    {
 01287                        await semaphore.WaitAsync();
 01288                        try
 01289                        {
 01290                            var shipmentNumber = order.ShipmentNumber;
 01291                            if (string.IsNullOrWhiteSpace(shipmentNumber))
 01292                            {
 01293                                lock (resultLock) failedShipments.Add("Заказ без номера отправления");
 01294                                return;
 01295                            }
 01296
 01297                            var fileName = $"{shipmentNumber}.pdf";
 01298                            if (!force && await _labelFileStorage.ExistsAsync(fileName))
 01299                            {
 01300                                var storedBytes = await _labelFileStorage.TryDownloadAsync(fileName);
 01301                                if (storedBytes is not { Length: > 0 })
 01302                                    throw new Exception("Пустой файл этикетки в хранилище");
 01303
 01304                                lock (resultLock)
 01305                                {
 01306                                    labels[shipmentNumber] = storedBytes;
 01307                                    labelsFromDropbox.Add(shipmentNumber);
 01308                                }
 01309
 01310                                return;
 01311                            }
 01312
 01313                            if (!long.TryParse(shipmentNumber, out var yandexOrderId))
 01314                            {
 01315                                lock (resultLock) failedShipments.Add($"{shipmentNumber}: некорректный номер заказа");
 01316                                return;
 01317                            }
 01318
 01319                            var campaignId = await _yandexDataManager.GetCampaignIdForOrderAsync(
 01320                                client.DecryptClientId, yandexOrderId);
 01321                            if (!campaignId.HasValue)
 01322                            {
 01323                                lock (resultLock) failedShipments.Add($"{shipmentNumber}: не удалось получить campaignId
 01324                                return;
 01325                            }
 01326
 01327                            var labelBytes = await _yandexDataManager.GetOrderLabelPdfAsync(
 01328                                campaignId.Value, yandexOrderId, f);
 01329                            if (labelBytes is not { Length: > 0 })
 01330                            {
 01331                                lock (resultLock) failedShipments.Add($"{shipmentNumber}: пустой ответ API этикеток");
 01332                                return;
 01333                            }
 01334
 01335                            await _labelFileStorage.UploadAsync(labelBytes, fileName, overwrite: true);
 01336                            lock (resultLock)
 01337                            {
 01338                                labels[shipmentNumber] = labelBytes;
 01339                                labelsGenerated.Add(shipmentNumber);
 01340                                storedLabelLogs.Add(CreateStudioLabelLog(fileName, order.Id));
 01341                            }
 01342                        }
 01343                        catch (Exception ex)
 01344                        {
 01345                            lock (resultLock) failedShipments.Add($"{order.ShipmentNumber}: {ex.Message}");
 01346                        }
 01347                        finally
 01348                        {
 01349                            semaphore.Release();
 01350                        }
 01351                    }));
 01352                }
 1353
 1354
 01355                if (labels.Count == 0)
 1356
 1357                {
 01358                    return Json(new
 01359
 01360                    {
 01361                        success = false,
 01362
 01363                        message = "Не удалось получить или сгенерировать ни одной этикетки Яндекс",
 01364
 01365                        failedShipments = failedShipments
 01366                    });
 1367                }
 1368
 01369                if (storedLabelLogs.Count > 0)
 1370                {
 01371                    await _labelPrintLogService.RecordAsync(storedLabelLogs);
 1372                }
 1373
 1374
 01375                var sessionKey = $"labels_{Guid.NewGuid()}";
 1376
 01377                HttpContext.Session.SetString(sessionKey, JsonConvert.SerializeObject(labels.Keys.ToList()));
 1378
 01379                foreach (var label in labels)
 1380
 1381                {
 01382                    var fileKey = $"{sessionKey}_{label.Key}";
 1383
 01384                    HttpContext.Session.Set(fileKey, label.Value);
 1385                }
 1386
 1387
 01388                var message = $"Обработано {labels.Count} этикеток Яндекс";
 1389
 01390                if (labelsFromDropbox.Count > 0 && labelsGenerated.Count > 0)
 1391
 01392                    message += $" ({labelsFromDropbox.Count} из системы, {labelsGenerated.Count} сгенерировано)";
 1393
 01394                else if (labelsFromDropbox.Count > 0)
 1395
 01396                    message += " (все из системы)";
 1397
 01398                else if (labelsGenerated.Count > 0)
 1399
 01400                    message += " (все сгенерированы)";
 1401
 1402
 01403                foreach (var shipmentNumber in labels.Keys)
 1404
 01405                    UpdateLabelCache(shipmentNumber, true);
 1406
 1407
 01408                return Json(new
 01409
 01410                {
 01411                    success = true,
 01412
 01413                    message = message,
 01414
 01415                    sessionKey = sessionKey,
 01416
 01417                    generatedLabels = labels.Keys.ToList(),
 01418
 01419                    failedShipments = failedShipments,
 01420
 01421                    totalCount = labels.Count,
 01422
 01423                    fromDropbox = labelsFromDropbox,
 01424
 01425                    newlyGenerated = labelsGenerated
 01426                });
 1427            }
 1428
 01429            catch (Exception ex)
 1430            {
 01431                return Json(new { success = false, message = $"Ошибка при генерации этикеток Яндекс: {ex.Message}" });
 1432            }
 01433        }
 1434
 1435        private LabelPrintLogEntry CreateStudioLabelLog(string fileName, int orderId) =>
 01436            new(
 01437                fileName,
 01438                orderId,
 01439                LabelPrintSource.Studio2,
 01440                LabelPrintActorKind.StudioUser,
 01441                User.FindFirstValue(ClaimTypes.NameIdentifier),
 01442                User.Identity?.Name ?? "Неизвестный пользователь");
 1443
 1444
 1445        [ServiceEndpoint]
 1446        [HttpGet]
 1447        public async Task<IActionResult> DownloadLabels(string sessionKey)
 1448
 1449        {
 1450            try
 1451
 1452            {
 01453                var labelKeysJson = HttpContext.Session.GetString(sessionKey);
 1454
 01455                if (string.IsNullOrEmpty(labelKeysJson))
 1456
 1457                {
 01458                    return NotFound("Файлы этикеток не найдены или срок их хранения истек");
 1459                }
 1460
 1461
 01462                var labelKeys = JsonConvert.DeserializeObject<List<string>>(labelKeysJson);
 01463                if (labelKeys == null || labelKeys.Count == 0)
 1464                {
 01465                    return NotFound("Файлы этикеток не найдены или срок их хранения истек");
 1466                }
 1467
 01468                Dictionary<string, byte[]> labels = new Dictionary<string, byte[]>();
 1469
 1470
 01471                foreach (var key in labelKeys)
 1472
 1473                {
 01474                    await Task.CompletedTask;
 01475                    var fileKey = $"{sessionKey}_{key}";
 1476
 01477                    var bytes = HttpContext.Session.Get(fileKey);
 1478
 01479                    if (bytes != null)
 1480
 1481                    {
 01482                        labels.Add(key, bytes);
 1483                    }
 01484                }
 1485
 1486
 01487                if (labels.Count == 0)
 1488
 1489                {
 01490                    return NotFound("Файлы этикеток не найдены");
 1491                }
 1492
 1493
 1494                // Очищаем сессию после использования
 1495
 01496                HttpContext.Session.Remove(sessionKey);
 1497
 01498                foreach (var key in labelKeys)
 1499
 1500                {
 01501                    var fileKey = $"{sessionKey}_{key}";
 1502
 01503                    HttpContext.Session.Remove(fileKey);
 1504                }
 1505
 1506
 1507                // Если только один файл - возвращаем его напрямую
 1508
 01509                if (labels.Count == 1)
 1510
 1511                {
 01512                    var label = labels.First();
 1513
 01514                    Response.Headers.Append("Content-Disposition", $"inline; filename=\"{label.Key}.pdf\"");
 01515                    return File(label.Value, "application/pdf");
 1516                }
 1517
 1518
 1519                // Несколько этикеток возвращаем единым многостраничным PDF.
 01520                await using var mergedStream = new MemoryStream();
 01521                using var mergedPdf = new PdfDocument(new PdfWriter(mergedStream));
 01522                foreach (var label in labels)
 1523                {
 01524                    await using var sourceStream = new MemoryStream(label.Value);
 01525                    using var sourcePdf = new PdfDocument(new PdfReader(sourceStream));
 01526                    sourcePdf.CopyPagesTo(1, sourcePdf.GetNumberOfPages(), mergedPdf);
 1527                }
 1528
 01529                mergedPdf.Close();
 01530                Response.Headers.Append("Content-Disposition",
 01531                    $"inline; filename=\"labels_{DateTime.Now:yyyyMMdd_HHmmss}.pdf\"");
 01532                return File(mergedStream.ToArray(), "application/pdf");
 01533            }
 1534
 01535            catch (Exception ex)
 1536            {
 01537                return StatusCode(500, $"Ошибка при скачивании этикеток: {ex.Message}");
 1538            }
 01539        }
 1540
 1541
 1542        [ServiceEndpoint]
 1543        [HttpPost]
 1544        public IActionResult ClearLabelsSession(string sessionKey)
 1545        {
 01546            _ozonLabelSessionStore.ClearSession(HttpContext.Session, sessionKey);
 01547            return Ok(new { success = true });
 1548        }
 1549
 1550
 1551        private IActionResult? SendFilesToUser(Dictionary<string, byte[]> files)
 1552
 1553        {
 01554            List<FileContentResult> fileResults = new List<FileContentResult>();
 1555
 1556
 01557            foreach (var file in files)
 1558
 1559            {
 01560                var fileResult = new FileContentResult(file.Value, "application/pdf")
 01561
 01562                {
 01563                    FileDownloadName = $"{file.Key}.pdf"
 01564                };
 1565
 1566
 01567                fileResults.Add(fileResult);
 1568            }
 1569
 1570
 01571            if (fileResults.Count == 0)
 1572
 1573            {
 01574                return null;
 1575            }
 1576
 1577
 1578            // Если только один файл — отдаем его напрямую, без ZIP
 1579
 01580            if (fileResults.Count == 1)
 1581
 1582            {
 01583                var single = fileResults[0];
 1584
 01585                return File(single.FileContents, single.ContentType, single.FileDownloadName);
 1586            }
 1587
 1588
 01589            string tempDirectory = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString());
 1590
 01591            Directory.CreateDirectory(tempDirectory);
 1592
 1593
 01594            List<string> tempFiles = new List<string>();
 1595
 01596            foreach (var fileResult in fileResults)
 1597
 1598            {
 01599                string tempFilePath = Path.Combine(tempDirectory, fileResult.FileDownloadName);
 1600
 01601                System.IO.File.WriteAllBytes(tempFilePath, fileResult.FileContents);
 1602
 01603                tempFiles.Add(tempFilePath);
 1604            }
 1605
 1606
 01607            string zipFilePath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString() + ".zip");
 1608
 01609            ZipFile.CreateFromDirectory(tempDirectory, zipFilePath);
 1610
 1611
 01612            Directory.Delete(tempDirectory, true);
 1613
 1614
 01615            return PhysicalFile(zipFilePath, "application/zip", $"labels_{DateTime.Now:dd.MM.yyyy}.zip");
 1616        }
 1617}
 1618
 1619public sealed class OzonProductCountrySelection
 1620{
 1621    public int OrderId { get; set; }
 1622    public long ProductId { get; set; }
 1623    public string CountryIsoCode { get; set; } = string.Empty;
 1624}
 1625
 1626/// <summary>Данные экземпляров одного заказа, введённые в форме перед печатью этикетки.</summary>
 1627public sealed class OzonExemplarSubmission
 1628{
 1629    public int OrderId { get; set; }
 1630
 1631    public List<OzonExemplarInput> Exemplars { get; set; } = new();
 1632}

/home/runner/work/Studio2Prod/Studio2Prod/OzonOrdersWeb/Areas/Studio2/Controllers/OrdersController.Upload.cs

#LineLine coverage
 1using Hangfire;
 2using Hangfire.States;
 3using Microsoft.AspNetCore.Authorization;
 4using Microsoft.AspNetCore.Mvc;
 5using Microsoft.AspNetCore.Mvc.Rendering;
 6using Newtonsoft.Json;
 7using OServices.FiltersServices.FilterModels;
 8using OzonDomains;
 9using OzonDomains.Models;
 10using OzonDomains.Models.AccessControl;
 11using OzonDomains.ViewModels;
 12using OzonOrdersWeb.ViewModels.OrderViewModels;
 13using Services.ApiServices.OzonApi;
 14using Services.ApiServices.YandexApi;
 15using Services.DataServices;
 16using Services.DataServices.DTO;
 17using Services.HangfireService;
 18using Services.ParserServices;
 19using Services.ParserServices.FielParsers;
 20using Services.ParserServices.HelpDictEnum;
 21using Services.ReleasServices.ReleaseManager;
 22using Services.SignalRServices;
 23
 24namespace OzonOrdersWeb.Controllers;
 25
 26public partial class OrdersController
 27{
 28        public async Task<IActionResult> Upload()
 29
 30        {
 031            await Task.CompletedTask;
 032            if (TempData.ContainsKey("ErorrResult") && TempData["ErorrResult"] != null)
 33
 34            {
 035                string result = TempData["ErorrResult"]?.ToString() ?? string.Empty;
 36
 037                ViewData["ErorrResult"] = result;
 38            }
 39
 040            if (TempData.ContainsKey("TransactionErrorCode") && TempData["TransactionErrorCode"] != null)
 41            {
 042                ViewData["TransactionErrorCode"] = TempData["TransactionErrorCode"]?.ToString();
 43            }
 44
 45
 046            return View();
 047        }
 48
 49
 50        public async Task<IActionResult> UploadFile()
 51
 52        {
 053            await Task.CompletedTask;
 054            if (TempData.ContainsKey("ErorrResult") && TempData["ErorrResult"] != null)
 55
 56            {
 057                string result = TempData["ErorrResult"]?.ToString() ?? string.Empty;
 58
 059                ViewData["ErorrResult"] = result;
 60            }
 61
 062            if (TempData.ContainsKey("TransactionErrorCode") && TempData["TransactionErrorCode"] != null)
 63            {
 064                ViewData["TransactionErrorCode"] = TempData["TransactionErrorCode"]?.ToString();
 65            }
 66
 67
 068            return View();
 069        }
 70
 71
 72        [HttpPost]
 73        public IActionResult Upload(int period, ClientType clientType)
 74
 75        {
 076            BackgroundJobClient client = new BackgroundJobClient();
 77
 078            client.Create(
 079                () => UploadInBackground(period, clientType),
 080                new EnqueuedState(HangfireQueueNames.Main)
 081            );
 82
 083            TempData["TransactionResult"] = "Загрузка обрабатывается в фоне.";
 84
 085            ClearSelectedIdsSession();
 86
 087            return RedirectToAction(nameof(Index));
 88        }
 89
 90
 91        public async Task UploadInBackground(int period, ClientType clientType)
 92
 93        {
 094            string resultString = await UploadClientsData(period, clientType);
 95
 096            await NotificationService.NotifyAllAsync($"Ручная загрузка завершена:{resultString}");
 097        }
 98
 99
 100        public async Task<string> UploadClientsData(int period, ClientType clientType)
 101
 102        {
 0103            List<OzonClient> ozonClients = (await _ozonClientServices.GetOzonClients())
 104                .Where(c => c.ClientType == ClientType.OZON).ToList();
 105
 0106            List<OzonClient> yandexClients = (await _ozonClientServices.GetOzonClients())
 107                .Where(c => c.ClientType == ClientType.YANDEX).ToList();
 108
 0109            int[] result = new int[2];
 110
 0111            int timeZone = _releaseManager.GetTimeZone();
 112
 0113            var start = DateTime.Now.AddHours(-period * 24 + timeZone);
 114
 0115            var end = DateTime.Now.AddHours(timeZone);
 116
 0117            var clientStatus = "<br>Результат загрузки по клиентам<br>";
 118
 119
 0120            if (clientType == ClientType.ALL || clientType == ClientType.YANDEX)
 121
 122            {
 0123                clientStatus += await UploadOrders(yandexClients, start, end, ClientType.YANDEX, result);
 124            }
 125
 126
 0127            if (clientType == ClientType.ALL || clientType == ClientType.OZON)
 128
 129            {
 0130                clientStatus += await UploadOrders(ozonClients, start, end, ClientType.OZON, result);
 131            }
 132
 133
 0134            await _cacheUpdater.Update(_cache);
 135
 0136            _duplicateOrdersServices.DeleteDuplicateOrders();
 137
 138
 0139            return $"{start:dd.MM.yyyy HH:mm:ss} - {end:dd.MM.yyyy HH:mm:ss}<br/>{clientStatus}";
 0140        }
 141
 142
 143        private async Task<string> UploadOrders(List<OzonClient> clients, DateTime start, DateTime end,
 144            ClientType clientType, int[] result)
 145
 146        {
 0147            var clientStatus = string.Empty;
 148
 149
 0150            foreach (var client in clients)
 151
 152            {
 153                try
 154
 155                {
 0156                    if (clientType == ClientType.YANDEX)
 157
 158                    {
 0159                        if (string.IsNullOrEmpty(client.DecryptClientId) || string.IsNullOrEmpty(client.DecryptApiKey))
 160                        {
 0161                            clientStatus += $"{client.Name}: отсутствуют учётные данные API<br>";
 0162                            continue;
 163                        }
 164
 0165                        _yandexDataManager.SetClient(client.DecryptClientId, client.DecryptApiKey);
 166
 0167                        var jsonData = await _yandexDataManager.GetOrders(start, end);
 168
 0169                        if (jsonData.Count == 0 && _yandexDataManager.LastError != null)
 170                        {
 0171                            clientStatus += $"{client.Name}: {_yandexDataManager.LastError}<br>";
 0172                            continue;
 173                        }
 174
 0175                        var orders = await _orderCaster.YandexToOrders(jsonData);
 176
 177
 0178                        orders = orders.Select(order =>
 0179
 0180                        {
 0181                            order.OzonClient = client;
 0182
 0183                            return order;
 0184                        }).ToList();
 185
 186
 0187                        var uploadResult = await _orderServices.AddOrders(orders);
 188
 0189                        result[0] += uploadResult[0];
 190
 0191                        result[1] += uploadResult[1];
 192
 193                        // Статусы и причины отмены — как в UploadingYandex
 0194                        var statusSync = await _yandexOrderStatusSyncService.SyncAsync(client, jsonData);
 195
 0196                        clientStatus += $"{client.Name}: статусов обновлено {statusSync.ClientStatusUpdated}," +
 0197                                        $" отмен зафиксировано {statusSync.CancellationsRecorded}<br>";
 0198                    }
 199
 0200                    else if (clientType == ClientType.OZON)
 201
 202                    {
 0203                        if (string.IsNullOrEmpty(client.DecryptClientId) || string.IsNullOrEmpty(client.DecryptApiKey))
 204                        {
 0205                            clientStatus += $"{client.Name}: отсутствуют учётные данные API<br>";
 0206                            continue;
 207                        }
 208
 0209                        _jsonDataBuilder.SetClient(client.DecryptClientId, client.DecryptApiKey);
 210
 0211                        var jsonData = await _jsonDataBuilder.BuildData(start, end);
 212
 0213                        var orders = await _orderCaster.JsonToOrders(jsonData);
 214
 215
 0216                        orders = orders.Select(order =>
 0217
 0218                        {
 0219                            order.OzonClient = client;
 0220
 0221                            return order;
 0222                        }).ToList();
 223
 224
 0225                        var uploadResult = await _orderServices.AddOrders(orders);
 226
 0227                        result[0] += uploadResult[0];
 228
 0229                        result[1] += uploadResult[1];
 230                    }
 231
 0232                    clientStatus += $"{client.Name}: отчёт успешно создан<br>";
 0233                }
 234
 0235                catch (Exception ex)
 236                {
 0237                    clientStatus += $"{client.Name}: {ex.Message}<br>";
 0238                }
 0239            }
 240
 241
 0242            return clientStatus;
 0243        }
 244
 245
 246        [HttpPost]
 247        public async Task<IActionResult> UploadExcelFile(IFormFile file, int startRow = 1, int startColumn = 1,
 248            char delimiter = ';')
 249
 250        {
 0251            if (file != null && file.Length > 0)
 252
 253            {
 0254                string tempDir = Path.GetTempPath();
 255
 0256                string tempFilePath =
 0257                    Path.Combine(tempDir, Guid.NewGuid().ToString() + Path.GetExtension(file.FileName));
 258
 259
 260                try
 261
 262                {
 0263                    using (var fileStream = new FileStream(tempFilePath, FileMode.Create))
 264
 265                    {
 0266                        await file.CopyToAsync(fileStream);
 0267                    }
 268
 269
 0270                    List<Dictionary<string, string>> tableData = new List<Dictionary<string, string>>();
 271
 0272                    List<string> tableHeaders = [];
 273
 0274                    using (var memoryStream = new MemoryStream())
 275
 276                    {
 0277                        using (var fileStream = new FileStream(tempFilePath, FileMode.Open, FileAccess.Read))
 278
 279                        {
 0280                            var fileExtension = Path.GetExtension(file.FileName).ToLower();
 281
 282
 283                            // Поддерживаем .xlsx, .xls и .csv файлы
 284
 0285                            if (fileExtension == ".xlsx" ||
 0286                                fileExtension == ".xls" ||
 0287                                file.ContentType ==
 0288                                "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" ||
 0289                                file.ContentType == "application/vnd.ms-excel")
 290
 291                            {
 0292                                await fileStream.CopyToAsync(memoryStream);
 293                            }
 294
 0295                            else if (fileExtension == ".csv" || file.ContentType == "text/csv")
 296
 297                            {
 0298                                await _excelParser.ConvertCsvToExcel(fileStream, delimiter).CopyToAsync(memoryStream);
 299                            }
 300
 301                            else
 302
 303                            {
 0304                                throw new Exception(
 0305                                    $"Неподдерживаемый формат файла: {fileExtension}. Поддерживаются форматы: .xlsx, .xl
 306                            }
 0307                        }
 308
 309
 0310                        memoryStream.Position = 0;
 311
 312
 0313                        tableHeaders = await _excelParser.GetTableHeadersAsync(memoryStream, startRow: startRow,
 0314                            startColumn: startColumn);
 315
 0316                        tableData = await _excelParser.GetTableDataAsync(memoryStream, startRow: startRow,
 0317                            startColumn: startColumn);
 0318                    }
 319
 320
 0321                    string dropBoxFilePath = $"/ExcelFiles/{DateTime.Now.ToString("dd.MM.yyyy")}";
 322
 0323                    await _dropboxApiClient.UploadFileAsync(tempFilePath, dropBoxFilePath, file.FileName);
 324
 325
 0326                    if (System.IO.File.Exists(tempFilePath))
 327
 328                    {
 0329                        System.IO.File.Delete(tempFilePath);
 330                    }
 331
 332
 0333                    HttpContext.Session.SetString("TableHeaders", JsonConvert.SerializeObject(tableHeaders));
 334
 0335                    HttpContext.Session.SetString("TableData", JsonConvert.SerializeObject(tableData));
 336
 0337                    HttpContext.Session.SetString("FilePath", JsonConvert.SerializeObject(dropBoxFilePath));
 338
 0339                    HttpContext.Session.SetString("FileName", JsonConvert.SerializeObject(file.FileName));
 340
 341
 0342                    return RedirectToAction(nameof(PrepaExcelTable));
 343                }
 344
 0345                catch (Exception)
 346
 347                {
 0348                    TempData["ErorrResult"] = $"Ошибка при загрузке файла.";
 349
 0350                    return RedirectToAction(nameof(Upload));
 351                }
 352            }
 353
 354
 0355            TempData["ErorrResult"] = $"Файл не был выбран.";
 356
 0357            return RedirectToAction(nameof(Upload));
 0358        }
 359
 360
 361        public async Task<IActionResult> PrepaExcelTable()
 362
 363        {
 0364            var headersJson = HttpContext.Session.GetString("TableHeaders");
 0365            var dataJson = HttpContext.Session.GetString("TableData");
 366
 0367            if (string.IsNullOrEmpty(headersJson) || string.IsNullOrEmpty(dataJson))
 368            {
 0369                TempData["ErorrResult"] = "Данные файла не найдены. Пожалуйста, загрузите файл заново.";
 0370                return RedirectToAction(nameof(Upload));
 371            }
 372
 0373            var tableHeaders = JsonConvert.DeserializeObject<List<string>>(headersJson);
 0374            var tableData = JsonConvert.DeserializeObject<List<Dictionary<string, string>>>(dataJson);
 375
 0376            var filrPath = HttpContext.Session.GetString("FilePath")?.Trim('"');
 0377            var fileName = HttpContext.Session.GetString("FileName")?.Trim('"');
 378
 0379            if (tableHeaders == null || tableData == null)
 380            {
 0381                TempData["ErorrResult"] = "Данные файла не найдены. Пожалуйста, загрузите файл заново.";
 0382                return RedirectToAction(nameof(Upload));
 383            }
 384
 385
 386            // Создаем словарь нормализованных заголовков для быстрого поиска
 387
 0388            var normalizedTableHeaders = tableHeaders
 0389                .Select(h => new { Original = h, Normalized = NormalizeHeaderForComparison(h) })
 390                .Where(x => !string.IsNullOrWhiteSpace(x.Normalized))
 391                .GroupBy(x => x.Normalized)
 392                .ToDictionary(g => g.Key, g => g.First().Original);
 393
 394
 395            // Получаем сохраненные сопоставления и проверяем их валидность
 396
 0397            var allSavedMappings = await _columnMappingDataServices.GetColumnMappings();
 398
 0399            var validSavedMappings = new List<OzonDomains.Models.ColumnMapping>();
 400
 0401            var invalidMappings = new List<(string MappingName, List<string> MissingColumns)>();
 402
 403
 0404            foreach (var mapping in allSavedMappings)
 405
 406            {
 0407                if (mapping.ColumnMappings == null || mapping.ColumnMappings.Count == 0)
 408
 409                {
 0410                    validSavedMappings.Add(mapping);
 411
 0412                    continue;
 413                }
 414
 415
 0416                var missingColumns = new List<string>();
 417
 0418                foreach (var columnMapping in mapping.ColumnMappings)
 419
 420                {
 0421                    if (string.IsNullOrWhiteSpace(columnMapping.Value))
 422
 423                        continue;
 424
 425
 426                    // Проверяем, существует ли столбец в текущем файле
 427
 0428                    var normalizedValue = NormalizeHeaderForComparison(columnMapping.Value);
 429
 0430                    if (!normalizedTableHeaders.ContainsKey(normalizedValue))
 431
 432                    {
 0433                        missingColumns.Add(columnMapping.Value);
 434                    }
 435                }
 436
 437
 0438                if (missingColumns.Count > 0)
 439
 440                {
 0441                    invalidMappings.Add((mapping.MappingName, missingColumns));
 442                }
 443
 444                else
 445
 446                {
 0447                    validSavedMappings.Add(mapping);
 448                }
 449            }
 450
 451
 452            // Если есть невалидные сопоставления, показываем предупреждение
 453
 0454            if (invalidMappings.Count > 0)
 455
 456            {
 0457                var errorMessages = invalidMappings.Select(m =>
 458                    $"Сопоставление '{m.MappingName}': не найдены столбцы: {string.Join(", ", m.MissingColumns)}");
 459
 0460                TempData["MappingWarning"] = string.Join("; ", errorMessages);
 461            }
 462
 463
 0464            var model = new ExelDataViewModel
 0465
 0466            {
 0467                TableHeaders = tableHeaders,
 0468
 0469                TableData = tableData,
 0470
 0471                SavedMappings = validSavedMappings,
 0472
 0473                FilePath = filrPath ?? string.Empty,
 0474
 0475                FileName = fileName ?? string.Empty,
 0476
 0477                OzonClients = await _ozonClientServices.GetOzonClients(),
 0478
 0479                Statuses = new SelectList(
 480                    (await _cache.Get()).Select(s => s.ClientStatus?.Name).Where(s => !string.IsNullOrEmpty(s))
 481                    .Select(s => s!).Distinct().Select(s => new { Name = s }).ToList()
 482                    .OrderBy(a => a.Name), "Name", "Name"),
 0483
 484                Manufacturers = (await _manufacturerDataService.GetManufacturers()).Where(m => m.Name != null).ToList(),
 0485
 0486                Warehouses = await _warehouseDataServices.GetWarehouses(),
 0487
 488                Suppliers = (await _supplierDataServices.GetSuppliers()).OrderBy(a => a.Name).ToList(),
 0489
 0490                CurrencyCodes =
 0491
 0492                [
 0493                    (CurrencyCode.RUB, "RUB"),
 0494
 0495                    (CurrencyCode.USD, "USD"),
 0496
 0497                    (CurrencyCode.EUR, "EUR"),
 0498
 0499                    (CurrencyCode.BYN, "BYN")
 0500                ],
 0501            };
 502
 0503            ViewBag.HasActiveFilters = HasActiveOrderFilters(HttpContext.Request.Cookies["FilterData"]);
 504
 0505            await _currencyRatesViewBagService.SetViewBagAsync(ViewData);
 506
 0507            return View(model);
 0508        }
 509
 510
 511        /// <summary>
 512        /// Нормализует имя заголовка для сравнения (аналогично методу в ExcelParser)
 513        /// </summary>
 514        private string NormalizeHeaderForComparison(string headerName)
 515
 516        {
 0517            if (string.IsNullOrEmpty(headerName))
 518
 0519                return string.Empty;
 520
 521
 522            // Заменяем различные варианты переносов строк на пробелы
 523
 0524            var normalized = headerName
 0525                .Replace("\r\n", " ") // Windows line break
 0526                .Replace("\n", " ") // Unix line break
 0527                .Replace("\r", " ") // Old Mac line break
 0528                .Replace("\t", " ") // Tabs
 0529                .Replace("\u00A0", " ") // Non-breaking space
 0530                .Replace("\u2009", " ") // Thin space
 0531                .Replace("\u200A", " ") // Hair space
 0532                .Replace("\u202F", " ") // Narrow no-break space
 0533                .Replace("\u205F", " "); // Medium mathematical space
 534
 535
 536            // Убираем множественные пробелы и обрезаем
 537
 0538            normalized = System.Text.RegularExpressions.Regex.Replace(normalized, @"\s+", " ").Trim();
 539
 540
 0541            return normalized;
 542        }
 543
 544
 545        [HttpPost]
 546        public async Task<IActionResult> GroupFileData(ExelDataViewModel model)
 547
 548        {
 0549            var tableDataJson = HttpContext.Session.GetString("TableData");
 0550            model.TableData =
 0551                string.IsNullOrEmpty(tableDataJson)
 0552                    ? new List<Dictionary<string, string>>()
 0553                    : JsonConvert.DeserializeObject<List<Dictionary<string, string>>>(tableDataJson)
 0554                      ?? new List<Dictionary<string, string>>();
 555
 556
 0557            model.TableData = _excelParser.UpdateTableToStandartColumns(model.TableData, model.ColumnMappings);
 558
 559
 0560            var orders = await _orderCaster.ExcelToOrders(
 0561                model.TableData,
 0562                model.SelectedClient!,
 0563                model.SelectedManufacturer!,
 0564                model.SelectedWarehouse!,
 0565                model.SelectedSupplier!,
 0566                model.SelectedStatus!,
 0567                model.SelectedCurrencyCode,
 0568                model.SelectedShippingDate,
 0569                model.SelectedProcessingDate
 0570            );
 571
 0572            orders = _orderServices.MergeOrders(orders);
 573
 574
 0575            orders = await _orderServices.CalculateCostPriceForNotFullOrders(orders);
 576
 577
 0578            foreach (var order in orders)
 579
 580            {
 0581                if (order.EtProducerId != null)
 582
 583                {
 0584                    order.EtProducer = _etProducerDataServices.GetEtProducer(order.EtProducerId.Value);
 585                }
 586            }
 587
 588
 0589            byte[] excelFile = _excelExporter.ExportToExcel(orders, [
 0590                "Артикул", "ЧистыйАртикул", "Производитель",
 0591
 0592                "Оригинальная цена", "Цена закупки", "Количество"
 0593            ]);
 594
 595
 0596            return File(excelFile, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
 0597                $"GroupOrders_{DateTime.Now:dd.MM.yyyy}.xlsx");
 0598        }
 599
 600
 601        private bool HasActiveOrderFilters(string? filterDataString)
 602
 603        {
 0604            if (string.IsNullOrEmpty(filterDataString))
 605
 606            {
 0607                return false;
 608            }
 609
 610
 611            try
 612
 613            {
 0614                var filterData = JsonConvert.DeserializeObject<OrderFilterModel>(filterDataString);
 615
 0616                if (filterData == null)
 617
 618                {
 0619                    return false;
 620                }
 621
 622
 623                bool HasAnyString(params string?[] values) =>
 0624                    values.Any(v => !string.IsNullOrWhiteSpace(v));
 625
 626
 627                bool HasAnyNullableNumber(params decimal?[] values) =>
 0628                    values.Any(v => v.HasValue);
 629
 630
 631                bool HasAnyNullableInt(params int?[] values) =>
 0632                    values.Any(v => v.HasValue);
 633
 634
 635                bool HasAnyNullableDouble(params double?[] values) =>
 0636                    values.Any(v => v.HasValue);
 637
 638
 0639                bool hasDates = filterData.ProcessingDate.HasValue
 0640                                || filterData.ShippingDate.HasValue
 0641                                || filterData.LastStatusChangeDate.HasValue;
 642
 643
 0644                bool hasStrings = HasAnyString(
 0645                    filterData.Key,
 0646                    filterData.ShipmentNumber,
 0647                    filterData.Status,
 0648                    filterData.AppStatus,
 0649                    filterData.ProductName,
 0650                    filterData.Article,
 0651                    filterData.Manufacturer,
 0652                    filterData.ShipmentWarehouse,
 0653                    filterData.Сurrency,
 0654                    filterData.Supplier,
 0655                    filterData.OrderNumberToSupplier,
 0656                    filterData.CommercialCategory,
 0657                    filterData.DeliveryCity,
 0658                    filterData.NewCategory,
 0659                    filterData.OzonClient,
 0660                    filterData.DeliveryPeriod,
 0661                    filterData.Delivery,
 0662                    filterData.Provider
 0663                );
 664
 665
 0666                bool hasDecimals = HasAnyNullableNumber(
 0667                    filterData.ShipmentAmount,
 0668                    filterData.Price,
 0669                    filterData.PurchasePrice,
 0670                    filterData.CurrentPriceWithDiscount,
 0671                    filterData.OzonCommission,
 0672                    filterData.Profit,
 0673                    filterData.Discount,
 0674                    filterData.CostPrice
 0675                );
 676
 677
 0678                bool hasInts = HasAnyNullableInt(filterData.TimeLeftDay, filterData.Quantity);
 679
 680
 0681                bool hasDoubles = HasAnyNullableDouble(filterData.Volume, filterData.VolumetricWeight);
 682
 683
 0684                bool includeArchived = filterData.IncludeArchived;
 685
 686
 0687                return hasDates
 0688                       || hasStrings
 0689                       || hasDecimals
 0690                       || hasInts
 0691                       || hasDoubles
 0692                       || includeArchived;
 693            }
 694
 0695            catch
 696
 697            {
 0698                return false;
 699            }
 0700        }
 701
 702
 703        [HttpPost]
 704        public async Task<IActionResult> AppendFile(
 705            ExelDataViewModel model,
 706            List<string> compareColumnsList,
 707            List<string> addColumnsList)
 708
 709        {
 0710            var standartFileDataJson = HttpContext.Session.GetString("TableData");
 0711            var standartFileData = string.IsNullOrEmpty(standartFileDataJson)
 0712                ? null
 0713                : JsonConvert.DeserializeObject<List<Dictionary<string, string>>>(standartFileDataJson);
 714
 0715            var fileName = HttpContext.Session.GetString("FileName")?.Trim('"');
 716
 717
 0718            if (standartFileData == null || string.IsNullOrEmpty(fileName))
 719
 720            {
 0721                return BadRequest("Не удалось получить данные из сессии");
 722            }
 723
 724
 0725            var contextOrders = await _orderServices.GetOrders();
 726
 0727            var standartTableData = _excelParser.UpdateTableToStandartColumns(standartFileData, model.ColumnMappings);
 728
 0729            var compareColumnsSet = new HashSet<string>(compareColumnsList, StringComparer.OrdinalIgnoreCase);
 730
 731
 0732            var ordersToFile = new Dictionary<int, Order>();
 733
 0734            for (int i = 0; i < standartTableData.Count; i++)
 735
 736            {
 0737                var row = standartTableData[i];
 738
 0739                var filterCriteria = row
 0740                    .Where(w => compareColumnsSet.Contains(w.Key))
 741                    .ToDictionary(kvp => kvp.Key, kvp => kvp.Value);
 742
 743
 0744                if (filterCriteria.Count == 0) continue;
 745
 746
 0747                var order = _dataFilterManager.FilterOrdersByColumns(filterCriteria, contextOrders, addColumnsList)
 0748                    .FirstOrDefault();
 749
 750
 0751                if (order != null)
 752
 753                {
 0754                    ordersToFile[i] = order;
 755                }
 756            }
 757
 758
 0759            var resultData = _excelExporter.AddOrdersToFileData(standartFileData, ordersToFile, addColumnsList);
 760
 0761            byte[] excelFile = _excelExporter.ExportToExcel(resultData);
 762
 763
 0764            return File(excelFile, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", fileName);
 0765        }
 766
 767
 768        [HttpPost]
 769        public async Task<IActionResult> CheckFile(ExelDataViewModel model, string saveName, bool notFull, bool isGroup)
 770
 771        {
 0772            var tableDataJson = HttpContext.Session.GetString("TableData");
 0773            model.TableData = string.IsNullOrEmpty(tableDataJson)
 0774                ? new List<Dictionary<string, string>>()
 0775                : JsonConvert.DeserializeObject<List<Dictionary<string, string>>>(tableDataJson)
 0776                  ?? new List<Dictionary<string, string>>();
 777
 778
 0779            model.TableData = _excelParser.UpdateTableToStandartColumns(model.TableData, model.ColumnMappings);
 780
 781
 0782            if (saveName != null)
 783
 784            {
 785                try
 786
 787                {
 0788                    ColumnMapping newMapping = new ColumnMapping()
 0789
 0790                    {
 0791                        MappingName = saveName,
 0792
 0793                        ColumnMappings = model.ColumnMappings,
 0794
 0795                        SelectedClientId = model.SelectedClient != null && model.SelectedClient.Id != 0
 0796                            ? model.SelectedClient.Id
 0797                            : null,
 0798
 0799                        SelectedCurrencyCode = model.SelectedCurrencyCode,
 0800
 0801                        SelectedManufacturerId =
 0802                            model.SelectedManufacturer != null && model.SelectedManufacturer.Id != -1 &&
 0803                            model.SelectedManufacturer.Id != 0
 0804                                ? model.SelectedManufacturer.Id
 0805                                : null,
 0806
 0807                        SelectedWarehouseId = model.SelectedWarehouse != null && model.SelectedWarehouse.Id != 0
 0808                            ? model.SelectedWarehouse.Id
 0809                            : null,
 0810
 0811                        SelectedSupplierId = model.SelectedSupplier != null && model.SelectedSupplier.Id != 0
 0812                            ? model.SelectedSupplier.Id
 0813                            : null,
 0814
 0815                        SelectedStatus = model.SelectedStatus,
 0816
 0817                        ManufacturerFromArticle =
 0818                            model.SelectedManufacturer != null && model.SelectedManufacturer.Id == -1 ? true : false,
 0819                    };
 820
 821
 0822                    await _columnMappingDataServices.AddColumnMapping(newMapping);
 0823                }
 824
 0825                catch (Exception ex)
 826                {
 0827                    throw new Exception(
 0828                        $"Произошла ошибка при cохранении состояния сопоставлений столбцов: {ex.Message}");
 829                }
 830            }
 831
 832
 833            List<string> allOzonClients = (await _ozonClientServices.GetOzonClients()).Select(c => c.Name).ToList();
 834
 0835            List<string> allStatuses = (await _clientStatusServices.GetClientStatuses())
 836                .Select(s => s.Name)
 837                .Where(s => !string.IsNullOrEmpty(s))
 0838                .Distinct()
 0839                .ToList();
 840
 0841            List<string> allWarehouses = (await _warehouseDataServices.GetWarehouses())
 842                .Where(w => w.Name != null)
 843                .Select(w => w.Name!)
 0844                .ToList();
 845
 846
 0847            List<string> newOzonClients = [];
 848
 0849            List<string> newStatuses = [];
 850
 0851            List<string> newWarehouses = [];
 852
 853
 0854            foreach (var row in model.TableData)
 855
 856            {
 0857                foreach (var kvp in row)
 858
 859                {
 0860                    string key = kvp.Key;
 861
 0862                    string value = kvp.Value;
 863
 864
 0865                    if (value != "")
 866
 867                    {
 0868                        if (key == "Клиент" && !allOzonClients.Contains(value) && !newOzonClients.Contains(value))
 869
 870                        {
 0871                            newOzonClients.Add(value);
 872                        }
 873
 874
 0875                        if (key == "Статус клинта" && !allStatuses.Contains(value) && !newStatuses.Contains(value))
 876
 877                        {
 0878                            newStatuses.Add(value);
 879                        }
 880
 881
 0882                        if (key == "Склад отгрузки" && !allWarehouses.Contains(value) && !newWarehouses.Contains(value))
 883
 884                        {
 0885                            newWarehouses.Add(value);
 886                        }
 887                    }
 888                }
 889            }
 890
 891
 0892            HttpContext.Session.SetString("ExelDataViewModel", JsonConvert.SerializeObject(model));
 893
 0894            HttpContext.Session.SetString("NotFull", JsonConvert.SerializeObject(notFull));
 895
 0896            HttpContext.Session.SetString("IsGroup", JsonConvert.SerializeObject(isGroup));
 897
 898
 0899            if (newOzonClients.Count > 0 || newStatuses.Count > 0 || newWarehouses.Count > 0)
 900
 901            {
 0902                return View(new CheckUniqueValueViewModel()
 0903
 0904                {
 0905                    NewOzonClients = newOzonClients,
 0906
 0907                    NewStatuses = newStatuses,
 0908
 0909                    NewWarehouses = newWarehouses
 0910                });
 911            }
 912
 913            else
 914
 915            {
 0916                return RedirectToAction(nameof(UploadExcelOrders));
 917            }
 0918        }
 919
 920
 921        [HttpPost]
 922        public IActionResult NewValuesProcessing(CheckUniqueValueViewModel newValues)
 923
 924        {
 0925            var modelJson = HttpContext.Session.GetString("ExelDataViewModel");
 926
 0927            if (modelJson != null)
 928
 929            {
 0930                var model = JsonConvert.DeserializeObject<ExelDataViewModel>(modelJson);
 0931                if (model?.TableData == null)
 932                {
 0933                    return RedirectToAction(nameof(UploadExcelOrders));
 934                }
 935
 936
 0937                foreach (var row in model.TableData)
 938
 939                {
 0940                    foreach (var key in row.Keys.ToList())
 941
 942                    {
 0943                        string value = row[key];
 944
 945
 0946                        if (newValues.NewOzonClientsDict.TryGetValue(value, out bool isAcceptedOzonClient) &&
 0947                            !isAcceptedOzonClient)
 948
 949                        {
 0950                            row[key] = "Не указан";
 951                        }
 952
 953
 0954                        else if (newValues.NewStatusesDict.TryGetValue(value, out bool isAcceptedStatus) &&
 0955                                 !isAcceptedStatus)
 956
 957                        {
 0958                            row[key] = "Не указан";
 959                        }
 960
 0961                        else if (newValues.NewWarehousesDict.TryGetValue(value, out bool isAcceptedWarehouse) &&
 0962                                 !isAcceptedWarehouse)
 963
 964                        {
 0965                            row[key] = "Не указан";
 966                        }
 967                    }
 968                }
 969
 970
 0971                HttpContext.Session.SetString("ExelDataViewModel", JsonConvert.SerializeObject(model));
 972
 973
 0974                return RedirectToAction(nameof(UploadExcelOrders));
 975            }
 976
 977
 0978            return RedirectToAction(nameof(UploadExcelOrders));
 979        }
 980
 981
 982        public async Task<IActionResult> UploadExcelOrders()
 983
 984        {
 985            try
 986
 987            {
 0988                var notFullJson = HttpContext.Session.GetString("NotFull");
 989
 0990                var isGroupJson = HttpContext.Session.GetString("IsGroup");
 991
 0992                var modelJson = HttpContext.Session.GetString("ExelDataViewModel");
 993
 994
 0995                if (bool.TryParse(notFullJson, out var notFull) && notFull)
 996
 997                {
 0998                    return RedirectToAction(nameof(SetNotFullOrdersData));
 999                }
 1000
 1001
 01002                if (modelJson != null)
 1003
 1004                {
 01005                    var model = JsonConvert.DeserializeObject<ExelDataViewModel>(modelJson);
 01006                    if (model == null)
 1007                    {
 01008                        return RedirectToAction(nameof(Index));
 1009                    }
 1010
 1011
 01012                    int[] result = [0, 0];
 1013
 1014
 01015                    List<Order> orders = await _orderCaster.ExcelToOrders(model.TableData,
 01016                        model.SelectedClient!,
 01017                        model.SelectedManufacturer!,
 01018                        model.SelectedWarehouse!,
 01019                        model.SelectedSupplier!,
 01020                        model.SelectedStatus!,
 01021                        model.SelectedCurrencyCode,
 01022                        model.SelectedShippingDate,
 01023                        model.SelectedProcessingDate);
 1024
 1025
 01026                    if (bool.TryParse(isGroupJson, out var isGroup) && isGroup && !string.IsNullOrEmpty(modelJson))
 1027
 1028                    {
 01029                        orders = _orderServices.MergeOrders(orders);
 1030                    }
 1031
 1032
 01033                    orders = await _orderServices.SetUniqueShipmentNumberAndKey(orders);
 1034
 1035
 01036                    orders = await _orderCaster.SetFileDataAsync(orders, model.FilePath, model.FileName);
 1037
 1038
 01039                    var uploadResult = await _orderServices.AddOrders(orders);
 1040
 01041                    result[0] += uploadResult[0];
 1042
 01043                    result[1] += uploadResult[1];
 1044
 1045
 01046                    TempData["UploadResult"] = result;
 1047
 01048                    await NotificationService.NotifyAllAsync(
 01049                        $"Файл успешно загружен! <br />Результат: <br />Добавлено {uploadResult[0]} строк.<br />Обновлен
 1050
 01051                    await _cacheUpdater.Update(_cache);
 1052
 01053                    _duplicateOrdersServices.DeleteDuplicateOrders();
 1054
 01055                    ClearSelectedIdsSession();
 01056                }
 01057            }
 1058
 01059            catch (Exception ex)
 1060            {
 01061                throw new Exception($"Произошла ошибка при загрузке данных: {ex.Message}");
 1062            }
 1063
 1064
 01065            return RedirectToAction(nameof(Index));
 01066        }
 1067
 1068
 1069        public async Task<IActionResult> SetNotFullOrdersData()
 1070
 1071        {
 01072            var modelJson = HttpContext.Session.GetString("ExelDataViewModel");
 1073
 01074            var isGroupJson = HttpContext.Session.GetString("IsGroup");
 1075
 01076            if (modelJson == null)
 1077
 1078            {
 01079                return NotFound();
 1080            }
 1081
 1082
 01083            var model = JsonConvert.DeserializeObject<ExelDataViewModel>(modelJson);
 01084            if (model == null)
 1085            {
 01086                return NotFound();
 1087            }
 1088
 01089            List<Order> orders = await _orderCaster.ExcelToOrders(
 01090                model.TableData,
 01091                model.SelectedClient!,
 01092                model.SelectedManufacturer!,
 01093                model.SelectedWarehouse!,
 01094                model.SelectedSupplier!,
 01095                model.SelectedStatus!,
 01096                model.SelectedCurrencyCode,
 01097                model.SelectedShippingDate,
 01098                model.SelectedProcessingDate);
 1099
 1100
 01101            if (bool.TryParse(isGroupJson, out var isGroup) && isGroup && !string.IsNullOrEmpty(modelJson))
 1102
 1103            {
 01104                orders = _orderServices.MergeOrders(orders);
 1105            }
 1106
 1107
 01108            orders = await _orderCaster.SetFileDataAsync(orders, model.FilePath, model.FileName);
 1109
 01110            orders = await _orderServices.SetNumberInExcel(orders);
 1111
 1112            // Авто-номер: {base_filename}_{row} — только латиница/цифры, нижний регистр.
 1113            // Кириллица и спецсимволы → underscore (предсказуемо для внешних систем).
 01114            var shipmentBase = System.Text.RegularExpressions.Regex
 01115                .Replace(
 01116                    System.IO.Path.GetFileNameWithoutExtension(model.FileName ?? "").ToLowerInvariant(),
 01117                    @"[^a-z0-9]+",
 01118                    "_")
 01119                .Trim('_');
 1120
 1121            foreach (var order in orders.Where(o => string.IsNullOrWhiteSpace(o.ShipmentNumber)))
 01122                order.ShipmentNumber = $"{shipmentBase}_{order.NumberInExcel}";
 1123
 01124            orders = await _orderServices.CalculateCostPriceForNotFullOrders(orders);
 1125
 1126
 1127            // Применяем сохранённые фильтры с главной страницы, если они были установлены
 1128
 01129            var user = await _userCacheService.GetCachedUserAsync(User);
 1130
 01131            AccessProfile? accessProfile = null;
 1132
 01133            if (user?.AccessProfileId != null)
 1134
 1135            {
 01136                accessProfile = await _accessProfileDataServices.GetAccessProfileAsync(user.AccessProfileId.Value);
 1137            }
 1138
 1139
 01140            string? filterDataString = HttpContext.Request.Cookies["FilterData"];
 1141
 01142            OrderFilterModel filterData = new OrderFilterModel();
 1143
 01144            if (!string.IsNullOrEmpty(filterDataString))
 1145
 1146            {
 01147                filterData = JsonConvert.DeserializeObject<OrderFilterModel>(filterDataString) ?? new OrderFilterModel()
 1148            }
 1149
 1150
 01151            NotFullOrdersModel notFullOrdersModel =
 01152                _orderServices.GetNotFullOrdersModel(_dataFilterManager, orders, filterData, accessProfile);
 1153
 1154
 1155            // Ключ партии для фронта (используется для сброса/разделения сохранённого состояния между разными файлами)
 1156
 01157            ViewBag.NotFullBatchKey = $"{model.FileName}|{model.FilePath}|{model.TableData?.Count ?? 0}";
 01158            ViewBag.ExcelFileName = model.FileName ?? "";
 1159
 1160
 01161            return View(new NotFullOrdersViewModel()
 01162
 01163            {
 01164                UniqueOrders = notFullOrdersModel.UniqueOrders ?? [],
 01165
 01166                OrdersWithMultipleMatches = notFullOrdersModel.OrdersWithMultipleMatches,
 01167
 01168                OrdersWithOneMatches = notFullOrdersModel.OrdersWithOneMatches,
 01169
 01170                DistributionReasons = notFullOrdersModel.DistributionReasons,
 01171
 1172                OzonClients = (await _ozonClientServices.GetOzonClients()).OrderBy(o => o.Name).ToList(),
 01173
 1174                Suppliers = (await _supplierDataServices.GetSuppliers()).OrderBy(o => o.Name).ToList(),
 01175
 1176                AppStatuses = (await _appStatusServices.GetAppStatuses()).OrderBy(o => o.Name).ToList(),
 01177
 1178                Manufacturers = (await _manufacturerDataService.GetManufacturers()).OrderBy(o => o.Name).ToList(),
 01179
 1180                Warehouse = (await _warehouseDataServices.GetWarehouses()).OrderBy(o => o.Name).ToList(),
 01181
 01182                SelectedAppStatus = await GetDefaultAppStatus()
 01183            });
 01184        }
 1185
 1186
 1187        private async Task<AppStatus> GetDefaultAppStatus()
 1188
 1189        {
 01190            var defaultStatus =
 01191                await _appStatusServices.GetAppStatusAsync(new AppStatus() { Name = "Отгружен постащиком" });
 1192
 01193            return defaultStatus ?? await _appStatusServices.GetAppStatusAsync(new AppStatus() { Name = "Не указан" })
 01194                   ?? throw new InvalidOperationException("Статус приложения «Не указан» не найден.");
 01195        }
 1196
 1197
 1198        [HttpPost]
 1199        [AllowAnonymous]
 1200        public async Task<IActionResult> SetNotFullOrdersData(
 1201            [FromForm] List<ResultNotFullOrder> NotFullOrders,
 1202            [FromForm] bool toExcel,
 1203            [FromForm] bool onlyNewOrders,
 1204            [FromForm] string userName)
 1205
 1206        {
 01207            if (NotFullOrders == null || !NotFullOrders.Any())
 1208
 1209            {
 01210                return BadRequest("No orders.");
 1211            }
 1212
 1213
 01214            for (int i = NotFullOrders.Count - 1; i >= 0; i--)
 1215
 1216            {
 01217                if (NotFullOrders[i].Hidden)
 1218
 1219                {
 01220                    NotFullOrders.RemoveAt(i);
 1221                }
 1222            }
 1223
 1224
 1225            try
 1226
 1227            {
 01228                if (toExcel)
 1229
 1230                {
 01231                    List<Order> orderToExcel = new();
 1232
 1233
 1234                    // Если нужно экспортировать только новые заказы, фильтруем их по флагу IsUniqueOrder
 1235
 01236                    var ordersToProcess = onlyNewOrders
 1237                        ? NotFullOrders.Where(o => o.IsUniqueOrder).ToList()
 01238                        : NotFullOrders;
 1239
 1240
 01241                    if (onlyNewOrders && !ordersToProcess.Any())
 1242
 1243                    {
 01244                        return BadRequest("Новых заказов для экспорта не найдено.");
 1245                    }
 1246
 1247
 01248                    foreach (var notFullOrder in ordersToProcess)
 1249
 1250                    {
 1251                        try
 1252
 1253                        {
 01254                            var processed = await _orderServices.ProcessingNotFullOrderForExcel(notFullOrder.Order,
 01255                                notFullOrder.OrderIds);
 1256
 01257                            orderToExcel.AddRange(processed);
 01258                        }
 1259
 01260                        catch
 1261
 1262                        {
 01263                            continue;
 1264                        }
 1265                    }
 1266
 1267
 01268                    var builder = new ExcelExporterBuilder()
 01269                        .WithOrders(orderToExcel)
 01270                        .WithColumnsToExport(new List<string>
 01271
 01272                        {
 01273                            "Артикул", "ЧистыйАртикул", "Производитель", "Количество", "Клиент",
 01274
 01275                            "Номер заказа", "Сумма отправления", "Себестоимость",
 01276
 01277                            "Цена закупки", "Цена закупки до перевода", "Сумма закупки (вал)",
 01278
 01279                            "Дата отгрузки", "Наименование товара"
 01280                        })
 01281                        .WithColumnSums(new List<string>
 01282
 01283                        {
 01284                            "Цена закупки", "Сумма отправления", "Себестоимость",
 01285
 01286                            "Цена закупки до перевода", "Сумма закупки (вал)"
 01287                        });
 1288
 1289
 01290                    byte[] excelFile = builder.Build();
 1291
 1292
 01293                    ClearSelectedIdsSession();
 1294
 1295
 01296                    string fileName = onlyNewOrders
 01297                        ? $"NewOrders_{DateTime.Now:dd.MM.yyyy}.xlsx"
 01298                        : $"ImportOrders_{DateTime.Now:dd.MM.yyyy}.xlsx";
 1299
 1300
 01301                    return File(excelFile, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
 01302                        fileName);
 1303                }
 1304
 1305                else
 1306
 1307                {
 01308                    BackgroundJobClient client = new BackgroundJobClient();
 1309
 01310                    client.Create(
 01311                        () => ProcessNotFullOrdersInBackground(NotFullOrders, userName),
 01312                        new EnqueuedState(HangfireQueueNames.Main)
 01313                    );
 1314
 1315
 01316                    TempData["TransactionResult"] = "Обработка заказов запущена в фоне";
 1317
 01318                    return RedirectToAction("Index");
 1319                }
 1320            }
 1321
 01322            catch (Exception ex)
 1323            {
 01324                await NotificationService.NotifyAllAsync($"Ошибка при отгрузке: {ex.Message}");
 1325
 01326                return RedirectToAction("Index");
 1327            }
 01328        }
 1329
 1330
 1331        public async Task ProcessNotFullOrdersInBackground(List<ResultNotFullOrder> notFullOrders, string userName)
 1332
 1333        {
 1334            try
 1335
 1336            {
 01337                List<Order> ordersToTransaction = new();
 1338
 01339                foreach (var notFullOrder in notFullOrders)
 1340
 1341                {
 1342                    try
 1343
 1344                    {
 01345                        var processed = await _orderServices.ProcessingNotFullOrder(
 01346                            notFullOrder.Order,
 01347                            notFullOrder.OrderIds,
 01348                            userName
 01349                        );
 1350
 1351
 01352                        ordersToTransaction.AddRange(processed);
 01353                    }
 1354
 01355                    catch
 1356
 1357                    {
 01358                        continue;
 1359                    }
 1360                }
 1361
 1362
 01363                await _orderServices.AddOrders(ordersToTransaction);
 1364
 1365
 01366                Transaction shippedBySupplierTransaction = new Transaction()
 01367
 01368                {
 01369                    Type = TransactionType.ShippedBySupplier,
 01370
 01371                    Orders = ordersToTransaction,
 01372
 01373                    CreatedDateTime = DateTime.Now,
 01374
 01375                    CreateBy = userName,
 01376                };
 1377
 01378                await _transactionDataServices.AddTransaction(shippedBySupplierTransaction);
 01379                await _transactionDataServices.SaveChanges();
 1380
 01381                await _transactionCache.Update();
 1382
 01383                _duplicateOrdersServices.DeleteDuplicateOrders();
 1384
 1385
 01386                await NotificationService.NotifyAllAsync(
 01387                    $"Отгружено поставщиком <b>{ordersToTransaction.Count}</b> заказов");
 01388            }
 1389
 01390            catch (Exception ex)
 1391            {
 01392                await NotificationService.NotifyAllAsync($"Ошибка при фоновой обработке заказов: {ex.Message}");
 1393            }
 01394        }
 1395
 1396
 1397        public async Task<IActionResult> ViewFile(string filePath, string fileName)
 1398
 1399        {
 01400            return Redirect(await _dropboxApiClient.GetViewLinkAsync(filePath, fileName));
 01401        }
 1402        public async Task<IActionResult> SplitOrderFromSetNotFull(int orderId, string splitOption, bool onlyInSystem = f
 1403
 1404        {
 1405            try
 1406
 1407            {
 1408                var parts = splitOption.Split(' ').Select(s => s.Trim()).Select(int.Parse).ToArray();
 1409
 01410                if (parts.Length != 2)
 1411
 1412                {
 01413                    return Json(new { success = false, message = "Неверный формат разделения" });
 1414                }
 1415
 1416
 01417                var (newOrder, wasOzonSplit, wasOzonQuantityAdjusted, _) =
 01418                    await PerformSplitOrderAsync(orderId, parts[0], parts[1], onlyInSystem);
 1419
 01420                await _cache.Update();
 1421
 01422                var message = wasOzonSplit
 01423                    ? "Заказ успешно разделен на Ozon. Номера отправлений обновлены из ответа API."
 01424                    : wasOzonQuantityAdjusted
 01425                        ? "Заказ разделен локально: количество приведено к данным Ozon (в Ozon меньше, чем в системе)."
 01426                        : $"Заказ успешно разделен: {parts[0]} + {parts[1]}";
 1427
 01428                return Json(new
 01429
 01430                {
 01431                    success = true,
 01432
 01433                    message = message,
 01434
 01435                    newOrderId = newOrder.Id,
 01436
 01437                    originalOrderId = orderId,
 01438
 01439                    isOzonSplit = wasOzonSplit
 01440                });
 1441            }
 1442
 01443            catch (Exception ex)
 1444            {
 01445                return Json(new { success = false, message = GetFriendlySplitErrorMessage(ex) });
 1446            }
 01447        }
 1448}

Methods/Properties

MultiplayUpdateOrdersStatusV2()
UpdateOrdersStatusInBackground()
Details()
MultiplayEdit()
MultiplayEdit()
MultiplayEditV2()
MultiplayEditV2()
Create()
Create()
ReturnCreateViewWithReloadedListsAsync()
ResolveOrdersPageSize(System.Nullable`1<System.Int32>)
SanitizeFileName(System.String)
.ctor(OzonRepositories.Context.ApplicationDbContext,Microsoft.Extensions.Caching.Memory.IMemoryCache,Services.DataServices.OrdersDataServices,Services.DataServices.AppStatusDataServices,Services.DataServices.ClientStatusDataServices,Services.DataServices.SupplierDataServices,Services.DataServices.WarehouseDataServices,Services.ParserServices.OrderCaster,Services.CacheServices.Cache.OzonOrdersCache.OrderCache,Services.FiltersServices.DataFilterManagers.OrderDataFilterManager,Services.CacheServices.Cache.CacheUpdater`1<OzonDomains.Models.Order>,Services.ApiServices.OzonApi.OzonJsonDataBuilder,Services.ReleasServices.ReleaseManager.ReleaseManager,Services.DataServices.TransactionDataServices,Services.CacheServices.Cache.OzonOrdersCache.TransactionCache,Services.DataServices.DuplicateOrdersServices,Services.DataServices.OzonClientServices,Services.ParserServices.FielParsers.ExcelParser,Services.DataServices.ColumnMappingDataServices,Services.PriceСlculationServices.CurrencyRateFetcher,OzonOrdersWeb.WebServices.Currency.ICurrencyRatesViewBagService,Services.ApiServices.DropBoxApi.DropboxApiClient,Services.LabelServices.ILabelFileStorage,Services.DataServices.ManufacturerDataService,Services.ParserServices.FielParsers.ExcelExporter,Services.DataServices.AccessProfileDataServices,Services.ApiServices.YandexApi.YandexDataManager,Services.YandexServices.IYandexOrderStatusSyncService,Services.CacheServices.Cache.UserCacheService.IUserCacheService,Services.DataServices.OrderCartServices,Services.CacheServices.Cache.CartCache.CartCache,Services.DataServices.EtProducerDataServices,Services.DataServices.DeliveryDataServices,Services.ApiServices.OzonApi.OzonApiDataManager,OzonRepositories.Authorization.IUserScopeProvider,Services.Authorization.Services.IPermissionService,Services.LabelServices.IOzonLabelPrintService,Services.LabelServices.ILabelExistenceService,Services.LabelServices.ILabelPrintLogService,OzonOrdersWeb.WebServices.Orders.IOrderFilterListsService,OzonOrdersWeb.WebServices.Orders.IOrdersIndexQueryService,OzonOrdersWeb.WebServices.Orders.IOrderDataScopeQueryApplier,OzonOrdersWeb.WebServices.Orders.IOrdersIndexPageDetailsService,OzonOrdersWeb.WebServices.Orders.IOrderReturnInfoService,OzonOrdersWeb.WebServices.Orders.IOrdersClientStatusSyncService,OzonOrdersWeb.WebServices.Orders.IOrdersIndexClientStatusRefreshService,OzonOrdersWeb.WebServices.Orders.IOrdersIndexViewModelEnricher,OzonOrdersWeb.WebServices.Orders.IOrdersIndexPageViewDataService,OzonOrdersWeb.WebServices.Orders.IOrderDataScopeIdFilterService,Services.LabelServices.IOrdersOzonShipService,Services.LabelServices.IOzonExemplarService,OzonOrdersWeb.WebServices.Orders.IOrderCreateFormService,Services.OzonHandoverServices.IOzonHandoverImportService)
BuildOrderListFieldReadMapAsync()
BuildOrderListFieldReadMapFromBundle(Services.Authorization.Dtos.UserPermissionsBundle)
Returns()
Returns()
Index()
Index()
ViewOrders()
DelSortStateCookie()
DelSortStateCookieForV2()
SetPageSize()
SetPageSizeForV2()
Update()
UpdateV2()
Delete()
DeleteV2()
GetFriendlySplitErrorMessage(System.Exception)
PerformSplitOrderAsync()
SplitOrder()
MultiplayDeleteOrders()
DeleteOrderPage()
MultiplayDeleteOrdersV2()
CancellationOrders()
Recover()
ConfirmAccepted()
ConfirmAcceptedV2()
SaveSelectedStockListToSession()
SplitOrders()
DeleteDuplicateOrders()
OrderExists()
SetSortOrderViewData(Services.FiltersServices.SortModels.OrderSortState)
ApplySortOrder(System.Collections.Generic.IEnumerable`1<OzonDomains.Models.Order>,Services.FiltersServices.SortModels.OrderSortState)
LoadOrdersIndexPageAsync()
ShapeOrdersAfterLoad(System.Collections.Generic.List`1<OzonDomains.Models.Order>,System.Boolean,Services.FiltersServices.SortModels.OrderSortState,OServices.FiltersServices.FilterModels.OrderFilterModel,System.Int32)
ApplyOrdersIndexPageViewDataAsync()
ApplyFilterListsToViewAsync()
SetFilterLists()
ApplyOrderDataScopeAsync()
FilterOrderIdsByDataScopeAsync()
FilterByOrderScopeIdsAsync()
EnsureOrderColumnsVisibilityProfile(OzonRepositories.Context.Identity.CustomIdentityUser)
NormalizeSyntheticAccessProfile(OzonRepositories.Context.Identity.CustomIdentityUser)
SaveSortStateCookie(Services.FiltersServices.SortModels.OrderSortState)
SaveSortStateCookie(Services.FiltersServices.SortModels.OrderSortState,System.String)
GetSortStateCookie()
GetSortStateCookie(System.String)
SetInfoMessage()
RetrieveSelectedIdsFromSession()
ClearSelectedIdsSession()
UpdateLabelCache(System.String,System.Boolean)
GetOrdersByProductKey()
.cctor()
ResolveExcelColumnsAsync()
PrintOrdersExcel()
PrintOrdersExcelV2()
PrintOrdersExcelForClient()
UploadHandoverJson()
IndexTableAjax()
IndexDateCalendarAjax()
<IndexDateCalendarAjax()
get_Date()
get_Count()
get_ViewModel()
get_LoginRedirect()
BuildOrdersIndexTableAjaxViewModelAsync()
PrintOrdersLable()
<PrintOrdersLable()
GetOzonLabelEligibleOrders()
GetOrderProducts()
ResolvePostingProductImage(Newtonsoft.Json.Linq.JObject,System.Collections.Generic.IReadOnlyDictionary`2<System.String,System.String>,OzonDomains.Models.Order)
GetOzonCountryRequirements()
GetOzonExemplarRequirements()
SetOzonExemplars()
GetOzonProductCountries()
SetOzonProductCountries()
ParseOrderIds(System.String)
CheckLabelsInDropbox()
LoadOzonStatusesAsync()
UpdateOrdersStatus()
GenerateLabels()
GenerateYandexLabels()
<GenerateYandexLabels()
CreateStudioLabelLog(System.String,System.Int32)
DownloadLabels()
ClearLabelsSession(System.String)
SendFilesToUser(System.Collections.Generic.Dictionary`2<System.String,System.Byte[]>)
Upload()
UploadFile()
Upload(System.Int32,OzonDomains.ClientType)
UploadInBackground()
UploadClientsData()
UploadOrders()
UploadExcelFile()
PrepaExcelTable()
NormalizeHeaderForComparison(System.String)
GroupFileData()
HasActiveOrderFilters(System.String)
HasAnyString()
HasAnyNullableNumber()
HasAnyNullableInt()
HasAnyNullableDouble()
AppendFile()
CheckFile()
NewValuesProcessing(OzonOrdersWeb.ViewModels.OrderViewModels.CheckUniqueValueViewModel)
UploadExcelOrders()
SetNotFullOrdersData()
GetDefaultAppStatus()
SetNotFullOrdersData()
ProcessNotFullOrdersInBackground()
ViewFile()
SplitOrderFromSetNotFull()