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