< Summary

Information
Class: OzonOrdersWeb.Areas.Studio2.Controllers.Transactions.ShippedBySupplierTransactionController
Assembly: OzonOrdersWeb
File(s): /home/runner/work/Studio2Prod/Studio2Prod/OzonOrdersWeb/Areas/Studio2/Controllers/Transactions/ShippedBySupplierTransactionController.cs
Line coverage
0%
Covered lines: 0
Uncovered lines: 453
Coverable lines: 453
Total lines: 763
Line coverage: 0%
Branch coverage
0%
Covered branches: 0
Total branches: 262
Branch coverage: 0%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

File(s)

/home/runner/work/Studio2Prod/Studio2Prod/OzonOrdersWeb/Areas/Studio2/Controllers/Transactions/ShippedBySupplierTransactionController.cs

#LineLine coverage
 1using Microsoft.AspNetCore.Authorization;
 2using Microsoft.AspNetCore.Mvc;
 3using Microsoft.AspNetCore.Mvc.Rendering;
 4using OzonOrdersWeb.Authorization;
 5using Microsoft.EntityFrameworkCore;
 6using Microsoft.Extensions.DependencyInjection;
 7using Newtonsoft.Json;
 8using OzonDomains;
 9using OzonDomains.Models;
 10using OzonOrdersWeb.Areas.Studio2.Controllers.Base;
 11using OzonOrdersWeb.Areas.Studio2.Helpers;
 12using OzonOrdersWeb.Areas.Studio2.Models.Transactions;
 13using OzonOrdersWeb.Areas.Studio2.ViewModels.ShippedBySupplierTransaction;
 14using OzonOrdersWeb.ViewModels.OrderViewModels;
 15using OzonRepositories.Context;
 16using OzonRepositories.Data.Bitrix;
 17using PartsInfo.HttpUtils;
 18using Services.ApiServices;
 19using Services.ApiServices._1CApi;
 20using Services.ApiServices._1CApi.DTO;
 21using Services.ApiServices._1CApi.Models;
 22using Services.ApiServices._1CApi.OData;
 23using Services.CacheServices.Cache.OzonOrdersCache;
 24using Services.CacheServices.Cache.UserCacheService;
 25using Services.DataServices;
 26using Services.FiltersServices.SortModels;
 27using OzonOrdersWeb.WebServices.Currency;
 28using Services.SignalRServices;
 29using Services.TransactionUtilsServices;
 30using OzonOrdersWeb.TransactionServices;
 31using Services.CacheServices.Cache;
 32
 33namespace OzonOrdersWeb.Areas.Studio2.Controllers.Transactions;
 34
 35[Authorize(Roles = "User,Admin")]
 36[Area("Studio2")]
 37public class ShippedBySupplierTransactionController : Controller
 38{
 39    private readonly ApplicationDbContext _context;
 40    private readonly AppStatusDataServices _appStatusServices;
 41    private readonly OrdersDataServices _orderServices;
 42    private readonly TransactionManager _transaction;
 43    private readonly TransactionCache _transactionCache;
 44
 45    private readonly SupplierDataServices _supplierDataServices;
 46    private readonly IUserCacheService _userCacheService;
 47    private readonly ICurrencyRatesViewBagService _currencyRatesViewBagService;
 48    private readonly CacheUpdater<Order> _cacheUpdater;
 49    private readonly OrderCache _cache;
 50    private readonly ProxyHttpClientService _proxyHttpClientService;
 51    private readonly OneCReceiptManager _oneCReceiptManager;
 52    private readonly WarehouseDataServices _warehouseDataServices;
 53    private readonly OneCDataManager _oneCDataManager;
 54    private readonly StatusTransitionDataServices _statusTransitionDataServices;
 55
 056    public ShippedBySupplierTransactionController(
 057        ApplicationDbContext context,
 058        AppStatusDataServices appStatusServices,
 059        OrdersDataServices orderServices,
 060        TransactionManager transaction,
 061        TransactionCache transactionCache,
 062        SupplierDataServices supplierDataServices,
 063        IUserCacheService userCacheService,
 064        ICurrencyRatesViewBagService currencyRatesViewBagService,
 065        CacheUpdater<Order> cacheUpdater,
 066        OrderCache cache,
 067        ProxyHttpClientService proxyHttpClientService,
 068        OneCReceiptManager oneCReceiptManager,
 069        WarehouseDataServices warehouseDataServices,
 070        OneCDataManager oneCDataManager,
 071        StatusTransitionDataServices statusTransitionDataServices)
 72    {
 073        _context = context;
 074        _appStatusServices = appStatusServices;
 075        _orderServices = orderServices;
 076        _transaction = transaction;
 077        _transactionCache = transactionCache;
 078        _supplierDataServices = supplierDataServices;
 079        _userCacheService = userCacheService;
 080        _currencyRatesViewBagService = currencyRatesViewBagService;
 081        _cacheUpdater = cacheUpdater;
 082        _cache = cache;
 083        _proxyHttpClientService = proxyHttpClientService;
 084        _oneCReceiptManager = oneCReceiptManager;
 085        _warehouseDataServices = warehouseDataServices;
 086        _oneCDataManager = oneCDataManager;
 087        _statusTransitionDataServices = statusTransitionDataServices;
 088    }
 89
 90    // ==============================
 91    // GET: CreateShippedBySupplierTransaction
 92    // ==============================
 93    public async Task<IActionResult> CreateShippedBySupplierTransaction(string ids, int page)
 94    {
 095        var transitions = await _statusTransitionDataServices.GetByTransactionType(TransactionType.ShippedBySupplier);
 96        List<AppStatus> filteredStatuses;
 97        AppStatus targetStatus;
 098        HashSet<int>? validSourceStatusIds = null;
 99
 0100        if (transitions.Any())
 101        {
 0102            validSourceStatusIds = transitions.Select(t => t.FromStatusId).ToHashSet();
 0103            targetStatus = transitions.First().ToStatus!;
 0104            var cancelledStatus = await _appStatusServices.GetAppStatusAsync(new AppStatus() { Name = "Отменен" });
 0105            filteredStatuses = new List<AppStatus> { targetStatus };
 0106            if (cancelledStatus != null) filteredStatuses.Add(cancelledStatus);
 0107            filteredStatuses = filteredStatuses.GroupBy(s => s.Id).Select(g => g.First()).ToList();
 108        }
 109        else
 110        {
 0111            var appStatusShippedBySupplier = await _appStatusServices.GetAppStatusAsync(new AppStatus { Name = "Отгружен
 0112            if (appStatusShippedBySupplier == null)
 113            {
 0114                appStatusShippedBySupplier = new AppStatus { Name = "Отгружен поставщиком" };
 0115                _context.AppStatuses.Add(appStatusShippedBySupplier);
 0116                await _context.SaveChangesAsync();
 117            }
 0118            var cancelledStatus = await _appStatusServices.GetAppStatusAsync(new AppStatus() { Name = "Отменен" });
 0119            filteredStatuses = new List<AppStatus>();
 0120            if (appStatusShippedBySupplier != null) filteredStatuses.Add(appStatusShippedBySupplier);
 0121            if (cancelledStatus != null) filteredStatuses.Add(cancelledStatus);
 0122            filteredStatuses = filteredStatuses.GroupBy(s => s.Id).Select(g => g.First()).ToList();
 0123            targetStatus = appStatusShippedBySupplier!;
 0124        }
 125
 0126        ViewBag.AppStatuses = new SelectList(filteredStatuses, "Id", "Name");
 0127        ViewBag.StatusColors = filteredStatuses.ToDictionary(s => s.Id.ToString(), s => s.GetStatusColor());
 128
 0129        if (string.IsNullOrWhiteSpace(ids))
 0130            return BadRequest("Не выбраны заказы для транзакции.");
 131
 0132        int[] idArray = ids.Split(',', StringSplitOptions.RemoveEmptyEntries).Select(int.Parse).ToArray();
 133
 0134        if (idArray.Length == 0)
 0135            return BadRequest("Не выбраны заказы для транзакции.");
 136
 0137        List<Order> ordersToTransaction = new();
 0138        string? errorReason = null;
 0139        var errorCode = TransactionErrorCode.Unknown;
 140
 141        // ВАЖНО: Загружаем заказы БЕЗ ТРЕКИНГА - изменения статусов для отображения
 142        // НЕ сохранятся в БД при отмене транзакции
 0143        var orders = await _orderServices.GetOrdersNoTracking(idArray);
 144
 0145        foreach (var order in orders)
 146        {
 0147            bool allowed = validSourceStatusIds != null
 0148                ? (order.AppStatusId.HasValue && validSourceStatusIds.Contains(order.AppStatusId.Value))
 0149                : (order.AppStatus?.Name == "Заказан поставщику");
 150
 0151            if (allowed)
 152            {
 153                // Устанавливаем целевой статус для ОТОБРАЖЕНИЯ в форме
 154                // Заказ без трекинга - изменения НЕ сохранятся в БД
 0155                if (order.AppStatus?.Name != "Отменен")
 0156                    order.AppStatus = targetStatus;
 0157                order.UpdatedBy = User.Identity?.Name;
 0158                ordersToTransaction.Add(order);
 159            }
 160            else
 161            {
 0162                errorReason = validSourceStatusIds != null
 0163                    ? $"Заказ {order.ShipmentNumber} имеет статус '{order.AppStatus?.Name}', недопустимый для этой транз
 0164                    : $"Заказ {order.ShipmentNumber} имеет статус '{order.AppStatus?.Name}', а должен быть 'Заказан пост
 0165                errorCode = TransactionErrorCode.InvalidSourceStatus;
 166            }
 167        }
 168
 0169        if (!ordersToTransaction.Any())
 170        {
 0171            errorReason ??= validSourceStatusIds != null ? "Нет заказов с допустимым статусом для этой транзакции." : "Н
 0172            if (!string.IsNullOrWhiteSpace(errorReason) && errorCode == TransactionErrorCode.Unknown)
 0173                errorCode = TransactionErrorCode.NoOrdersForTransaction;
 174        }
 175        else
 176        {
 0177            var distinctSuppliers = ordersToTransaction.GroupBy(o => o.Supplier?.Name).ToList();
 0178            if (distinctSuppliers.Count > 1)
 179            {
 0180                errorReason = "Выбраны заказы с разными поставщиками: " +
 0181                              string.Join(", ",
 0182                                  distinctSuppliers.SelectMany(g =>
 0183                                      g.Select(o => $"{o.ShipmentNumber} (поставщик: {g.Key})")));
 0184                ordersToTransaction.Clear();
 0185                errorCode = TransactionErrorCode.MixedSuppliers;
 186            }
 187
 0188            var distinctWarehouses = ordersToTransaction
 0189                .GroupBy(TransactionWarehouseHelper.GetEffectiveWarehouseName)
 0190                .ToList();
 0191            if (distinctWarehouses.Count > 1)
 192            {
 0193                errorReason = "Выбраны заказы с разными эффективными складами: " +
 0194                              string.Join(", ",
 0195                                  distinctWarehouses.SelectMany(g =>
 0196                                      g.Select(o => $"{o.ShipmentNumber} ({TransactionWarehouseHelper.GetEffectiveWareho
 0197                ordersToTransaction.Clear();
 0198                errorCode = TransactionErrorCode.MixedWarehouses;
 199            }
 200        }
 201
 0202        List<TreatyResponse> treatyResponses = new();
 0203        if (ordersToTransaction.Any())
 204        {
 0205            var currentSupplier = ordersToTransaction.Select(s => s.Supplier).FirstOrDefault();
 0206            var supplierInn = currentSupplier?.INNCode?.Trim();
 207
 208            // Ссылка на редактирование поставщика — передаём до любых Clear()
 0209            if (currentSupplier?.Id > 0)
 0210                ViewData["SupplierEditUrl"] = Url.Action("Edit", "Supplier", new { area = "Studio2", id = currentSupplie
 211
 212            // OData: Catalog_Контрагенты по ИНН → Catalog_ДоговорыКонтрагентов по Owner
 0213            var treaties = await _oneCDataManager.GetTreatyListAsync(supplierInn);
 214
 0215            if (treaties.Count == 0)
 216            {
 0217                if (string.IsNullOrEmpty(supplierInn))
 218                {
 219                    // ИНН не заполнен — показываем предупреждение, форма остаётся открытой
 0220                    ViewData["ContractsWarning"] = "ИНН поставщика не заполнен — список договоров 1С недоступен. Заполни
 221                }
 222                else
 223                {
 0224                    var supplierLabel = string.IsNullOrEmpty(currentSupplier?.Name)
 0225                        ? $"ИНН {supplierInn}"
 0226                        : $"«{currentSupplier.Name}» (ИНН: {supplierInn})";
 0227                    ViewData["ContractsWarning"] = $"Для поставщика {supplierLabel} договоры не загрузились. Проверьте к
 228                }
 229            }
 230            else
 231            {
 0232                treatyResponses = treaties;
 233            }
 0234        }
 235
 0236        ViewData["TransactionErrorCode"] = errorCode.ToString();
 0237        ViewData["ErrorReason"] = errorReason;
 238
 0239        var conditionsMessage = transitions.Any()
 0240            ? string.Join("<br/>", transitions
 0241                .Select(t => t.Conditions)
 0242                .Where(c => !string.IsNullOrWhiteSpace(c))
 0243                .Select(c => System.Net.WebUtility.HtmlEncode(c!.Trim()))
 0244                .Distinct())
 0245            : null;
 0246        ViewData["TransactionConditionsMessage"] = conditionsMessage;
 247
 248        // === ОПТИМИЗАЦИЯ: non-DB параллельно, DB последовательно ===
 0249        var userTask = _userCacheService.GetCachedUserAsync(User);
 0250        var rateTasks = _currencyRatesViewBagService.StartFetch();
 0251        var zasylTask = _oneCDataManager.GetWarehouseListAsync();
 0252        var countriesTask = _oneCDataManager.GetCountryListAsync();
 253
 0254        var suppliers = await _supplierDataServices.GetSuppliers();
 0255        var articles = await _orderServices.GetArticleCountsForOrdersAsync(ordersToTransaction);
 0256        var cities = await _orderServices.GetDeliveryCityCountsForOrdersAsync(ordersToTransaction);
 0257        var numbers = await _orderServices.GetShipmentBaseCountsForOrdersAsync(ordersToTransaction);
 0258        var warehouses = await _warehouseDataServices.GetWarehouses();
 259
 0260        await Task.WhenAll(userTask, rateTasks.Usd, rateTasks.Eur, rateTasks.Byn, zasylTask, countriesTask);
 261
 0262        var rates = rateTasks.ToSnapshot();
 0263        var pageViewModel = new CreateShippedBySupplierTransactionModel
 0264        {
 0265            RedirectPage = page,
 0266            Orders = ordersToTransaction.OrderBy(o => o.ShipmentNumber).ToList(),
 0267            User = (await _userCacheService.GetCachedUserAsync(User))!,
 0268            Suppliers = suppliers.OrderBy(s => s.Name).ToList(),
 0269            RateUSD = rates.RateUsd,
 0270            RateEUR = rates.RateEur,
 0271            RateBYN = rates.RateByn,
 0272            UniqueArticles = articles,
 0273            UniqueDeliveryCitys = cities,
 0274            UniqueNumbers = numbers,
 0275            Warehouses = warehouses,
 0276            Treaties = treatyResponses,
 0277            AppStatus = targetStatus,
 0278            ZasylValues = zasylTask.Result
 0279                .Select(w => w.Name)
 0280                .Where(n => !string.IsNullOrWhiteSpace(n))
 0281                .Select(n => n!)
 0282                .ToList(),
 0283            CountryValues = countriesTask.Result
 0284        };
 0285        await AttachTrackingKeyAsync(pageViewModel, TransactionType.ShippedBySupplier);
 286
 0287        if (pageViewModel.User?.AccessProfileId != null)
 0288            pageViewModel.User.AccessProfile = await _context.AccessProfiles.FindAsync(pageViewModel.User.AccessProfileI
 289
 0290        if (pageViewModel.User?.AccessProfile?.AvailableOrderColumns?.Contains("Себестоимость") == true && ordersToTrans
 291        {
 0292            await _orderServices.LoadExtraExpensesForOrders(ordersToTransaction);
 293        }
 294
 0295        RunBackgroundCacheTask(pageViewModel.Orders);
 0296        return View(pageViewModel);
 0297    }
 298
 299    private void RunBackgroundCacheTask(List<Order> orders)
 300    {
 0301        _ = Task.Run(async () =>
 0302        {
 0303            foreach (var order in orders)
 0304            {
 0305                if (string.IsNullOrEmpty(order.EtProducer?.Name) || string.IsNullOrEmpty(order.Article))
 0306                    continue;
 0307                try
 0308                {
 0309                    _ = await _proxyHttpClientService.GetJsonAsync(
 0310                        $"https://api.interparts.ru/detail-full-info/{order.EtProducer.Name}/{order.Article}");
 0311
 0312                    _ = await _proxyHttpClientService.GetJsonAsync(
 0313                        $"https://api.interparts.ru/product-information/product/?article_number={order.Article}&manufact
 0314                }
 0315                catch (Exception)
 0316                {
 0317                    // фоновый прогрев кэша — ошибки некритичны
 0318                }
 0319            }
 0320        });
 0321    }
 322
 323    // ==============================
 324    // POST: CreateShippedBySupplierTransaction
 325    // ==============================
 326    [HttpPost]
 327    [ValidateAntiForgeryToken]
 328    public async Task<IActionResult> CreateShippedBySupplierTransaction(
 329        ShippedBySupplierTransactionViewModel model,
 330        Dictionary<int, string?>? orderSupplierMap,
 331        Dictionary<int, string> orderCountryMap,
 332        Dictionary<int, string> orderGtdMap,
 333        [FromServices] OzonOrdersWeb.WebServices.Transactions.ArmtekReceiptOriginService originService)
 334    {
 0335        var aiFlow = TransactionCommentAiSchedulingHelper.BeginFlow(
 0336            model.Comment ?? string.Empty,
 0337            TransactionType.ShippedBySupplier,
 0338            model.UserName ?? string.Empty,
 0339            model.ProcessIn1C);
 0340        model.Comment = aiFlow.UserCommentOnly;
 341
 0342        var trackingKey = Request.Form["TrackingKey"].FirstOrDefault();
 0343        var oneCOrganizationInn = Request.Form["oneCOrganizationInn"].FirstOrDefault();
 0344        var missingOrdersResult = TransactionErrorResponseHelper.EnsureOrdersProvided(
 0345            this,
 0346            model.Orders,
 0347            () => RedirectToAction("Index"));
 0348        if (missingOrdersResult != null)
 349        {
 0350            return missingOrdersResult;
 351        }
 352
 0353        if (model.Orders != null)
 354        {
 0355            if (model.DeletedOrders != null)
 356            {
 0357                var deletedOrderIds = DeletedOrderIds.Parse(model.DeletedOrders);
 0358                model.Orders = model.Orders.Where(o => !deletedOrderIds.Contains(o.Id)).ToList();
 359            }
 360        }
 361
 0362        if (model.ProcessIn1C && string.IsNullOrWhiteSpace(model.ReceiptWarehouse))
 0363            return BadRequest(new { error = "Не указан склад прихода." });
 364
 365        // Проверяем фактуру до любых изменений статусов; данные клиента не считаем доверенными.
 0366        if (model.ProcessIn1C && model.Orders is { Count: > 0 })
 367        {
 368            // Номер ГТД по умолчанию, вписанный вручную, важнее фактуры: брать его оттуда не обязательно.
 0369            var manualDefaultGtd = orderGtdMap
 0370                .Where(kv => OneCODataConstants.IsDefaultGtd(kv.Value))
 0371                .Select(kv => kv.Key).ToHashSet();
 0372            var origin = await originService.PreviewAsync(model.NumberVh,
 0373                model.Orders.Select(o => o.Id).ToArray(), HttpContext.RequestAborted, manualDefaultGtd);
 0374            if (origin.Applicable)
 375            {
 0376                if (!origin.Ready)
 0377                    return BadRequest(new { error = origin.Error ?? string.Join("; ", origin.Rows.Select(r => $"{r.Artic
 0378                orderCountryMap = origin.Rows.ToDictionary(r => r.OrderId, r => r.Country!);
 0379                orderGtdMap = origin.Rows.ToDictionary(r => r.OrderId, r => r.Gtd!);
 380            }
 381        }
 382
 383        // Очищаем пустые дополнительные расходы перед сохранением
 384        // Это гарантирует, что пустые элементы не попадут в базу данных
 0385        foreach (var order in model.Orders!)
 386        {
 0387            if (order.ExtraExpenses != null && order.ExtraExpenses.Any())
 388            {
 389                // Удаляем все расходы, где не заполнены оба поля (тип и сумма)
 0390                order.ExtraExpenses = order.ExtraExpenses
 0391                    .Where(e => e != null && e.ExpenseTypeId > 0 && e.Amount > 0)
 0392                    .ToList();
 393
 394                // Если список стал пустым, устанавливаем null
 0395                if (!order.ExtraExpenses.Any())
 396                {
 0397                    order.ExtraExpenses = new List<OrderExtraExpense>();
 398                }
 399            }
 400        }
 401
 402        try
 403        {
 0404            if (model.Orders.Count != 0)
 405            {
 0406                DateTime createAt = DateTime.Now;
 0407                var changeCount = 0;
 0408                long transactionId = 0;
 0409                var dateTime = "";
 0410                List<Order> ordersToUpdate = new List<Order>();
 0411                List<string> ordersNotFoundInOzone = [];
 412
 0413                var orderIds = model.Orders.Select(o => o.Id).Distinct().ToList();
 0414                var fromStatusByOrderId = await _context.Orders.AsNoTracking()
 0415                    .Where(o => orderIds.Contains(o.Id))
 0416                    .Select(o => new { o.Id, FromStatusId = o.AppStatusId ?? 0 })
 0417                    .ToDictionaryAsync(x => x.Id, x => x.FromStatusId);
 418
 0419                var submittedFields = SubmittedOrderFields.FromForm(Request.HasFormContentType ? Request.Form : null);
 0420                foreach (var order in model.Orders!)
 421                {
 0422                    var orderToUpdate = await _orderServices.TransactOrder(order, submittedFields.For(order.Id));
 0423                    ordersToUpdate.Add(orderToUpdate);
 424                }
 425
 0426                var postTransitions = await _statusTransitionDataServices.GetByTransactionType(TransactionType.ShippedBy
 427                AppStatus appStatus;
 0428                HashSet<int>? validTargetStatusIds = null;
 0429                var cancelledStatus = await _appStatusServices.GetAppStatusAsync(new AppStatus() { Name = "Отменен" });
 430
 0431                if (postTransitions.Any())
 432                {
 0433                    validTargetStatusIds = postTransitions.Select(t => t.ToStatusId).ToHashSet();
 0434                    appStatus = postTransitions.First().ToStatus!;
 435                }
 436                else
 437                {
 0438                    appStatus = await _appStatusServices.GetAppStatusAsync(new AppStatus() { Name = "Отгружен поставщико
 0439                        ?? new AppStatus { Name = "Отгружен поставщиком" };
 440                }
 441
 0442                var oldStatus = await _appStatusServices.GetAppStatusAsync(new AppStatus() { Name = "Заказан поставщику"
 0443                var backStatus = await _appStatusServices.GetAppStatusAsync(new AppStatus() { Name = "Утерян поставщиком
 0444                if (backStatus == null)
 445                {
 0446                    backStatus = new AppStatus() { Name = "Утерян поставщиком" };
 0447                    _context.AppStatuses.Add(backStatus);
 0448                    await _context.SaveChangesAsync();
 449                }
 450
 0451                List<Order> ordersToTransaction = validTargetStatusIds != null
 0452                    ? ordersToUpdate.Where(o => o.AppStatus != null && validTargetStatusIds.Contains(o.AppStatus.Id) && 
 0453                    : ordersToUpdate.Where(o => ((appStatus != null && o.AppStatus?.Id == appStatus.Id) || (cancelledSta
 0454                List<Order> ordersToBack = ordersToUpdate.Where(o => o.Quantity <= 0).ToList();
 0455                foreach (var order in ordersToBack)
 456                {
 0457                    order.AppStatus = backStatus;
 458                }
 459
 0460                int ordersToBackResult = await _orderServices.UpdateOrders(ordersToBack);
 461
 462
 0463                string oneCResult = "";
 0464                IEnumerable<string> oneCHash = [];
 0465                List<OneCResponse> transferResult = new List<OneCResponse>();
 0466                List<Order> ordersForPosting = new List<Order>();
 0467                if (model.ProcessIn1C)
 468                {
 469                    // Страна/ГТД не используются в OData-приходной — все заказы идут на проведение
 0470                    ordersForPosting = ordersToTransaction.ToList();
 471
 0472                    if (ordersForPosting.Count > 0)
 473                    {
 474                        try
 475                        {
 0476                            transferResult = await _oneCReceiptManager.CreateReceipts(ordersForPosting,
 0477                                new ShippedBySupplierTransactionDto()
 0478                                {
 0479                                    NDS = model.NDS,
 0480                                    ContractLink = model.ContractLink,
 0481                                    NumberVh = model.NumberVh,
 0482                                    DateVh = model.DateVh,
 0483                                    TransactionComment = model.TransactionComment,
 0484                                    VatRate = model.VatRate,
 0485                                    ReceiptWarehouseName = model.ReceiptWarehouse,
 0486                                    OrderCountryMap = orderCountryMap,
 0487                                    OrderGtdMap = orderGtdMap
 0488                                },
 0489                                model.UserName ?? string.Empty,
 0490                                orgInnOverride: oneCOrganizationInn);
 0491                        }
 0492                        catch (Exception e)
 493                        {
 494                            // Исключение при обращении к 1С — добавляем как failed-ответ, НЕ откатываем статус
 0495                            transferResult.Add(new OneCResponse
 0496                            {
 0497                                Success = false,
 0498                                Message = $"Исключение при создании документа 1С: {e.Message}"
 0499                            });
 0500                        }
 501
 0502                        var successfulTransfers = transferResult.Where(tr => tr.Success).ToList();
 0503                        var failedTransfers = transferResult.Where(tr => !tr.Success).ToList();
 504
 0505                        if (failedTransfers.Count > 0)
 506                        {
 507                            // Статус НЕ откатываем — заказ отгружен, просто 1С-документ не создан
 508                            // Пользователь видит предупреждение с конкретными причинами
 0509                            var errorLines = failedTransfers
 0510                                .Select(tr => tr.Message)
 0511                                .Distinct()
 0512                                .Select(m => $"<li class='small'>{System.Net.WebUtility.HtmlEncode(m)}</li>");
 0513                            oneCResult = $"<div class='alert alert-warning'>" +
 0514                                         $"<strong>⚠️ 1С: документ не создан ({failedTransfers.Count} ошибок) — статус з
 0515                                         $"<ul class='mb-0 mt-1'>{string.Join("", errorLines)}</ul>" +
 0516                                         $"</div>";
 517                        }
 518
 0519                        if (successfulTransfers.Count > 0)
 520                        {
 0521                            oneCHash = successfulTransfers
 0522                                .Select(h => h.TransactionId)
 0523                                .Where(id => !string.IsNullOrWhiteSpace(id))
 0524                                .Select(id => id!);
 0525                            oneCResult += OneCTransactionSummaryHelper.BuildAlertSummary(ordersForPosting, transferResul
 526                        }
 527                    }
 528                }
 529                else
 530                {
 0531                    oneCResult =
 0532                        "<div class='alert alert-info'><strong>ℹ️ Информация</strong><br/>Создание документов в 1С не тр
 533                }
 534
 0535                if (ordersToBackResult > 0)
 536                {
 0537                    oneCResult += $"<div class='alert alert-info'>" +
 0538                                  $"<strong>📊 Обновление статусов</strong><br/>" +
 0539                                  $"Статус 'Утерян реализатором' установлен для <strong>{ordersToBackResult}</strong> за
 0540                                  $"</div>";
 541                }
 542
 0543                await NotificationService.NotifyAllAsync(oneCResult);
 0544                var historyWindowStart = DateTime.Now;
 0545                var commentWithOneC = model.Comment ?? string.Empty;
 0546                TransactionCommentAiSchedulingHelper.AppendOneCCommentDetails(
 0547                    aiFlow,
 0548                    ref commentWithOneC,
 0549                    ordersToTransaction,
 0550                    transferResult);
 0551                var commentForDb = TransactionCommentAiSchedulingHelper.ResolveDatabaseComment(aiFlow, commentWithOneC);
 552
 0553                (changeCount, dateTime, transactionId) = await _transaction.CreateShippedBySupplierTransaction(ordersToT
 0554                    model.UserName ?? string.Empty,
 0555                    createAt,
 0556                    commentForDb);
 0557                int cancelCount = ordersToTransaction.Count(o => o.AppStatus?.Name == "Отменен");
 558
 0559                if (changeCount > 0)
 560                {
 0561                    if (model.ProcessIn1C && transferResult.Count > 0 && ordersForPosting.Count > 0)
 562                    {
 0563                        await OrderHistoryOneCPostingRecorder.TryRecordReceiptAsync(
 0564                            HttpContext.RequestServices,
 0565                            ordersForPosting,
 0566                            transferResult,
 0567                            model.UserName ?? string.Empty,
 0568                            transactionId > 0 ? (int?)transactionId : null,
 0569                            historyWindowStart);
 570                    }
 571
 0572                    var statusTransitionHistoryService = HttpContext.RequestServices.GetService<IStatusTransitionHistory
 0573                    if (postTransitions.Any() && statusTransitionHistoryService != null)
 574                    {
 0575                        var user = await _userCacheService.GetCachedUserAsync(User);
 0576                        await statusTransitionHistoryService.RecordStatusTransitionHistoryAsync(
 0577                            TransactionType.ShippedBySupplier,
 0578                            ordersToTransaction,
 0579                            fromStatusByOrderId,
 0580                            postTransitions,
 0581                            commentForDb,
 0582                            user?.AccessProfileId,
 0583                            createAt,
 0584                            null);
 585                    }
 586
 0587                    await _cacheUpdater.Update(_cache);
 0588                    var statusNameForMessage = string.Join(", ", ordersToTransaction.Select(o => o.AppStatus?.Name).Wher
 0589                    if (string.IsNullOrEmpty(statusNameForMessage) && postTransitions.Any())
 0590                        statusNameForMessage = string.Join(", ", postTransitions.Select(t => t.ToStatus?.Name).Where(n =
 0591                    if (string.IsNullOrEmpty(statusNameForMessage)) statusNameForMessage = "Отгружен поставщиком";
 0592                    string msg = TransactionConfig.BuildStandardSuccessMessage(
 0593                        changeCount,
 0594                        cancelCount,
 0595                        dateTime,
 0596                        model.UserName ?? string.Empty,
 0597                        commentForDb,
 0598                        model.Orders,
 0599                        statusNameForMessage,
 0600                        oneCResult);
 0601                    await NotificationService.NotifyAllAsync(msg);
 0602                    TempData["TransactionResult"] = msg;
 0603                    await CompleteTrackingAsync(trackingKey, transactionId, ordersToTransaction.Count, commentForDb);
 604
 0605                    if (aiFlow.ScheduleAiComment && aiFlow.Builder != null && transactionId > 0)
 606                    {
 0607                        await TransactionCommentAiSchedulingHelper.TryScheduleAsync(
 0608                            HttpContext.RequestServices,
 0609                            aiFlow.Builder,
 0610                            true,
 0611                            (int)transactionId,
 0612                            ordersToTransaction,
 0613                            fromStatusByOrderId,
 0614                            _context,
 0615                            ct: HttpContext.RequestAborted);
 616                    }
 0617                }
 618
 0619                await _transactionCache.Update();
 620
 0621                if (ordersNotFoundInOzone.Count > 0)
 622                {
 0623                    TempData["OrdersNotFoundInOzone"] =
 0624                        $"Заказы не найдены в системе Ozon - {ordersNotFoundInOzone.Aggregate((x, y) => x + ", " + y)}";
 625                }
 0626            }
 627
 0628            ClearSelectedIdsSession();
 0629            foreach (var order in model.Orders!)
 630            {
 0631                var cookieKey = $"PurchasePrice_{order.Id}";
 0632                if (Request.Cookies.ContainsKey(cookieKey))
 633                {
 0634                    Response.Cookies.Delete(cookieKey);
 635                }
 636            }
 637
 0638            await _orderServices.SetZasylAsync(orderSupplierMap ?? new Dictionary<int, string?>());
 0639            return RedirectToAction("Index", "Orders", new { sortOrder = GetSortStateCookie(), page = model.Page });
 640        }
 0641        catch (TransactionException ex)
 642        {
 0643            TempData["TransactionErrorCode"] = ex.Code.ToString();
 0644            TempData["ErorrResult"] = ex.Message;
 0645            await _orderServices.SetZasylAsync(orderSupplierMap ?? new Dictionary<int, string?>());
 0646            return RedirectToAction("Index", "Orders", new { sortOrder = GetSortStateCookie(), page = model.Page });
 647        }
 0648        catch (Exception ex)
 649        {
 0650            TempData["TransactionErrorCode"] = TransactionErrorCode.Unknown.ToString();
 0651            TempData["ErorrResult"] = $"Не удалось провести выбранные заказы ({ex.Message})";
 0652            await _orderServices.SetZasylAsync(orderSupplierMap ?? new Dictionary<int, string?>());
 0653            return RedirectToAction("Index", "Orders", new { sortOrder = GetSortStateCookie(), page = model.Page });
 654        }
 0655    }
 656
 657    [HttpPost]
 658    public IActionResult ClearSelectedIdsSession()
 659    {
 0660        HttpContext.Session.SetString("selectedIds", JsonConvert.SerializeObject(new List<int>()));
 0661        return Ok();
 662    }
 663
 664    private async Task AttachTrackingKeyAsync(MultiplayEditOrderViewModel model, TransactionType transactionType)
 665    {
 0666        if (model.Orders == null || !model.Orders.Any())
 0667            return;
 0668        var trackingService = HttpContext.RequestServices.GetService<ITransactionTimeTrackingService>();
 0669        if (trackingService == null)
 0670            return;
 0671        model.TrackingKey = await trackingService.StartSessionAsync(
 0672            User.Identity?.Name ?? "unknown",
 0673            model.User?.AccessProfileId,
 0674            transactionType,
 0675            model.Orders.Select(o => o.Id),
 0676            $"{Request.Path}{Request.QueryString}",
 0677            HttpContext.Connection.RemoteIpAddress?.ToString(),
 0678            Request.Headers.UserAgent.ToString());
 0679    }
 680
 681    private async Task CompleteTrackingAsync(string? trackingKey, long transactionId, int ordersCount, string? comment)
 682    {
 0683        if (string.IsNullOrWhiteSpace(trackingKey) || transactionId <= 0)
 0684            return;
 0685        var trackingService = HttpContext.RequestServices.GetService<ITransactionTimeTrackingService>();
 0686        if (trackingService == null)
 0687            return;
 0688        await trackingService.CompleteSessionAsync(
 0689            trackingKey,
 0690            transactionId,
 0691            ordersCount,
 0692            comment,
 0693            $"/Studio2/Transaction/GetTransaction?transactionId={transactionId}");
 0694    }
 695
 696    private OrderSortState GetSortStateCookie()
 697    {
 0698        var cookie = Request.Cookies["SortState"];
 0699        if (!string.IsNullOrEmpty(cookie) && Enum.TryParse<OrderSortState>(cookie, out var saved))
 0700            return saved;
 0701        return OrderSortState.StandardState;
 702    }
 703
 704    [ServiceEndpoint]
 705    [HttpGet]
 706    public async Task<IActionResult> SearchGtd(string? query, int limit = 20)
 707    {
 0708        var result = await _oneCDataManager.SearchGtdCodesAsync(query, limit);
 0709        return Json(result);
 0710    }
 711
 712    [HttpGet]
 713    [PageAuthorize("Settlements.Armtek.Index", OzonDomains.Models.Authorization.Enums.AccessType.View)]
 714    public async Task<IActionResult> PreviewInvoiceOrigin(string? number, [FromQuery] int[] ids, [FromQuery] int[] defau
 715        [FromServices] OzonOrdersWeb.WebServices.Transactions.ArmtekReceiptOriginService service,
 716        CancellationToken ct)
 717    {
 0718        if (ids.Length == 0 || ids.Length > 200 || number?.Length > 100)
 0719            return BadRequest();
 0720        return Json(await service.PreviewAsync(number, ids, ct, defaultGtd.ToHashSet()));
 0721    }
 722
 723    /// <summary>
 724    /// Подсказки для «Номер вх. документа»: фактуры Армтека, где найдены артикулы выбранных заказов (поиск без разделит
 725    /// </summary>
 726    [HttpGet]
 727    [PageAuthorize("Settlements.Armtek.Index", OzonDomains.Models.Authorization.Enums.AccessType.View)]
 728    public async Task<IActionResult> SuggestInvoiceNumbers([FromQuery] int[] ids,
 729        [FromServices] OzonOrdersWeb.WebServices.Transactions.ArmtekInvoiceNumberSuggestService service,
 730        CancellationToken ct)
 731    {
 0732        if (ids.Length == 0 || ids.Length > 200)
 0733            return BadRequest();
 0734        return Json(await service.SuggestAsync(ids, ct));
 0735    }
 736
 737    /// <summary>
 738    /// Страна происхождения по артикулам выбранных заказов из позиций фактур Армтека в написании справочника 1С.
 739    /// </summary>
 740    [HttpGet]
 741    [PageAuthorize("Settlements.Armtek.Index", OzonDomains.Models.Authorization.Enums.AccessType.View)]
 742    public async Task<IActionResult> SuggestCountries([FromQuery] int[] ids,
 743        [FromServices] OzonOrdersWeb.WebServices.Transactions.ArmtekInvoiceNumberSuggestService service,
 744        CancellationToken ct)
 745    {
 0746        if (ids.Length == 0 || ids.Length > 200)
 0747            return BadRequest();
 0748        var countries = await _oneCDataManager.GetCountryListAsync();
 0749        return Json(await service.SuggestCountriesAsync(ids, countries, ct));
 0750    }
 751
 752    /// <summary>
 753    /// Снимает проведение со всех тестовых документов 1С (содержащих [ТЕСТ] в комментарии).
 754    /// Используется только в development/testing. URL: /Studio2/ShippedBySupplierTransaction/CancelTestPostings
 755    /// </summary>
 756    [HttpPost]
 757    [Authorize(Roles = "Admin")]
 758    public async Task<IActionResult> CancelTestPostings()
 759    {
 0760        var log = await _oneCDataManager.CancelTestPostingsAsync();
 0761        return Json(new { success = true, log });
 0762    }
 763}