| | | 1 | | using Microsoft.AspNetCore.Authorization; |
| | | 2 | | using Microsoft.AspNetCore.Mvc; |
| | | 3 | | using Microsoft.AspNetCore.Mvc.Rendering; |
| | | 4 | | using Microsoft.Extensions.DependencyInjection; |
| | | 5 | | using Newtonsoft.Json; |
| | | 6 | | using OzonDomains; |
| | | 7 | | using OzonDomains.Models; |
| | | 8 | | using OzonOrdersWeb.Areas.Studio2.Controllers.Base; |
| | | 9 | | using OzonOrdersWeb.Areas.Studio2.Helpers; |
| | | 10 | | using OzonOrdersWeb.Areas.Studio2.Models.Transactions; |
| | | 11 | | using Services.BackgroundJobsServices; |
| | | 12 | | using OzonOrdersWeb.ViewModels.OrderViewModels; |
| | | 13 | | using OzonRepositories.Context; |
| | | 14 | | using OzonRepositories.Data.Bitrix; |
| | | 15 | | using PartsInfo.HttpUtils; |
| | | 16 | | using System.Security.Claims; |
| | | 17 | | using Microsoft.EntityFrameworkCore; |
| | | 18 | | using Services.ApiServices; |
| | | 19 | | using Services.ApiServices._1CApi; |
| | | 20 | | using Services.ApiServices._1CApi.Models; |
| | | 21 | | using Services.ApiServices._1CApi.Stock; |
| | | 22 | | using Services.Authorization.Services; |
| | | 23 | | using Services.CacheServices.Cache.OzonOrdersCache; |
| | | 24 | | using Services.CacheServices.Cache.UserCacheService; |
| | | 25 | | using Services.DataServices; |
| | | 26 | | using Services.FiltersServices.SortModels; |
| | | 27 | | using OzonOrdersWeb.WebServices.Currency; |
| | | 28 | | using Services.SignalRServices; |
| | | 29 | | using Services.TransactionUtilsServices; |
| | | 30 | | using OzonOrdersWeb.TransactionServices; |
| | | 31 | | using Services.CacheServices.Cache; |
| | | 32 | | |
| | | 33 | | namespace OzonOrdersWeb.Areas.Studio2.Controllers.Transactions |
| | | 34 | | { |
| | | 35 | | /// <summary> |
| | | 36 | | /// Контроллер для транзакции "Перемещение между складами" |
| | | 37 | | /// Позволяет выполнить перемещение товаров с явным указанием складов отправителя и получателя |
| | | 38 | | /// </summary> |
| | | 39 | | [Authorize(Roles = "User,Admin")] |
| | | 40 | | [Area("Studio2")] |
| | | 41 | | [Route("Studio2/WarehouseTransferTransaction")] |
| | | 42 | | public class WarehouseTransferTransactionController : Controller |
| | | 43 | | { |
| | | 44 | | private readonly ApplicationDbContext _context; |
| | | 45 | | private readonly AppStatusDataServices _appStatusServices; |
| | | 46 | | private readonly OrdersDataServices _orderServices; |
| | | 47 | | private readonly TransactionManager _transaction; |
| | | 48 | | private readonly TransactionCache _transactionCache; |
| | | 49 | | private readonly SupplierDataServices _supplierDataServices; |
| | | 50 | | private readonly IUserCacheService _userCacheService; |
| | | 51 | | private readonly ICurrencyRatesViewBagService _currencyRatesViewBagService; |
| | | 52 | | private readonly CacheUpdater<Order> _cacheUpdater; |
| | | 53 | | private readonly OrderCache _cache; |
| | | 54 | | private readonly OneCTransferManager _oneCTransferManager; |
| | | 55 | | private readonly OneCDataManager _oneCDataManager; |
| | | 56 | | private readonly ProxyHttpClientService _proxyHttpClientService; |
| | | 57 | | private readonly WarehouseDataServices _warehouseDataServices; |
| | | 58 | | private readonly StatusTransitionDataServices _statusTransitionDataServices; |
| | | 59 | | private readonly IScopeFilterValuesService _scopeFilterValuesService; |
| | | 60 | | private readonly WarehouseSelectionResolver _warehouseSelectionResolver; |
| | | 61 | | |
| | 0 | 62 | | public WarehouseTransferTransactionController( |
| | 0 | 63 | | ApplicationDbContext context, |
| | 0 | 64 | | AppStatusDataServices appStatusServices, |
| | 0 | 65 | | OrdersDataServices orderServices, |
| | 0 | 66 | | TransactionManager transaction, |
| | 0 | 67 | | TransactionCache transactionCache, |
| | 0 | 68 | | SupplierDataServices supplierDataServices, |
| | 0 | 69 | | IUserCacheService userCacheService, |
| | 0 | 70 | | ICurrencyRatesViewBagService currencyRatesViewBagService, |
| | 0 | 71 | | CacheUpdater<Order> cacheUpdater, |
| | 0 | 72 | | OrderCache cache, |
| | 0 | 73 | | OneCTransferManager oneCTransferManager, |
| | 0 | 74 | | OneCDataManager oneCDataManager, |
| | 0 | 75 | | ProxyHttpClientService proxyHttpClientService, |
| | 0 | 76 | | WarehouseDataServices warehouseDataServices, |
| | 0 | 77 | | StatusTransitionDataServices statusTransitionDataServices, |
| | 0 | 78 | | IScopeFilterValuesService scopeFilterValuesService, |
| | 0 | 79 | | WarehouseSelectionResolver warehouseSelectionResolver) |
| | | 80 | | { |
| | 0 | 81 | | _context = context; |
| | 0 | 82 | | _appStatusServices = appStatusServices; |
| | 0 | 83 | | _orderServices = orderServices; |
| | 0 | 84 | | _transaction = transaction; |
| | 0 | 85 | | _transactionCache = transactionCache; |
| | 0 | 86 | | _supplierDataServices = supplierDataServices; |
| | 0 | 87 | | _userCacheService = userCacheService; |
| | 0 | 88 | | _currencyRatesViewBagService = currencyRatesViewBagService; |
| | 0 | 89 | | _cacheUpdater = cacheUpdater; |
| | 0 | 90 | | _cache = cache; |
| | 0 | 91 | | _oneCTransferManager = oneCTransferManager; |
| | 0 | 92 | | _oneCDataManager = oneCDataManager; |
| | 0 | 93 | | _proxyHttpClientService = proxyHttpClientService; |
| | 0 | 94 | | _warehouseDataServices = warehouseDataServices; |
| | 0 | 95 | | _statusTransitionDataServices = statusTransitionDataServices; |
| | 0 | 96 | | _scopeFilterValuesService = scopeFilterValuesService; |
| | 0 | 97 | | _warehouseSelectionResolver = warehouseSelectionResolver; |
| | 0 | 98 | | } |
| | | 99 | | |
| | | 100 | | // ============================== |
| | | 101 | | // GET: CreateWarehouseTransferTransaction |
| | | 102 | | // ============================== |
| | | 103 | | [HttpGet] |
| | | 104 | | [Route("CreateWarehouseTransferTransaction")] |
| | | 105 | | public async Task<IActionResult> CreateWarehouseTransferTransaction(string ids, int page) |
| | | 106 | | { |
| | 0 | 107 | | var transitions = |
| | 0 | 108 | | await _statusTransitionDataServices.GetByTransactionType(TransactionType.WarehouseTransfer); |
| | | 109 | | AppStatus appStatus; |
| | 0 | 110 | | HashSet<int>? validSourceStatusIds = null; |
| | | 111 | | |
| | 0 | 112 | | if (transitions.Any()) |
| | | 113 | | { |
| | 0 | 114 | | validSourceStatusIds = transitions.Select(t => t.FromStatusId).ToHashSet(); |
| | 0 | 115 | | appStatus = transitions.First().ToStatus!; |
| | | 116 | | } |
| | | 117 | | else |
| | | 118 | | { |
| | 0 | 119 | | var appStatusWarehouseTransfer = |
| | 0 | 120 | | await _appStatusServices.GetAppStatusAsync(new AppStatus { Name = "Перемещен между складами" }); |
| | 0 | 121 | | if (appStatusWarehouseTransfer == null) |
| | | 122 | | { |
| | 0 | 123 | | appStatusWarehouseTransfer = new AppStatus { Name = "Перемещен между складами" }; |
| | 0 | 124 | | _context.AppStatuses.Add(appStatusWarehouseTransfer); |
| | 0 | 125 | | await _context.SaveChangesAsync(); |
| | | 126 | | } |
| | | 127 | | |
| | 0 | 128 | | appStatus = appStatusWarehouseTransfer; |
| | 0 | 129 | | } |
| | | 130 | | |
| | 0 | 131 | | var cancelledStatus = await _appStatusServices.GetAppStatusAsync(new AppStatus { Name = "Отменен" }); |
| | 0 | 132 | | var filteredStatuses = new List<AppStatus> { appStatus }; |
| | 0 | 133 | | if (cancelledStatus != null) filteredStatuses.Add(cancelledStatus); |
| | 0 | 134 | | filteredStatuses = filteredStatuses.GroupBy(s => s.Id).Select(g => g.First()).ToList(); |
| | | 135 | | |
| | 0 | 136 | | ViewBag.AppStatuses = new SelectList(filteredStatuses, "Id", "Name"); |
| | 0 | 137 | | ViewBag.StatusColors = filteredStatuses.ToDictionary(s => s.Id.ToString(), s => s.GetStatusColor()); |
| | | 138 | | |
| | 0 | 139 | | var bitrixWarehouses = await _oneCDataManager.GetWarehouseListAsync(); |
| | 0 | 140 | | var bitrixWarehouseNames = (await FilterBitrixWarehouseNamesByScope( |
| | 0 | 141 | | bitrixWarehouses |
| | 0 | 142 | | .Select(w => w.Name) |
| | 0 | 143 | | .Where(n => !string.IsNullOrWhiteSpace(n)) |
| | 0 | 144 | | .Select(n => n!) |
| | 0 | 145 | | .OrderBy(n => n) |
| | 0 | 146 | | .ToList())).ToList(); |
| | 0 | 147 | | ViewBag.BitrixWarehouses = new SelectList(bitrixWarehouseNames); |
| | 0 | 148 | | ViewData["BitrixWarehousesList"] = bitrixWarehouseNames; |
| | | 149 | | |
| | 0 | 150 | | if (string.IsNullOrWhiteSpace(ids)) |
| | 0 | 151 | | return BadRequest("Не выбраны заказы для транзакции."); |
| | | 152 | | |
| | 0 | 153 | | int[] idArray = ids.Split(',', StringSplitOptions.RemoveEmptyEntries).Select(int.Parse).ToArray(); |
| | | 154 | | |
| | 0 | 155 | | if (idArray.Length == 0) |
| | 0 | 156 | | return BadRequest("Не выбраны заказы для транзакции."); |
| | | 157 | | |
| | 0 | 158 | | List<Order> ordersToTransaction = new(); |
| | 0 | 159 | | string? errorReason = null; |
| | 0 | 160 | | var errorCode = TransactionErrorCode.Unknown; |
| | | 161 | | |
| | | 162 | | // ВАЖНО: Загружаем заказы БЕЗ ТРЕКИНГА - изменения статусов для отображения |
| | | 163 | | // НЕ сохранятся в БД при отмене транзакции |
| | 0 | 164 | | var orders = await _orderServices.GetOrdersNoTracking(idArray); |
| | | 165 | | |
| | 0 | 166 | | foreach (var order in orders) |
| | | 167 | | { |
| | 0 | 168 | | bool notCancelled = order.Status != "Отменен покупателем" |
| | 0 | 169 | | && !(order.AppStatus?.Name == "Отменен" && order.Status == "Отменён"); |
| | 0 | 170 | | bool allowedByStatus = validSourceStatusIds == null || |
| | 0 | 171 | | (order.AppStatusId.HasValue && |
| | 0 | 172 | | validSourceStatusIds.Contains(order.AppStatusId.Value)); |
| | | 173 | | |
| | 0 | 174 | | if (notCancelled && allowedByStatus) |
| | | 175 | | { |
| | 0 | 176 | | ordersToTransaction.Add(order); |
| | | 177 | | } |
| | | 178 | | } |
| | | 179 | | |
| | 0 | 180 | | if (!ordersToTransaction.Any()) |
| | | 181 | | { |
| | 0 | 182 | | errorReason = "Нет заказов, доступных для перемещения между складами."; |
| | 0 | 183 | | errorCode = TransactionErrorCode.NoOrdersForTransaction; |
| | | 184 | | } |
| | | 185 | | |
| | 0 | 186 | | var isAdmin = User.IsInRole("Admin"); |
| | 0 | 187 | | ViewData["ForceSenderWarehouse"] = !isAdmin; |
| | 0 | 188 | | var senderResolution = await _warehouseSelectionResolver.ResolveSenderWarehouseAsync(ordersToTransaction); |
| | 0 | 189 | | var warehouseBlockingErrors = new List<string>(); |
| | 0 | 190 | | if (!string.IsNullOrWhiteSpace(senderResolution.SenderWarehouseName)) |
| | | 191 | | { |
| | 0 | 192 | | ViewData["FixedSenderWarehouse"] = senderResolution.SenderWarehouseName; |
| | 0 | 193 | | ViewData["PreferredSenderWarehouse"] = senderResolution.SenderWarehouseName; |
| | | 194 | | |
| | 0 | 195 | | if (!bitrixWarehouseNames.Contains(senderResolution.SenderWarehouseName, StringComparer.OrdinalIgnoreCas |
| | | 196 | | { |
| | 0 | 197 | | warehouseBlockingErrors.Add( |
| | 0 | 198 | | $"Склад отправителя '{senderResolution.SenderWarehouseName}', полученный из настроек клиента Ozo |
| | | 199 | | } |
| | 0 | 200 | | else if (!bitrixWarehouseNames.Contains(senderResolution.SenderWarehouseName, StringComparer.OrdinalIgno |
| | | 201 | | { |
| | 0 | 202 | | ViewData["WarehouseSenderRuleError"] = |
| | 0 | 203 | | $"Автоопределенный склад отправителя '{senderResolution.SenderWarehouseName}' недоступен по теку |
| | 0 | 204 | | ViewData["WarehouseSenderRuleSeverity"] = "soft"; |
| | | 205 | | } |
| | | 206 | | } |
| | 0 | 207 | | else if (senderResolution.HasErrors && !isAdmin) |
| | | 208 | | { |
| | 0 | 209 | | warehouseBlockingErrors.AddRange(senderResolution.Errors); |
| | | 210 | | } |
| | 0 | 211 | | else if (senderResolution.HasErrors && isAdmin) |
| | | 212 | | { |
| | 0 | 213 | | ViewData["WarehouseSenderRuleError"] = |
| | 0 | 214 | | "Автоопределение склада отправителя не выполнено. Выберите склад отправителя вручную из списка."; |
| | 0 | 215 | | ViewData["WarehouseSenderRuleSeverity"] = "soft"; |
| | | 216 | | } |
| | | 217 | | |
| | | 218 | | // Для не-админа получатель: в приоритете склады из WarehouseMapping по (клиент + склад заказа), |
| | | 219 | | // но также разрешаем выбрать любой другой доступный склад. |
| | 0 | 220 | | var recipientResolution = await _warehouseSelectionResolver.ResolveRecipientWarehousesAsync(ordersToTransact |
| | 0 | 221 | | if (recipientResolution.Warnings.Count > 0) |
| | | 222 | | { |
| | 0 | 223 | | ViewData["WarehouseRecipientRuleInfo"] = string.Join("<br/>", recipientResolution.Warnings); |
| | | 224 | | } |
| | 0 | 225 | | if (recipientResolution.HasBlockingErrors) |
| | | 226 | | { |
| | 0 | 227 | | warehouseBlockingErrors.AddRange(recipientResolution.BlockingErrors); |
| | | 228 | | } |
| | | 229 | | |
| | 0 | 230 | | var preferredSet = new HashSet<string>(StringComparer.OrdinalIgnoreCase); |
| | 0 | 231 | | if (recipientResolution.PreferredRecipientWarehouses.Count > 0) |
| | | 232 | | { |
| | 0 | 233 | | preferredSet = recipientResolution.PreferredRecipientWarehouses |
| | 0 | 234 | | .ToHashSet(StringComparer.OrdinalIgnoreCase); |
| | | 235 | | |
| | 0 | 236 | | bitrixWarehouseNames = bitrixWarehouseNames |
| | 0 | 237 | | .OrderBy(n => preferredSet.Contains(n) ? 0 : 1) |
| | 0 | 238 | | .ThenBy(n => n) |
| | 0 | 239 | | .ToList(); |
| | | 240 | | } |
| | | 241 | | |
| | 0 | 242 | | if (!isAdmin) |
| | | 243 | | { |
| | 0 | 244 | | if (senderResolution.HasErrors) |
| | | 245 | | { |
| | 0 | 246 | | ViewData["WarehouseSenderRuleError"] = string.Join("<br/>", senderResolution.Errors); |
| | | 247 | | } |
| | | 248 | | |
| | | 249 | | // Дополнительно убираем из адресатов выбранный источник, если он определился. |
| | 0 | 250 | | if (!string.IsNullOrWhiteSpace(senderResolution.SenderWarehouseName)) |
| | | 251 | | { |
| | 0 | 252 | | bitrixWarehouseNames = bitrixWarehouseNames |
| | 0 | 253 | | .Where(n => !string.Equals(n, senderResolution.SenderWarehouseName, StringComparison.OrdinalIgno |
| | 0 | 254 | | .OrderBy(n => n) |
| | 0 | 255 | | .ToList(); |
| | 0 | 256 | | ViewData["BitrixWarehousesList"] = bitrixWarehouseNames; |
| | | 257 | | } |
| | | 258 | | |
| | | 259 | | } |
| | | 260 | | |
| | | 261 | | // Подставляем первый доступный приоритетный склад получателя (для админа и не-админа). |
| | 0 | 262 | | var preferredRecipient = bitrixWarehouseNames |
| | 0 | 263 | | .FirstOrDefault(n => preferredSet.Contains(n)); |
| | 0 | 264 | | if (!string.IsNullOrWhiteSpace(preferredRecipient)) |
| | | 265 | | { |
| | 0 | 266 | | ViewData["PreferredRecipientWarehouse"] = preferredRecipient; |
| | | 267 | | } |
| | | 268 | | |
| | 0 | 269 | | if (warehouseBlockingErrors.Count > 0) |
| | | 270 | | { |
| | 0 | 271 | | ViewData["WarehouseSelectionBlocked"] = true; |
| | 0 | 272 | | ViewData["WarehouseSenderRuleError"] = string.Join("<br/>", |
| | 0 | 273 | | warehouseBlockingErrors.Distinct(StringComparer.OrdinalIgnoreCase)); |
| | | 274 | | } |
| | | 275 | | |
| | 0 | 276 | | ViewData["BitrixWarehousesList"] = bitrixWarehouseNames; |
| | | 277 | | |
| | 0 | 278 | | ViewData["TransactionErrorCode"] = errorCode.ToString(); |
| | 0 | 279 | | ViewData["ErrorReason"] = errorReason; |
| | | 280 | | |
| | 0 | 281 | | var conditionsMessage = transitions.Any() |
| | 0 | 282 | | ? string.Join("<br/>", transitions |
| | 0 | 283 | | .Select(t => t.Conditions) |
| | 0 | 284 | | .Where(c => !string.IsNullOrWhiteSpace(c)) |
| | 0 | 285 | | .Select(c => System.Net.WebUtility.HtmlEncode(c!.Trim())) |
| | 0 | 286 | | .Distinct()) |
| | 0 | 287 | | : null; |
| | 0 | 288 | | ViewData["TransactionConditionsMessage"] = conditionsMessage; |
| | | 289 | | |
| | | 290 | | // === ОПТИМИЗАЦИЯ: non-DB параллельно, DB последовательно === |
| | 0 | 291 | | var userTask = _userCacheService.GetCachedUserAsync(User); |
| | 0 | 292 | | var rateTasks = _currencyRatesViewBagService.StartFetch(); |
| | | 293 | | |
| | 0 | 294 | | var suppliers = await _supplierDataServices.GetSuppliers(); |
| | 0 | 295 | | var articles = await _orderServices.GetArticleCountsForOrdersAsync(ordersToTransaction); |
| | 0 | 296 | | var cities = await _orderServices.GetDeliveryCityCountsForOrdersAsync(ordersToTransaction); |
| | 0 | 297 | | var numbers = await _orderServices.GetShipmentBaseCountsForOrdersAsync(ordersToTransaction); |
| | 0 | 298 | | var warehouses = await _warehouseDataServices.GetWarehouses(); |
| | | 299 | | |
| | 0 | 300 | | await Task.WhenAll(userTask, rateTasks.Usd, rateTasks.Eur, rateTasks.Byn); |
| | | 301 | | |
| | 0 | 302 | | var rates = rateTasks.ToSnapshot(); |
| | 0 | 303 | | var pageViewModel = new MultiplayEditOrderViewModel |
| | 0 | 304 | | { |
| | 0 | 305 | | RedirectPage = page, |
| | 0 | 306 | | Orders = ordersToTransaction.OrderBy(o => o.ShipmentNumber).ToList(), |
| | 0 | 307 | | User = userTask.Result!, |
| | 0 | 308 | | Suppliers = suppliers.OrderBy(s => s.Name).ToList(), |
| | 0 | 309 | | RateUSD = rates.RateUsd, |
| | 0 | 310 | | RateEUR = rates.RateEur, |
| | 0 | 311 | | RateBYN = rates.RateByn, |
| | 0 | 312 | | UniqueArticles = articles, |
| | 0 | 313 | | UniqueDeliveryCitys = cities, |
| | 0 | 314 | | UniqueNumbers = numbers, |
| | 0 | 315 | | AppStatus = appStatus, |
| | 0 | 316 | | Warehouses = warehouses |
| | 0 | 317 | | }; |
| | 0 | 318 | | await AttachTrackingKeyAsync(pageViewModel, TransactionType.WarehouseTransfer); |
| | | 319 | | |
| | 0 | 320 | | if (pageViewModel.User.AccessProfileId != null) |
| | | 321 | | { |
| | 0 | 322 | | pageViewModel.User.AccessProfile = _context.AccessProfiles.Find(pageViewModel.User.AccessProfileId); |
| | | 323 | | } |
| | | 324 | | |
| | 0 | 325 | | if (pageViewModel.User.AccessProfile?.AvailableOrderColumns?.Contains("Себестоимость") == true && ordersToTr |
| | | 326 | | { |
| | 0 | 327 | | await _orderServices.LoadExtraExpensesForOrders(ordersToTransaction); |
| | | 328 | | } |
| | | 329 | | |
| | 0 | 330 | | RunBackgroundCacheTask(pageViewModel.Orders); |
| | 0 | 331 | | return View(pageViewModel); |
| | 0 | 332 | | } |
| | | 333 | | |
| | | 334 | | private void RunBackgroundCacheTask(List<Order> orders) |
| | | 335 | | { |
| | 0 | 336 | | if (_proxyHttpClientService == null) return; |
| | | 337 | | |
| | 0 | 338 | | _ = Task.Run(async () => |
| | 0 | 339 | | { |
| | 0 | 340 | | foreach (var order in orders) |
| | 0 | 341 | | { |
| | 0 | 342 | | try |
| | 0 | 343 | | { |
| | 0 | 344 | | if (order.EtProducer != null && !string.IsNullOrEmpty(order.Article)) |
| | 0 | 345 | | { |
| | 0 | 346 | | await _proxyHttpClientService.GetJsonAsync( |
| | 0 | 347 | | $"https://api.interparts.ru/detail-full-info/{order.EtProducer.Name}/{order.Article}"); |
| | 0 | 348 | | await _proxyHttpClientService.GetJsonAsync( |
| | 0 | 349 | | $"https://api.interparts.ru/product-information/product/?article_number={order.Article}& |
| | 0 | 350 | | } |
| | 0 | 351 | | } |
| | 0 | 352 | | catch |
| | 0 | 353 | | { |
| | 0 | 354 | | /* Игнорируем ошибки фонового кэширования */ |
| | 0 | 355 | | } |
| | 0 | 356 | | } |
| | 0 | 357 | | }); |
| | 0 | 358 | | } |
| | | 359 | | |
| | | 360 | | // ============================== |
| | | 361 | | // POST: CreateWarehouseTransferTransaction |
| | | 362 | | // ============================== |
| | | 363 | | [HttpPost] |
| | | 364 | | [ValidateAntiForgeryToken] |
| | | 365 | | [Route("CreateWarehouseTransferTransaction")] |
| | | 366 | | public async Task<IActionResult> CreateWarehouseTransferTransaction( |
| | | 367 | | List<Order> orders, |
| | | 368 | | string userName, |
| | | 369 | | string? comment, |
| | | 370 | | int page, |
| | | 371 | | string? deletedOrders, |
| | | 372 | | string warehouseSender, |
| | | 373 | | string warehouseRecipient, |
| | | 374 | | bool processIn1C = false) |
| | | 375 | | { |
| | 0 | 376 | | var aiFlow = TransactionCommentAiSchedulingHelper.BeginFlow( |
| | 0 | 377 | | comment, |
| | 0 | 378 | | TransactionType.WarehouseTransfer, |
| | 0 | 379 | | userName, |
| | 0 | 380 | | processIn1C); |
| | 0 | 381 | | comment = aiFlow.UserCommentOnly; |
| | 0 | 382 | | var trackingKey = Request.Form["TrackingKey"].FirstOrDefault(); |
| | 0 | 383 | | var oneCOrganizationInn = Request.Form["oneCOrganizationInn"].FirstOrDefault(); |
| | 0 | 384 | | var missingOrdersResult = TransactionErrorResponseHelper.EnsureOrdersProvided( |
| | 0 | 385 | | this, |
| | 0 | 386 | | orders, |
| | 0 | 387 | | () => RedirectToAction("Index", "Orders")); |
| | 0 | 388 | | if (missingOrdersResult != null) |
| | | 389 | | { |
| | 0 | 390 | | return missingOrdersResult; |
| | | 391 | | } |
| | | 392 | | |
| | 0 | 393 | | ArgumentNullException.ThrowIfNull(orders); |
| | | 394 | | |
| | | 395 | | // Удаляем исключенные заказы |
| | 0 | 396 | | if (!string.IsNullOrEmpty(deletedOrders)) |
| | | 397 | | { |
| | 0 | 398 | | var deletedOrderIds = DeletedOrderIds.Parse(deletedOrders); |
| | 0 | 399 | | orders = orders.Where(o => !deletedOrderIds.Contains(o.Id)).ToList(); |
| | | 400 | | } |
| | | 401 | | |
| | 0 | 402 | | var stockCheck = StockCheckResult.Empty; |
| | | 403 | | |
| | | 404 | | // Валидация складов |
| | 0 | 405 | | if (processIn1C) |
| | | 406 | | { |
| | 0 | 407 | | var bitrixWarehouses = await _oneCDataManager.GetWarehouseListAsync(); |
| | 0 | 408 | | var bitrixWarehouseNames = (await FilterBitrixWarehouseNamesByScope( |
| | 0 | 409 | | bitrixWarehouses |
| | 0 | 410 | | .Select(w => w.Name) |
| | 0 | 411 | | .Where(n => !string.IsNullOrWhiteSpace(n)) |
| | 0 | 412 | | .Select(n => n!) |
| | 0 | 413 | | .OrderBy(n => n) |
| | 0 | 414 | | .ToList())).ToList(); |
| | | 415 | | |
| | 0 | 416 | | var isAdmin = User.IsInRole("Admin"); |
| | 0 | 417 | | var senderResolution = await _warehouseSelectionResolver.ResolveSenderWarehouseAsync(orders); |
| | 0 | 418 | | var recipientResolution = await _warehouseSelectionResolver.ResolveRecipientWarehousesAsync(orders); |
| | 0 | 419 | | var blockingErrors = new List<string>(); |
| | | 420 | | |
| | 0 | 421 | | if (!isAdmin && (senderResolution.HasErrors || string.IsNullOrWhiteSpace(senderResolution.SenderWarehous |
| | | 422 | | { |
| | 0 | 423 | | blockingErrors.AddRange(senderResolution.Errors); |
| | 0 | 424 | | if (!senderResolution.HasErrors) |
| | | 425 | | { |
| | 0 | 426 | | blockingErrors.Add("Не удалось определить склад отправителя по правилу 'склад покупателя'."); |
| | | 427 | | } |
| | | 428 | | } |
| | 0 | 429 | | else if (!isAdmin && !string.IsNullOrWhiteSpace(senderResolution.SenderWarehouseName) && |
| | 0 | 430 | | !bitrixWarehouseNames.Contains(senderResolution.SenderWarehouseName, StringComparer.OrdinalIgno |
| | | 431 | | { |
| | 0 | 432 | | blockingErrors.Add( |
| | 0 | 433 | | $"Склад отправителя '{senderResolution.SenderWarehouseName}', полученный из настроек клиента Ozo |
| | | 434 | | } |
| | | 435 | | |
| | 0 | 436 | | if (blockingErrors.Count > 0) |
| | | 437 | | { |
| | 0 | 438 | | throw new TransactionException( |
| | 0 | 439 | | TransactionErrorCode.InvalidSourceStatus, |
| | 0 | 440 | | string.Join("<br/>", blockingErrors.Distinct(StringComparer.OrdinalIgnoreCase))); |
| | | 441 | | } |
| | | 442 | | |
| | 0 | 443 | | if (!isAdmin) |
| | | 444 | | { |
| | 0 | 445 | | warehouseSender = senderResolution.SenderWarehouseName ?? string.Empty; |
| | | 446 | | } |
| | | 447 | | |
| | 0 | 448 | | if (string.IsNullOrEmpty(warehouseSender)) |
| | | 449 | | { |
| | 0 | 450 | | throw new TransactionException( |
| | 0 | 451 | | TransactionErrorCode.OneCSenderWarehouseMissing, |
| | 0 | 452 | | "Не указан склад отправителя."); |
| | | 453 | | } |
| | | 454 | | |
| | 0 | 455 | | if (!bitrixWarehouseNames.Contains(warehouseSender, StringComparer.OrdinalIgnoreCase)) |
| | | 456 | | { |
| | 0 | 457 | | throw new TransactionException( |
| | 0 | 458 | | TransactionErrorCode.OneCSenderWarehouseMissing, |
| | 0 | 459 | | "Склад отправителя недоступен в списке складов 1С или по текущим правам."); |
| | | 460 | | } |
| | | 461 | | |
| | 0 | 462 | | if (string.IsNullOrEmpty(warehouseRecipient)) |
| | | 463 | | { |
| | 0 | 464 | | throw new TransactionException( |
| | 0 | 465 | | TransactionErrorCode.OneCRecipientWarehouseMissing, |
| | 0 | 466 | | "Не указан склад получателя."); |
| | | 467 | | } |
| | | 468 | | |
| | 0 | 469 | | if (warehouseSender == warehouseRecipient) |
| | | 470 | | { |
| | 0 | 471 | | throw new TransactionException( |
| | 0 | 472 | | TransactionErrorCode.OneCWarehousesMustDiffer, |
| | 0 | 473 | | "Склад отправителя и получателя не могут совпадать."); |
| | | 474 | | } |
| | | 475 | | |
| | | 476 | | // Остатки спрашиваем до изменения статусов: при нехватке 1С создаст документ, но |
| | | 477 | | // откажет в проведении, а сообщение об отказе не содержит проблемных позиций. |
| | 0 | 478 | | var stockOrders = await TransactionStockCheckLineBuilder.LoadForStockCheckAsync( |
| | 0 | 479 | | _context, orders.Select(o => o.Id)); |
| | | 480 | | |
| | 0 | 481 | | stockCheck = await OneCStockGate.RunAsync( |
| | 0 | 482 | | HttpContext, |
| | 0 | 483 | | TransactionStockCheckLineBuilder.FromExplicitWarehouse( |
| | 0 | 484 | | stockOrders, |
| | 0 | 485 | | warehouseSender, |
| | 0 | 486 | | TransactionStockCheckLineBuilder.QuantitiesFromForm(orders))); |
| | 0 | 487 | | await OneCStockGate.ReportAsync(HttpContext, stockCheck, "Перемещение между складами"); |
| | | 488 | | |
| | 0 | 489 | | } |
| | | 490 | | |
| | | 491 | | try |
| | | 492 | | { |
| | 0 | 493 | | if (orders.Count != 0) |
| | | 494 | | { |
| | 0 | 495 | | DateTime createAt = DateTime.Now; |
| | 0 | 496 | | int changeCount = 0; |
| | 0 | 497 | | long transactionId = 0; |
| | 0 | 498 | | string dateTime = ""; |
| | 0 | 499 | | List<Order> ordersToUpdate = new(); |
| | | 500 | | |
| | | 501 | | // Сохраняем исходные статусы заказов для возможного отката |
| | 0 | 502 | | var originalStatuses = new Dictionary<int, AppStatus>(); |
| | 0 | 503 | | foreach (var order in orders) |
| | | 504 | | { |
| | 0 | 505 | | var originalOrder = await _orderServices.GetOrder(order.Id); |
| | 0 | 506 | | if (originalOrder?.AppStatus != null) |
| | | 507 | | { |
| | 0 | 508 | | originalStatuses[order.Id] = originalOrder.AppStatus; |
| | | 509 | | } |
| | 0 | 510 | | } |
| | | 511 | | |
| | | 512 | | // Очищаем пустые дополнительные расходы перед сохранением |
| | | 513 | | // Это гарантирует, что пустые элементы не попадут в базу данных |
| | 0 | 514 | | foreach (var order in orders) |
| | | 515 | | { |
| | 0 | 516 | | if (order.ExtraExpenses != null && order.ExtraExpenses.Any()) |
| | | 517 | | { |
| | | 518 | | // Удаляем все расходы, где не заполнены оба поля (тип и сумма) |
| | 0 | 519 | | order.ExtraExpenses = order.ExtraExpenses |
| | 0 | 520 | | .Where(e => e != null && e.ExpenseTypeId > 0 && e.Amount > 0) |
| | 0 | 521 | | .ToList(); |
| | | 522 | | |
| | | 523 | | // Если список стал пустым, устанавливаем null |
| | 0 | 524 | | if (!order.ExtraExpenses.Any()) |
| | | 525 | | { |
| | 0 | 526 | | order.ExtraExpenses = []; |
| | | 527 | | } |
| | | 528 | | } |
| | | 529 | | } |
| | | 530 | | |
| | 0 | 531 | | var postTransitions = |
| | 0 | 532 | | await _statusTransitionDataServices.GetByTransactionType(TransactionType.WarehouseTransfer); |
| | 0 | 533 | | AppStatus appStatus = postTransitions.Any() |
| | 0 | 534 | | ? postTransitions.First().ToStatus! |
| | 0 | 535 | | : (await _appStatusServices.GetAppStatusAsync( |
| | 0 | 536 | | new AppStatus { Name = "Перемещен между складами" }))!; |
| | 0 | 537 | | var cancelledStatus = |
| | 0 | 538 | | await _appStatusServices.GetAppStatusAsync(new AppStatus { Name = "Отменен" }); |
| | | 539 | | |
| | 0 | 540 | | var submittedFields = SubmittedOrderFields.FromForm(Request.HasFormContentType ? Request.Form : null |
| | | 541 | | |
| | | 542 | | // Подготавливаем заказы для транзакции БЕЗ изменения статуса |
| | | 543 | | // Статус будет изменен только после успешного создания транзакции |
| | 0 | 544 | | foreach (var order in orders) |
| | | 545 | | { |
| | | 546 | | // Сохраняем исходный статус из модели, но не применяем изменения |
| | 0 | 547 | | var transactedOrder = await _orderServices.TransactOrder(order, submittedFields.For(order.Id)); |
| | | 548 | | // Восстанавливаем исходный статус до сохранения транзакции |
| | 0 | 549 | | if (originalStatuses.ContainsKey(order.Id)) |
| | | 550 | | { |
| | 0 | 551 | | transactedOrder.AppStatus = originalStatuses[order.Id]; |
| | | 552 | | } |
| | | 553 | | |
| | 0 | 554 | | ordersToUpdate.Add(transactedOrder); |
| | 0 | 555 | | } |
| | | 556 | | |
| | | 557 | | // Все заказы подходят для транзакции (фильтрация по статусу не нужна, так как статус еще не изменен |
| | 0 | 558 | | List<Order> ordersToTransaction = ordersToUpdate.ToList(); |
| | 0 | 559 | | var blockingOrderIds = OneCStockGate.GetBlockingOrderIds(stockCheck); |
| | 0 | 560 | | var ordersForOneC = ordersToTransaction.Where(o => !blockingOrderIds.Contains(o.Id)).ToList(); |
| | 0 | 561 | | var ordersForOneCDraft = ordersToTransaction.Where(o => blockingOrderIds.Contains(o.Id)).ToList(); |
| | | 562 | | |
| | 0 | 563 | | string oneCResult = ""; |
| | 0 | 564 | | IEnumerable<string> oneCHash = []; |
| | 0 | 565 | | List<OneCResponse> transferResult = new(); |
| | 0 | 566 | | List<OneCResponse> draftTransferResult = new(); |
| | | 567 | | |
| | | 568 | | // Обработка в 1С |
| | 0 | 569 | | if (processIn1C && ordersForOneC.Any()) |
| | | 570 | | { |
| | | 571 | | try |
| | | 572 | | { |
| | 0 | 573 | | transferResult = await _oneCTransferManager.TransferStockWithCustomWarehouses( |
| | 0 | 574 | | ordersForOneC, |
| | 0 | 575 | | warehouseSender, |
| | 0 | 576 | | warehouseRecipient, |
| | 0 | 577 | | TransactionType.WarehouseTransfer, |
| | 0 | 578 | | userName: userName, |
| | 0 | 579 | | orgInnOverride: oneCOrganizationInn); |
| | 0 | 580 | | } |
| | 0 | 581 | | catch (Exception e) |
| | | 582 | | { |
| | | 583 | | // Восстанавливаем исходные статусы при ошибке |
| | 0 | 584 | | foreach (var order in ordersToUpdate) |
| | | 585 | | { |
| | 0 | 586 | | if (originalStatuses.ContainsKey(order.Id)) |
| | | 587 | | { |
| | 0 | 588 | | order.AppStatus = originalStatuses[order.Id]; |
| | 0 | 589 | | await _orderServices.SaveChanges(); |
| | | 590 | | } |
| | | 591 | | } |
| | | 592 | | |
| | 0 | 593 | | string msg = $"<div class='alert alert-danger'>" + |
| | 0 | 594 | | $"<strong>⚠️ Ошибка обработки в 1С</strong><br/>" + |
| | 0 | 595 | | $"{e.Message}</div>"; |
| | | 596 | | |
| | 0 | 597 | | TempData["TransactionResult"] = msg; |
| | 0 | 598 | | return Json(new |
| | 0 | 599 | | { |
| | 0 | 600 | | redirectTo = Url.Action("Index", "Orders", |
| | 0 | 601 | | new { sortOrder = GetSortStateCookie(), page }) |
| | 0 | 602 | | }); |
| | | 603 | | } |
| | | 604 | | |
| | 0 | 605 | | var successfulTransfers = transferResult.Where(tr => tr.Success).ToList(); |
| | 0 | 606 | | var failedTransfers = transferResult.Where(tr => !tr.Success).ToList(); |
| | | 607 | | |
| | 0 | 608 | | if (failedTransfers.Count > 0) |
| | | 609 | | { |
| | | 610 | | // Восстанавливаем исходные статусы при частичной ошибке |
| | 0 | 611 | | foreach (var order in ordersToUpdate) |
| | | 612 | | { |
| | 0 | 613 | | if (originalStatuses.ContainsKey(order.Id)) |
| | | 614 | | { |
| | 0 | 615 | | order.AppStatus = originalStatuses[order.Id]; |
| | 0 | 616 | | await _orderServices.SaveChanges(); |
| | | 617 | | } |
| | | 618 | | } |
| | | 619 | | |
| | 0 | 620 | | var errorMessages = failedTransfers.Select(tr => tr.Message).Distinct().Take(3); |
| | 0 | 621 | | var errorCount = failedTransfers.Count; |
| | 0 | 622 | | var successCount = successfulTransfers.Count; |
| | 0 | 623 | | var totalCount = transferResult.Count; |
| | | 624 | | |
| | 0 | 625 | | string msg = $"<div class='alert alert-warning'>" + |
| | 0 | 626 | | $"<strong>⚠️ Частичный результат обработки в 1С</strong><br/>" + |
| | 0 | 627 | | $"Успешно: {successCount} из {totalCount} документов<br/>" + |
| | 0 | 628 | | $"С ошибками: {errorCount} документов<br/>" + |
| | 0 | 629 | | $"<small>Ошибки: {string.Join("; ", errorMessages)}" + |
| | 0 | 630 | | (errorCount > 3 ? "..." : "") + "</small>" + |
| | 0 | 631 | | "</div>"; |
| | | 632 | | |
| | 0 | 633 | | TempData["TransactionResult"] = msg; |
| | 0 | 634 | | return Json(new |
| | 0 | 635 | | { |
| | 0 | 636 | | redirectTo = Url.Action("Index", "Orders", |
| | 0 | 637 | | new { sortOrder = GetSortStateCookie(), page }) |
| | 0 | 638 | | }); |
| | | 639 | | } |
| | | 640 | | else |
| | | 641 | | { |
| | 0 | 642 | | oneCHash = successfulTransfers |
| | 0 | 643 | | .Select(h => h.TransactionId) |
| | 0 | 644 | | .Where(id => id != null) |
| | 0 | 645 | | .Select(id => id!); |
| | 0 | 646 | | if (ordersForOneCDraft.Any()) |
| | | 647 | | { |
| | 0 | 648 | | draftTransferResult = await _oneCTransferManager.TransferStockWithCustomWarehouses( |
| | 0 | 649 | | ordersForOneCDraft, warehouseSender, warehouseRecipient, TransactionType.WarehouseTr |
| | 0 | 650 | | userName: userName, orgInnOverride: oneCOrganizationInn, createAsDraft: true, |
| | 0 | 651 | | draftReason: "недостаточно остатка в 1С. Проведите после оприходования товара."); |
| | 0 | 652 | | transferResult.AddRange(draftTransferResult); |
| | | 653 | | } |
| | 0 | 654 | | oneCResult = OneCTransactionSummaryHelper.WrapSection("Проведённые документы 1С", |
| | 0 | 655 | | OneCTransactionSummaryHelper.BuildAlertSummary(ordersForOneC, successfulTra |
| | 0 | 656 | | + OneCTransactionSummaryHelper.WrapSection("Черновики 1С по дефицитным позициям |
| | 0 | 657 | | OneCTransactionSummaryHelper.BuildAlertSummary(ordersForOneCDraft, draftTra |
| | 0 | 658 | | + OneCStockGate.PartialPostingNotice(stockCheck, ordersForOneC.Count, draftTran |
| | 0 | 659 | | + OneCStockGate.SkippedNotice(stockCheck); |
| | | 660 | | } |
| | 0 | 661 | | } |
| | 0 | 662 | | else if (processIn1C && ordersForOneCDraft.Any()) |
| | | 663 | | { |
| | 0 | 664 | | draftTransferResult = await _oneCTransferManager.TransferStockWithCustomWarehouses( |
| | 0 | 665 | | ordersForOneCDraft, warehouseSender, warehouseRecipient, TransactionType.WarehouseTransfer, |
| | 0 | 666 | | userName: userName, orgInnOverride: oneCOrganizationInn, createAsDraft: true, |
| | 0 | 667 | | draftReason: "недостаточно остатка в 1С. Проведите после оприходования товара."); |
| | 0 | 668 | | transferResult.AddRange(draftTransferResult); |
| | 0 | 669 | | oneCResult = OneCTransactionSummaryHelper.WrapSection("Черновики 1С по дефицитным позициям", |
| | 0 | 670 | | OneCTransactionSummaryHelper.BuildAlertSummary(ordersForOneCDraft, draftTransfe |
| | 0 | 671 | | + OneCStockGate.PartialPostingNotice(stockCheck, 0, draftTransferResult.Any(r => r. |
| | | 672 | | } |
| | | 673 | | else |
| | | 674 | | { |
| | 0 | 675 | | oneCResult = OneCStockGate.NoDocumentNotice(stockCheck); |
| | | 676 | | } |
| | | 677 | | |
| | 0 | 678 | | await NotificationService.NotifyAllAsync(oneCResult); |
| | | 679 | | |
| | | 680 | | // Устанавливаем новый статус только после успешной обработки в 1С (если требуется) |
| | | 681 | | // и перед созданием транзакции в БД |
| | 0 | 682 | | foreach (var order in ordersToTransaction) |
| | | 683 | | { |
| | 0 | 684 | | if (order.AppStatus?.Name != "Отменен" && appStatus != null) |
| | | 685 | | { |
| | 0 | 686 | | order.AppStatus = appStatus; |
| | | 687 | | } |
| | | 688 | | } |
| | | 689 | | |
| | | 690 | | // Сохраняем изменения статусов |
| | 0 | 691 | | await _orderServices.SaveChanges(); |
| | | 692 | | |
| | | 693 | | // Создаем транзакцию в БД |
| | 0 | 694 | | var historyWindowStart = DateTime.Now; |
| | 0 | 695 | | var commentWithOneC = comment; |
| | 0 | 696 | | if (transferResult.Count > 0) |
| | | 697 | | { |
| | 0 | 698 | | if (aiFlow.ScheduleAiComment && aiFlow.Builder != null) |
| | | 699 | | { |
| | 0 | 700 | | aiFlow.Builder.WithOneCResults(transferResult, warehouseSender, warehouseRecipient); |
| | | 701 | | } |
| | | 702 | | else |
| | | 703 | | { |
| | 0 | 704 | | commentWithOneC += " | " + OneCTransactionSummaryHelper.BuildCommentDetails( |
| | 0 | 705 | | ordersForOneC, |
| | 0 | 706 | | transferResult.Where(r => !r.IsDraft).ToList(), |
| | 0 | 707 | | warehouseSender, |
| | 0 | 708 | | warehouseRecipient); |
| | | 709 | | } |
| | | 710 | | } |
| | | 711 | | |
| | 0 | 712 | | var commentForDb = TransactionCommentAiSchedulingHelper.ResolveDatabaseComment(aiFlow, commentWithOn |
| | | 713 | | |
| | | 714 | | try |
| | | 715 | | { |
| | 0 | 716 | | (changeCount, dateTime, transactionId) = await _transaction.CreateWarehouseTransferTransaction( |
| | 0 | 717 | | ordersToTransaction, |
| | 0 | 718 | | userName, |
| | 0 | 719 | | createAt, |
| | 0 | 720 | | commentForDb); |
| | 0 | 721 | | } |
| | 0 | 722 | | catch (Exception ex) |
| | | 723 | | { |
| | | 724 | | // Восстанавливаем исходные статусы при ошибке создания транзакции |
| | 0 | 725 | | foreach (var order in ordersToTransaction) |
| | | 726 | | { |
| | 0 | 727 | | if (originalStatuses.ContainsKey(order.Id)) |
| | | 728 | | { |
| | 0 | 729 | | order.AppStatus = originalStatuses[order.Id]; |
| | | 730 | | } |
| | | 731 | | } |
| | | 732 | | |
| | 0 | 733 | | await _orderServices.SaveChanges(); |
| | | 734 | | |
| | 0 | 735 | | string msg = $"<div class='alert alert-danger'>" + |
| | 0 | 736 | | $"<strong>⚠️ Ошибка создания транзакции</strong><br/>" + |
| | 0 | 737 | | $"{ex.Message}</div>"; |
| | | 738 | | |
| | 0 | 739 | | TempData["TransactionResult"] = msg; |
| | 0 | 740 | | return Json(new |
| | 0 | 741 | | { |
| | 0 | 742 | | redirectTo = Url.Action("Index", "Orders", |
| | 0 | 743 | | new { sortOrder = GetSortStateCookie(), page }) |
| | 0 | 744 | | }); |
| | | 745 | | } |
| | | 746 | | |
| | 0 | 747 | | int cancelCount = ordersToTransaction.Count(o => o.AppStatus?.Name == "Отменен"); |
| | | 748 | | |
| | 0 | 749 | | if (changeCount > 0) |
| | | 750 | | { |
| | 0 | 751 | | if (processIn1C && transferResult.Count > 0) |
| | | 752 | | { |
| | 0 | 753 | | await OrderHistoryOneCPostingRecorder.TryRecordMovementAsync( |
| | 0 | 754 | | HttpContext.RequestServices, |
| | 0 | 755 | | ordersToTransaction, |
| | 0 | 756 | | transferResult, |
| | 0 | 757 | | userName, |
| | 0 | 758 | | transactionId > 0 ? (int?)transactionId : null, |
| | 0 | 759 | | historyWindowStart, |
| | 0 | 760 | | warehouseSender, |
| | 0 | 761 | | warehouseRecipient); |
| | | 762 | | } |
| | | 763 | | |
| | 0 | 764 | | var fromStatusByOrderId = originalStatuses.ToDictionary(x => x.Key, x => x.Value.Id); |
| | 0 | 765 | | var statusTransitionHistoryService = |
| | 0 | 766 | | HttpContext.RequestServices.GetService<IStatusTransitionHistoryService>(); |
| | 0 | 767 | | if (postTransitions.Any() && statusTransitionHistoryService != null) |
| | | 768 | | { |
| | 0 | 769 | | var user = await _userCacheService.GetCachedUserAsync(User); |
| | 0 | 770 | | await statusTransitionHistoryService.RecordStatusTransitionHistoryAsync( |
| | 0 | 771 | | TransactionType.WarehouseTransfer, |
| | 0 | 772 | | ordersToTransaction, |
| | 0 | 773 | | fromStatusByOrderId, |
| | 0 | 774 | | postTransitions, |
| | 0 | 775 | | commentForDb, |
| | 0 | 776 | | user?.AccessProfileId, |
| | 0 | 777 | | createAt, |
| | 0 | 778 | | null); |
| | | 779 | | } |
| | | 780 | | |
| | 0 | 781 | | await _cacheUpdater.Update(_cache); |
| | 0 | 782 | | var statusNameForMessage = string.Join(", ", |
| | 0 | 783 | | ordersToTransaction.Select(o => o.AppStatus?.Name).Where(n => !string.IsNullOrEmpty(n)) |
| | 0 | 784 | | .Distinct()); |
| | 0 | 785 | | if (string.IsNullOrEmpty(statusNameForMessage) && postTransitions.Any()) |
| | 0 | 786 | | statusNameForMessage = string.Join(", ", |
| | 0 | 787 | | postTransitions.Select(t => t.ToStatus?.Name).Where(n => !string.IsNullOrEmpty(n)) |
| | 0 | 788 | | .Distinct()); |
| | 0 | 789 | | if (string.IsNullOrEmpty(statusNameForMessage)) |
| | 0 | 790 | | statusNameForMessage = "Перемещение между складами"; |
| | | 791 | | |
| | 0 | 792 | | string msg = TransactionConfig.BuildStandardSuccessMessage( |
| | 0 | 793 | | changeCount, |
| | 0 | 794 | | cancelCount, |
| | 0 | 795 | | dateTime, |
| | 0 | 796 | | userName, |
| | 0 | 797 | | commentForDb, |
| | 0 | 798 | | orders, |
| | 0 | 799 | | statusNameForMessage, |
| | 0 | 800 | | oneCResult); |
| | | 801 | | |
| | 0 | 802 | | await NotificationService.NotifyAllAsync(msg); |
| | 0 | 803 | | TempData["TransactionResult"] = msg; |
| | 0 | 804 | | await CompleteTrackingAsync(trackingKey, transactionId, ordersToTransaction.Count, commentForDb) |
| | | 805 | | |
| | 0 | 806 | | if (aiFlow.ScheduleAiComment && aiFlow.Builder != null && transactionId > 0) |
| | | 807 | | { |
| | 0 | 808 | | await TransactionCommentAiSchedulingHelper.TryScheduleAsync( |
| | 0 | 809 | | HttpContext.RequestServices, |
| | 0 | 810 | | aiFlow.Builder, |
| | 0 | 811 | | true, |
| | 0 | 812 | | (int)transactionId, |
| | 0 | 813 | | ordersToTransaction, |
| | 0 | 814 | | fromStatusByOrderId, |
| | 0 | 815 | | _context, |
| | 0 | 816 | | ct: HttpContext.RequestAborted); |
| | | 817 | | } |
| | 0 | 818 | | } |
| | | 819 | | |
| | 0 | 820 | | await _transactionCache.Update(); |
| | 0 | 821 | | } |
| | | 822 | | |
| | 0 | 823 | | ClearSelectedIdsSession(); |
| | | 824 | | |
| | | 825 | | // Очищаем cookies для заказов |
| | 0 | 826 | | foreach (var order in orders) |
| | | 827 | | { |
| | 0 | 828 | | var cookieKey = $"PurchasePrice_{order.Id}"; |
| | 0 | 829 | | if (Request.Cookies.ContainsKey(cookieKey)) |
| | | 830 | | { |
| | 0 | 831 | | Response.Cookies.Delete(cookieKey); |
| | | 832 | | } |
| | | 833 | | } |
| | | 834 | | |
| | 0 | 835 | | return Json(new |
| | 0 | 836 | | { |
| | 0 | 837 | | redirectTo = Url.Action("Index", "Orders", new { sortOrder = GetSortStateCookie(), page }) |
| | 0 | 838 | | }); |
| | | 839 | | } |
| | 0 | 840 | | catch (TransactionException ex) |
| | | 841 | | { |
| | 0 | 842 | | TempData["TransactionErrorCode"] = ex.Code.ToString(); |
| | 0 | 843 | | TempData["ErorrResult"] = ex.Message; |
| | 0 | 844 | | return Json(new |
| | 0 | 845 | | { |
| | 0 | 846 | | redirectTo = Url.Action("Index", "Orders", new { sortOrder = GetSortStateCookie(), page }) |
| | 0 | 847 | | }); |
| | | 848 | | } |
| | 0 | 849 | | catch (Exception ex) |
| | | 850 | | { |
| | 0 | 851 | | TempData["TransactionErrorCode"] = TransactionErrorCode.Unknown.ToString(); |
| | 0 | 852 | | TempData["ErorrResult"] = $"Не удалось провести выбранные заказы ({ex.Message})"; |
| | 0 | 853 | | return Json(new |
| | 0 | 854 | | { |
| | 0 | 855 | | redirectTo = Url.Action("Index", "Orders", new { sortOrder = GetSortStateCookie(), page }) |
| | 0 | 856 | | }); |
| | | 857 | | } |
| | 0 | 858 | | } |
| | | 859 | | |
| | | 860 | | [HttpPost] |
| | | 861 | | public IActionResult ClearSelectedIdsSession() |
| | | 862 | | { |
| | 0 | 863 | | HttpContext.Session.SetString("selectedIds", JsonConvert.SerializeObject(new List<int>())); |
| | 0 | 864 | | return Ok(); |
| | | 865 | | } |
| | | 866 | | |
| | | 867 | | private OrderSortState GetSortStateCookie() |
| | | 868 | | { |
| | 0 | 869 | | var cookie = Request.Cookies["SortState"]; |
| | 0 | 870 | | if (!string.IsNullOrEmpty(cookie) && Enum.TryParse<OrderSortState>(cookie, out var saved)) |
| | 0 | 871 | | return saved; |
| | 0 | 872 | | return OrderSortState.StandardState; |
| | | 873 | | } |
| | | 874 | | |
| | | 875 | | private async Task<List<string>> FilterBitrixWarehouseNamesByScope(IEnumerable<string> warehouseNames) |
| | | 876 | | { |
| | 0 | 877 | | var names = warehouseNames |
| | 0 | 878 | | .Where(n => !string.IsNullOrWhiteSpace(n)) |
| | 0 | 879 | | .Distinct(StringComparer.OrdinalIgnoreCase) |
| | 0 | 880 | | .OrderBy(n => n) |
| | 0 | 881 | | .ToList(); |
| | | 882 | | |
| | 0 | 883 | | var userId = User?.FindFirstValue(ClaimTypes.NameIdentifier); |
| | 0 | 884 | | if (string.IsNullOrWhiteSpace(userId)) |
| | | 885 | | { |
| | 0 | 886 | | return names; |
| | | 887 | | } |
| | | 888 | | |
| | 0 | 889 | | var allowed = await _scopeFilterValuesService.GetAllowedValuesAsync( |
| | 0 | 890 | | userId, |
| | 0 | 891 | | "BitrixWarehouse", |
| | 0 | 892 | | HttpContext.RequestAborted); |
| | | 893 | | |
| | 0 | 894 | | if (!allowed.TryGetValue("Name", out var allowedNames) || allowedNames.Count == 0) |
| | | 895 | | { |
| | 0 | 896 | | return names; |
| | | 897 | | } |
| | | 898 | | |
| | 0 | 899 | | return names |
| | 0 | 900 | | .Where(n => allowedNames.Contains(n)) |
| | 0 | 901 | | .OrderBy(n => n) |
| | 0 | 902 | | .ToList(); |
| | 0 | 903 | | } |
| | | 904 | | |
| | | 905 | | private async Task AttachTrackingKeyAsync(MultiplayEditOrderViewModel model, TransactionType transactionType) |
| | | 906 | | { |
| | 0 | 907 | | if (model.Orders == null || !model.Orders.Any()) |
| | 0 | 908 | | return; |
| | 0 | 909 | | var trackingService = HttpContext.RequestServices.GetService<ITransactionTimeTrackingService>(); |
| | 0 | 910 | | if (trackingService == null) |
| | 0 | 911 | | return; |
| | 0 | 912 | | model.TrackingKey = await trackingService.StartSessionAsync( |
| | 0 | 913 | | User.Identity?.Name ?? "unknown", |
| | 0 | 914 | | model.User?.AccessProfileId, |
| | 0 | 915 | | transactionType, |
| | 0 | 916 | | model.Orders.Select(o => o.Id), |
| | 0 | 917 | | $"{Request.Path}{Request.QueryString}", |
| | 0 | 918 | | HttpContext.Connection.RemoteIpAddress?.ToString(), |
| | 0 | 919 | | Request.Headers.UserAgent.ToString()); |
| | 0 | 920 | | } |
| | | 921 | | |
| | | 922 | | private async Task CompleteTrackingAsync(string? trackingKey, long transactionId, int ordersCount, string? comme |
| | | 923 | | { |
| | 0 | 924 | | if (string.IsNullOrWhiteSpace(trackingKey) || transactionId <= 0) |
| | 0 | 925 | | return; |
| | 0 | 926 | | var trackingService = HttpContext.RequestServices.GetService<ITransactionTimeTrackingService>(); |
| | 0 | 927 | | if (trackingService == null) |
| | 0 | 928 | | return; |
| | 0 | 929 | | await trackingService.CompleteSessionAsync( |
| | 0 | 930 | | trackingKey, |
| | 0 | 931 | | transactionId, |
| | 0 | 932 | | ordersCount, |
| | 0 | 933 | | comment, |
| | 0 | 934 | | $"/Studio2/Transaction/GetTransaction?transactionId={transactionId}"); |
| | 0 | 935 | | } |
| | | 936 | | } |
| | | 937 | | } |