| | | 1 | | using Microsoft.AspNetCore.Authorization; |
| | | 2 | | using Microsoft.AspNetCore.Mvc; |
| | | 3 | | using Microsoft.Extensions.DependencyInjection; |
| | | 4 | | using Microsoft.AspNetCore.Mvc.Rendering; |
| | | 5 | | using Microsoft.EntityFrameworkCore; |
| | | 6 | | using System.ComponentModel; |
| | | 7 | | using System.ComponentModel.DataAnnotations; |
| | | 8 | | using Newtonsoft.Json; |
| | | 9 | | using OzonDomains; |
| | | 10 | | using OzonDomains.Models; |
| | | 11 | | using OzonDomains.Models.AccessControl; |
| | | 12 | | using OzonOrdersWeb.Areas.Studio2.Controllers; |
| | | 13 | | using OzonOrdersWeb.Areas.Studio2.Helpers; |
| | | 14 | | using OzonOrdersWeb.ViewModels.OrderViewModels; |
| | | 15 | | using OzonOrdersWeb.Areas.Admin.OrderList; |
| | | 16 | | using OzonOrdersWeb.Areas.Studio2.Models.Transactions; |
| | | 17 | | using OzonRepositories.Context; |
| | | 18 | | using OzonRepositories.Context.Identity; |
| | | 19 | | using OzonRepositories.Data.Bitrix; |
| | | 20 | | using PartsInfo.HttpUtils; |
| | | 21 | | using System.Reflection; |
| | | 22 | | using System.Security.Claims; |
| | | 23 | | using Services.ApiServices.TelegramBotApi; |
| | | 24 | | using Services.ApiServices; |
| | | 25 | | using Services.ApiServices._1CApi; |
| | | 26 | | using Services.ApiServices._1CApi.Models; |
| | | 27 | | using Services.ApiServices._1CApi.Stock; |
| | | 28 | | using Services.ApiServices.OzonApi; |
| | | 29 | | using Services.Authorization.Services; |
| | | 30 | | using Services.CacheServices.Cache.OzonOrdersCache; |
| | | 31 | | using Services.CacheServices.Cache.UserCacheService; |
| | | 32 | | using Services.DataServices; |
| | | 33 | | using Services.FiltersServices.SortModels; |
| | | 34 | | using OzonOrdersWeb.WebServices.Currency; |
| | | 35 | | using Services.SignalRServices; |
| | | 36 | | using Services.TransactionUtilsServices; |
| | | 37 | | using Services.TransactionCommentAi; |
| | | 38 | | using OzonOrdersWeb.TransactionServices; |
| | | 39 | | using Services.CacheServices.Cache; |
| | | 40 | | using Services.StockSyncServices; |
| | | 41 | | using Services.ApiSupplierSettingsServices; |
| | | 42 | | |
| | | 43 | | namespace OzonOrdersWeb.Areas.Studio2.Controllers.Base |
| | | 44 | | { |
| | | 45 | | /// <summary> |
| | | 46 | | /// Абстрактный базовый контроллер для всех транзакционных операций с заказами |
| | | 47 | | /// </summary> |
| | | 48 | | [Authorize(Roles = "User,Admin")] |
| | | 49 | | [Area("Studio2")] |
| | | 50 | | public abstract class BaseTransactionController : Controller |
| | | 51 | | { |
| | | 52 | | protected const string TransactionErrorCodeTempDataKey = "TransactionErrorCode"; |
| | | 53 | | |
| | | 54 | | protected readonly ApplicationDbContext Context; |
| | | 55 | | protected readonly AppStatusDataServices AppStatusServices; |
| | | 56 | | protected readonly OrdersDataServices OrderServices; |
| | | 57 | | protected readonly TransactionManager TransactionManager; |
| | | 58 | | protected readonly TransactionCache TransactionCache; |
| | | 59 | | protected readonly SupplierDataServices SupplierDataServices; |
| | | 60 | | protected readonly IUserCacheService UserCacheService; |
| | | 61 | | protected readonly ICurrencyRatesViewBagService CurrencyRatesViewBagService; |
| | | 62 | | protected readonly CacheUpdater<Order> CacheUpdater; |
| | | 63 | | protected readonly OrderCache OrderCache; |
| | | 64 | | protected readonly OneCDataManager? OneCDataManager; |
| | | 65 | | protected readonly WarehouseDataServices? WarehouseDataServices; |
| | | 66 | | protected readonly BitrixStockRepository? BitrixStockRepository; |
| | | 67 | | protected readonly ProxyHttpClientService? ProxyHttpClientService; |
| | | 68 | | protected readonly OneCTransferManager? OneCTransferManager; |
| | | 69 | | protected readonly OrderCartServices? OrderCartServices; |
| | | 70 | | protected readonly WarehouseMappingDataServices? WarehouseMappingDataServices; |
| | | 71 | | protected readonly ExpenseTypeDataServices? ExpenseTypeDataServices; |
| | | 72 | | protected readonly OrderExtraExpenseDataServices? OrderExtraExpenseDataServices; |
| | | 73 | | protected readonly StatusTransitionDataServices? StatusTransitionDataServices; |
| | | 74 | | protected readonly IScopeFilterValuesService? ScopeFilterValuesService; |
| | | 75 | | protected readonly IDataScopeService? DataScopeService; |
| | | 76 | | protected readonly OzonOrderCancellationService? OzonCancellationService; |
| | | 77 | | |
| | 0 | 78 | | protected BaseTransactionController( |
| | 0 | 79 | | ApplicationDbContext context, |
| | 0 | 80 | | AppStatusDataServices appStatusServices, |
| | 0 | 81 | | OrdersDataServices orderServices, |
| | 0 | 82 | | TransactionManager transactionManager, |
| | 0 | 83 | | TransactionCache transactionCache, |
| | 0 | 84 | | SupplierDataServices supplierDataServices, |
| | 0 | 85 | | IUserCacheService userCacheService, |
| | 0 | 86 | | ICurrencyRatesViewBagService currencyRatesViewBagService, |
| | 0 | 87 | | CacheUpdater<Order> cacheUpdater, |
| | 0 | 88 | | OrderCache orderCache, |
| | 0 | 89 | | OneCDataManager? oneCDataManager = null, |
| | 0 | 90 | | WarehouseDataServices? warehouseDataServices = null, |
| | 0 | 91 | | BitrixStockRepository? bitrixStockRepository = null, |
| | 0 | 92 | | ProxyHttpClientService? proxyHttpClientService = null, |
| | 0 | 93 | | OneCTransferManager? oneCTransferManager = null, |
| | 0 | 94 | | OrderCartServices? orderCartServices = null, |
| | 0 | 95 | | WarehouseMappingDataServices? warehouseMappingDataServices = null, |
| | 0 | 96 | | ExpenseTypeDataServices? expenseTypeDataServices = null, |
| | 0 | 97 | | OrderExtraExpenseDataServices? orderExtraExpenseDataServices = null, |
| | 0 | 98 | | StatusTransitionDataServices? statusTransitionDataServices = null, |
| | 0 | 99 | | IScopeFilterValuesService? scopeFilterValuesService = null, |
| | 0 | 100 | | IDataScopeService? dataScopeService = null, |
| | 0 | 101 | | OzonOrderCancellationService? ozonCancellationService = null) |
| | | 102 | | { |
| | 0 | 103 | | Context = context; |
| | 0 | 104 | | AppStatusServices = appStatusServices; |
| | 0 | 105 | | OrderServices = orderServices; |
| | 0 | 106 | | TransactionManager = transactionManager; |
| | 0 | 107 | | TransactionCache = transactionCache; |
| | 0 | 108 | | SupplierDataServices = supplierDataServices; |
| | 0 | 109 | | UserCacheService = userCacheService; |
| | 0 | 110 | | CurrencyRatesViewBagService = currencyRatesViewBagService; |
| | 0 | 111 | | CacheUpdater = cacheUpdater; |
| | 0 | 112 | | OrderCache = orderCache; |
| | 0 | 113 | | OneCDataManager = oneCDataManager; |
| | 0 | 114 | | WarehouseDataServices = warehouseDataServices; |
| | 0 | 115 | | BitrixStockRepository = bitrixStockRepository; |
| | 0 | 116 | | ProxyHttpClientService = proxyHttpClientService; |
| | 0 | 117 | | OneCTransferManager = oneCTransferManager; |
| | 0 | 118 | | OrderCartServices = orderCartServices; |
| | 0 | 119 | | WarehouseMappingDataServices = warehouseMappingDataServices; |
| | 0 | 120 | | ExpenseTypeDataServices = expenseTypeDataServices; |
| | 0 | 121 | | OrderExtraExpenseDataServices = orderExtraExpenseDataServices; |
| | 0 | 122 | | StatusTransitionDataServices = statusTransitionDataServices; |
| | 0 | 123 | | ScopeFilterValuesService = scopeFilterValuesService; |
| | 0 | 124 | | DataScopeService = dataScopeService; |
| | 0 | 125 | | OzonCancellationService = ozonCancellationService; |
| | 0 | 126 | | } |
| | | 127 | | |
| | | 128 | | private WebServices.Transactions.TransactionProgressReporter? _progressReporter; |
| | | 129 | | private string? _progressKey; |
| | | 130 | | private bool _progressKeyResolved; |
| | | 131 | | |
| | | 132 | | /// <summary>Ключ прогресса, который страница передала вместе с формой.</summary> |
| | | 133 | | private string? ProgressKey |
| | | 134 | | { |
| | | 135 | | get |
| | | 136 | | { |
| | 0 | 137 | | if (_progressKeyResolved) return _progressKey; |
| | 0 | 138 | | _progressKeyResolved = true; |
| | 0 | 139 | | if (Request.HasFormContentType && Request.Form.TryGetValue("progressKey", out var value)) |
| | 0 | 140 | | _progressKey = value.FirstOrDefault(); |
| | 0 | 141 | | return _progressKey; |
| | | 142 | | } |
| | | 143 | | } |
| | | 144 | | |
| | | 145 | | /// <summary> |
| | | 146 | | /// Сообщить странице о реально пройденном этапе проведения: оверлей рисует их по событиям, |
| | | 147 | | /// а не по секундомеру. |
| | | 148 | | /// </summary> |
| | | 149 | | protected Task ReportStageAsync(string code, string title, string? detail = null) |
| | | 150 | | { |
| | 0 | 151 | | if (string.IsNullOrWhiteSpace(ProgressKey)) |
| | 0 | 152 | | return Task.CompletedTask; |
| | | 153 | | |
| | 0 | 154 | | _progressReporter ??= HttpContext.RequestServices |
| | 0 | 155 | | .GetService<WebServices.Transactions.TransactionProgressReporter>(); |
| | 0 | 156 | | return _progressReporter?.StageAsync(ProgressKey, code, title, detail) ?? Task.CompletedTask; |
| | | 157 | | } |
| | | 158 | | |
| | | 159 | | protected Task ReportProgressFinishedAsync(bool success, string? message = null) |
| | | 160 | | { |
| | 0 | 161 | | if (string.IsNullOrWhiteSpace(ProgressKey)) |
| | 0 | 162 | | return Task.CompletedTask; |
| | | 163 | | |
| | 0 | 164 | | _progressReporter ??= HttpContext.RequestServices |
| | 0 | 165 | | .GetService<WebServices.Transactions.TransactionProgressReporter>(); |
| | 0 | 166 | | return _progressReporter?.CompleteAsync(ProgressKey, success, message) ?? Task.CompletedTask; |
| | | 167 | | } |
| | | 168 | | |
| | | 169 | | /// <summary> |
| | | 170 | | /// Абстрактный метод для получения конфигурации транзакции |
| | | 171 | | /// </summary> |
| | | 172 | | protected abstract TransactionConfig GetTransactionConfig(); |
| | | 173 | | |
| | | 174 | | /// <summary> |
| | | 175 | | /// Базовый GET метод для подготовки транзакции |
| | | 176 | | /// </summary> |
| | | 177 | | protected async Task<IActionResult> CreateTransactionBase(string ids, int page) |
| | | 178 | | { |
| | 0 | 179 | | return await CreateTransactionBase(ids, page, GetTransactionConfig()); |
| | 0 | 180 | | } |
| | | 181 | | |
| | | 182 | | /// <summary> |
| | | 183 | | /// Подготовка транзакции с явной конфигурацией (например, массовый сценарий из файла). |
| | | 184 | | /// </summary> |
| | | 185 | | protected async Task<IActionResult> CreateTransactionBase(string ids, int page, TransactionConfig config) |
| | | 186 | | { |
| | 0 | 187 | | ViewBag.ClearPurchasePriceOnLoad = config.ClearPurchasePriceOnLoad; |
| | | 188 | | |
| | 0 | 189 | | List<StatusTransition>? transitions = null; |
| | 0 | 190 | | HashSet<int>? validSourceStatusIds = null; |
| | 0 | 191 | | Dictionary<int, (AppStatus ToStatus, StatusTransition Transition)>? fromStatusToTarget = null; |
| | | 192 | | |
| | 0 | 193 | | if (config.UseStatusTransitions && StatusTransitionDataServices != null) |
| | | 194 | | { |
| | 0 | 195 | | transitions = await StatusTransitionDataServices.GetByTransactionType(config.TransactionType); |
| | 0 | 196 | | if (transitions.Any()) |
| | | 197 | | { |
| | 0 | 198 | | validSourceStatusIds = transitions.Select(t => t.FromStatusId).ToHashSet(); |
| | 0 | 199 | | fromStatusToTarget = transitions |
| | 0 | 200 | | .GroupBy(t => t.FromStatusId) |
| | 0 | 201 | | .ToDictionary(g => g.Key, g => (g.First().ToStatus!, g.First())); |
| | | 202 | | } |
| | | 203 | | } |
| | | 204 | | |
| | 0 | 205 | | AppStatus? primaryStatus = null; |
| | 0 | 206 | | var filteredStatuses = new List<AppStatus>(); |
| | | 207 | | |
| | 0 | 208 | | if (transitions != null && transitions.Any()) |
| | | 209 | | { |
| | 0 | 210 | | var allToStatuses = transitions.Select(t => t.ToStatus!).Where(s => s != null).Distinct().ToList(); |
| | 0 | 211 | | var allFromStatuses = transitions.Select(t => t.FromStatus!).Where(s => s != null).Distinct().ToList(); |
| | 0 | 212 | | filteredStatuses = allToStatuses.Union(allFromStatuses).GroupBy(s => s!.Id).Select(g => g.First()!) |
| | 0 | 213 | | .OrderBy(s => s.Name).ToList(); |
| | 0 | 214 | | primaryStatus = filteredStatuses.FirstOrDefault(); |
| | | 215 | | |
| | | 216 | | // Добавляем дополнительные статусы если указаны флаги |
| | 0 | 217 | | if (config.IncludeCancelledOrders) |
| | | 218 | | { |
| | 0 | 219 | | var cancelledStatus = await GetOrCreateAppStatus("Отменен"); |
| | 0 | 220 | | if (!filteredStatuses.Any(s => s.Id == cancelledStatus.Id)) |
| | 0 | 221 | | filteredStatuses.Add(cancelledStatus); |
| | 0 | 222 | | } |
| | | 223 | | |
| | 0 | 224 | | if (config.IncludeToCancellationStatus) |
| | | 225 | | { |
| | 0 | 226 | | var toCancellationStatus = await GetOrCreateAppStatus("К отмене"); |
| | 0 | 227 | | if (!filteredStatuses.Any(s => s.Id == toCancellationStatus.Id)) |
| | 0 | 228 | | filteredStatuses.Add(toCancellationStatus); |
| | 0 | 229 | | } |
| | | 230 | | |
| | 0 | 231 | | if (config.IncludeNotReadyStatus) |
| | | 232 | | { |
| | 0 | 233 | | var notReadyStatus = await GetOrCreateAppStatus("Не готов"); |
| | 0 | 234 | | if (!filteredStatuses.Any(s => s.Id == notReadyStatus.Id)) |
| | 0 | 235 | | filteredStatuses.Add(notReadyStatus); |
| | 0 | 236 | | } |
| | | 237 | | } |
| | | 238 | | else |
| | | 239 | | { |
| | 0 | 240 | | if (!string.IsNullOrEmpty(config.PrimaryStatusName)) |
| | 0 | 241 | | primaryStatus = await GetOrCreateAppStatus(config.PrimaryStatusName); |
| | 0 | 242 | | foreach (var statusName in config.AdditionalStatusNames) |
| | | 243 | | { |
| | 0 | 244 | | var status = await GetOrCreateAppStatus(statusName); |
| | 0 | 245 | | filteredStatuses.Add(status); |
| | | 246 | | } |
| | | 247 | | |
| | 0 | 248 | | if (primaryStatus != null) |
| | 0 | 249 | | filteredStatuses.Insert(0, primaryStatus); |
| | 0 | 250 | | if (config.IncludeCancelledOrders) |
| | | 251 | | { |
| | 0 | 252 | | var cancelledStatus = await AppStatusServices.GetAppStatusAsync(new AppStatus { Name = "Отменен" }); |
| | 0 | 253 | | if (cancelledStatus != null) |
| | 0 | 254 | | filteredStatuses.Add(cancelledStatus); |
| | | 255 | | } |
| | | 256 | | |
| | 0 | 257 | | if (config.IncludeToCancellationStatus) |
| | | 258 | | { |
| | 0 | 259 | | var toCancellationStatus = await GetOrCreateAppStatus("К отмене"); |
| | 0 | 260 | | filteredStatuses.Add(toCancellationStatus); |
| | | 261 | | } |
| | | 262 | | |
| | 0 | 263 | | if (string.IsNullOrEmpty(config.PrimaryStatusName)) |
| | | 264 | | { |
| | 0 | 265 | | var allStatuses = await AppStatusServices.GetAppStatuses(); |
| | 0 | 266 | | filteredStatuses = allStatuses.OrderBy(s => s.Name).ToList(); |
| | | 267 | | } |
| | | 268 | | } |
| | | 269 | | |
| | 0 | 270 | | filteredStatuses = filteredStatuses.GroupBy(s => s.Id).Select(g => g.First()).ToList(); |
| | 0 | 271 | | filteredStatuses = await FilterByOrderScopeIdsAsync(filteredStatuses, s => s.Id, "AppStatusId"); |
| | | 272 | | |
| | 0 | 273 | | ViewBag.AppStatuses = new SelectList(filteredStatuses, "Id", "Name"); |
| | 0 | 274 | | ViewBag.StatusColors = filteredStatuses.ToDictionary(s => s.Id.ToString(), s => s.GetStatusColor()); |
| | | 275 | | |
| | | 276 | | // Проверка на пустой или null ids |
| | 0 | 277 | | if (string.IsNullOrWhiteSpace(ids)) |
| | | 278 | | { |
| | 0 | 279 | | SetTransactionErrorViewData( |
| | 0 | 280 | | TransactionErrorCode.NoOrdersSelected, |
| | 0 | 281 | | "Ни один заказ не был выбран. Выберите заказы с помощью галочек."); |
| | | 282 | | |
| | 0 | 283 | | var emptyUser = (await UserCacheService.GetCachedUserAsync(User))!; |
| | 0 | 284 | | var emptyRates = await CurrencyRatesViewBagService.GetRatesAsync(); |
| | 0 | 285 | | var emptyArmtekSettings = await HttpContext.RequestServices |
| | 0 | 286 | | .GetRequiredService<ApiSupplierSettingsService>() |
| | 0 | 287 | | .GetArmtekAsync(HttpContext.RequestAborted); |
| | 0 | 288 | | var emptyArmtekBySettings = await HttpContext.RequestServices |
| | 0 | 289 | | .GetRequiredService<ApiSupplierSettingsService>() |
| | 0 | 290 | | .GetArmtekByAsync(HttpContext.RequestAborted); |
| | 0 | 291 | | var emptyShateMSettings = await HttpContext.RequestServices |
| | 0 | 292 | | .GetRequiredService<ApiSupplierSettingsService>() |
| | 0 | 293 | | .GetShateMAsync(HttpContext.RequestAborted); |
| | 0 | 294 | | var emptyAvdSettings = await HttpContext.RequestServices |
| | 0 | 295 | | .GetRequiredService<ApiSupplierSettingsService>() |
| | 0 | 296 | | .GetAvdAsync(HttpContext.RequestAborted); |
| | 0 | 297 | | var emptyBergSettings = await HttpContext.RequestServices |
| | 0 | 298 | | .GetRequiredService<ApiSupplierSettingsService>() |
| | 0 | 299 | | .GetBergAsync(HttpContext.RequestAborted); |
| | 0 | 300 | | var emptyMotexSettings = await HttpContext.RequestServices |
| | 0 | 301 | | .GetRequiredService<ApiSupplierSettingsService>() |
| | 0 | 302 | | .GetMotexAsync(HttpContext.RequestAborted); |
| | 0 | 303 | | var emptyMlAutoSettings = await HttpContext.RequestServices |
| | 0 | 304 | | .GetRequiredService<ApiSupplierSettingsService>() |
| | 0 | 305 | | .GetMlAutoAsync(HttpContext.RequestAborted); |
| | 0 | 306 | | var emptyMlAutoRuSettings = await HttpContext.RequestServices |
| | 0 | 307 | | .GetRequiredService<ApiSupplierSettingsService>() |
| | 0 | 308 | | .GetMlAutoRuAsync(HttpContext.RequestAborted); |
| | 0 | 309 | | var emptyForumAutoSettings = await HttpContext.RequestServices |
| | 0 | 310 | | .GetRequiredService<ApiSupplierSettingsService>() |
| | 0 | 311 | | .GetForumAutoAsync(HttpContext.RequestAborted); |
| | 0 | 312 | | var emptyForumAutoInterpartsSettings = await HttpContext.RequestServices |
| | 0 | 313 | | .GetRequiredService<ApiSupplierSettingsService>() |
| | 0 | 314 | | .GetForumAutoInterpartsAsync(HttpContext.RequestAborted); |
| | 0 | 315 | | var emptyForumAutoPiterSettings = await HttpContext.RequestServices |
| | 0 | 316 | | .GetRequiredService<ApiSupplierSettingsService>() |
| | 0 | 317 | | .GetForumAutoPiterAsync(HttpContext.RequestAborted); |
| | 0 | 318 | | var emptyForumAutoRostovSettings = await HttpContext.RequestServices |
| | 0 | 319 | | .GetRequiredService<ApiSupplierSettingsService>() |
| | 0 | 320 | | .GetForumAutoRostovAsync(HttpContext.RequestAborted); |
| | 0 | 321 | | var emptyForumAutoIstraSettings = await HttpContext.RequestServices |
| | 0 | 322 | | .GetRequiredService<ApiSupplierSettingsService>() |
| | 0 | 323 | | .GetForumAutoIstraAsync(HttpContext.RequestAborted); |
| | 0 | 324 | | var emptyFavoritPartsSettings = await HttpContext.RequestServices |
| | 0 | 325 | | .GetRequiredService<ApiSupplierSettingsService>() |
| | 0 | 326 | | .GetFavoritPartsAsync(HttpContext.RequestAborted); |
| | 0 | 327 | | var emptyFavoritPartsIstraSettings = await HttpContext.RequestServices |
| | 0 | 328 | | .GetRequiredService<ApiSupplierSettingsService>() |
| | 0 | 329 | | .GetFavoritPartsIstraAsync(HttpContext.RequestAborted); |
| | 0 | 330 | | var emptyFavoritPartsRostovSettings = await HttpContext.RequestServices |
| | 0 | 331 | | .GetRequiredService<ApiSupplierSettingsService>() |
| | 0 | 332 | | .GetFavoritPartsRostovAsync(HttpContext.RequestAborted); |
| | 0 | 333 | | var emptyMoskvorechieIstraSettings = await HttpContext.RequestServices |
| | 0 | 334 | | .GetRequiredService<ApiSupplierSettingsService>() |
| | 0 | 335 | | .GetMoskvorechieIstraAsync(HttpContext.RequestAborted); |
| | 0 | 336 | | var emptyProfitLigaSettings = await HttpContext.RequestServices |
| | 0 | 337 | | .GetRequiredService<ApiSupplierSettingsService>() |
| | 0 | 338 | | .GetProfitLigaAsync(HttpContext.RequestAborted); |
| | 0 | 339 | | var emptyNikeiSettings = await HttpContext.RequestServices |
| | 0 | 340 | | .GetRequiredService<ApiSupplierSettingsService>() |
| | 0 | 341 | | .GetNikeiAsync(HttpContext.RequestAborted); |
| | 0 | 342 | | var emptyMikadoSettings = await HttpContext.RequestServices |
| | 0 | 343 | | .GetRequiredService<ApiSupplierSettingsService>() |
| | 0 | 344 | | .GetMikadoMskHod20Async(HttpContext.RequestAborted); |
| | 0 | 345 | | var emptyZZapMoscowSettings = await HttpContext.RequestServices |
| | 0 | 346 | | .GetRequiredService<ApiSupplierSettingsService>() |
| | 0 | 347 | | .GetZZapMoscowAsync(HttpContext.RequestAborted); |
| | 0 | 348 | | var emptyJapartsSettings = await HttpContext.RequestServices |
| | 0 | 349 | | .GetRequiredService<ApiSupplierSettingsService>() |
| | 0 | 350 | | .GetJapartsAsync(HttpContext.RequestAborted); |
| | 0 | 351 | | var emptyViewModel = new MultiplayEditOrderViewModel |
| | 0 | 352 | | { |
| | 0 | 353 | | RedirectPage = page, |
| | 0 | 354 | | Orders = new List<Order>(), |
| | 0 | 355 | | User = emptyUser, |
| | 0 | 356 | | Suppliers = (await SupplierDataServices.GetSuppliers()).OrderBy(s => s.Name).ToList(), |
| | 0 | 357 | | RateUSD = emptyRates.RateUsd, |
| | 0 | 358 | | RateEUR = emptyRates.RateEur, |
| | 0 | 359 | | RateBYN = emptyRates.RateByn, |
| | 0 | 360 | | ArmtekQuotationEnabled = emptyArmtekSettings.IsQuotationEnabled, |
| | 0 | 361 | | ArmtekByQuotationEnabled = emptyArmtekBySettings.IsQuotationEnabled, |
| | 0 | 362 | | ShateMQuotationEnabled = emptyShateMSettings.IsQuotationEnabled, |
| | 0 | 363 | | AvdQuotationEnabled = emptyAvdSettings.IsQuotationEnabled, |
| | 0 | 364 | | BergQuotationEnabled = emptyBergSettings.IsQuotationEnabled, |
| | 0 | 365 | | MotexQuotationEnabled = emptyMotexSettings.IsQuotationEnabled, |
| | 0 | 366 | | MlAutoQuotationEnabled = emptyMlAutoSettings.IsQuotationEnabled, |
| | 0 | 367 | | MlAutoRuQuotationEnabled = emptyMlAutoRuSettings.IsQuotationEnabled, |
| | 0 | 368 | | ForumAutoQuotationEnabled = emptyForumAutoSettings.IsQuotationEnabled, |
| | 0 | 369 | | ForumAutoInterpartsQuotationEnabled = emptyForumAutoInterpartsSettings.IsQuotationEnabled, |
| | 0 | 370 | | ForumAutoPiterQuotationEnabled = emptyForumAutoPiterSettings.IsQuotationEnabled, |
| | 0 | 371 | | ForumAutoRostovQuotationEnabled = emptyForumAutoRostovSettings.IsQuotationEnabled, |
| | 0 | 372 | | ForumAutoIstraQuotationEnabled = emptyForumAutoIstraSettings.IsQuotationEnabled, |
| | 0 | 373 | | ForumAutoDisplayName = emptyForumAutoSettings.DisplayName ?? ApiSupplierSettingsService.GetDefaultDi |
| | 0 | 374 | | ForumAutoInterpartsDisplayName = emptyForumAutoInterpartsSettings.DisplayName ?? ApiSupplierSettings |
| | 0 | 375 | | ForumAutoPiterDisplayName = emptyForumAutoPiterSettings.DisplayName ?? ApiSupplierSettingsService.Ge |
| | 0 | 376 | | ForumAutoRostovDisplayName = emptyForumAutoRostovSettings.DisplayName ?? ApiSupplierSettingsService. |
| | 0 | 377 | | ForumAutoIstraDisplayName = emptyForumAutoIstraSettings.DisplayName ?? ApiSupplierSettingsService.Ge |
| | 0 | 378 | | FavoritPartsQuotationEnabled = emptyFavoritPartsSettings.IsQuotationEnabled, |
| | 0 | 379 | | FavoritPartsIstraQuotationEnabled = emptyFavoritPartsIstraSettings.IsQuotationEnabled, |
| | 0 | 380 | | FavoritPartsRostovQuotationEnabled = emptyFavoritPartsRostovSettings.IsQuotationEnabled, |
| | 0 | 381 | | MoskvorechieIstraQuotationEnabled = emptyMoskvorechieIstraSettings.IsQuotationEnabled, |
| | 0 | 382 | | ProfitLigaQuotationEnabled = emptyProfitLigaSettings.IsQuotationEnabled, |
| | 0 | 383 | | NikeiQuotationEnabled = emptyNikeiSettings.IsQuotationEnabled, |
| | 0 | 384 | | MikadoMskHod20QuotationEnabled = emptyMikadoSettings.IsQuotationEnabled, |
| | 0 | 385 | | ZZapMoscowQuotationEnabled = emptyZZapMoscowSettings.IsQuotationEnabled, |
| | 0 | 386 | | JapartsQuotationEnabled = emptyJapartsSettings.IsQuotationEnabled, |
| | 0 | 387 | | ApiSupplierAllowedWarehouses = HttpContext.RequestServices.GetService<ApiSupplierWarehouseService>() |
| | 0 | 388 | | ? await emptyWhService.GetAllAllowedWarehouseIdsByProviderAsync(HttpContext.RequestAborted) |
| | 0 | 389 | | : new Dictionary<string, List<int>>(), |
| | 0 | 390 | | UniqueArticles = new Dictionary<string, int>(), |
| | 0 | 391 | | UniqueDeliveryCitys = new Dictionary<string, int>(), |
| | 0 | 392 | | UniqueNumbers = new Dictionary<string, int>(), |
| | 0 | 393 | | AppStatus = primaryStatus ?? new AppStatus() |
| | 0 | 394 | | }; |
| | | 395 | | |
| | 0 | 396 | | if (emptyUser.AccessProfileId != null) |
| | | 397 | | { |
| | 0 | 398 | | emptyViewModel.User.AccessProfile = await Context.AccessProfiles.FindAsync(emptyUser.AccessProfileId |
| | | 399 | | } |
| | | 400 | | |
| | 0 | 401 | | return View(config.ViewName, emptyViewModel); |
| | | 402 | | } |
| | | 403 | | |
| | 0 | 404 | | int[] idArray = ids.Split(',', StringSplitOptions.RemoveEmptyEntries) |
| | 0 | 405 | | .Select(s => int.TryParse(s.Trim(), out var v) ? v : (int?)null) |
| | 0 | 406 | | .Where(v => v.HasValue).Select(v => v!.Value).ToArray(); |
| | 0 | 407 | | idArray = await FilterOrderIdsByDataScopeAsync(idArray); |
| | | 408 | | // Уникальные id в порядке первого вхождения — один заказ = одна строка, без дубликатов |
| | 0 | 409 | | int[] orderIds = idArray.Distinct().ToArray(); |
| | 0 | 410 | | List<Order> ordersToTransaction = new(); |
| | 0 | 411 | | string? errorReason = null; |
| | 0 | 412 | | var errorCode = TransactionErrorCode.Unknown; |
| | | 413 | | |
| | | 414 | | // Удаляем заказы из корзины одним запросом вместо N |
| | 0 | 415 | | if (OrderCartServices != null) |
| | | 416 | | { |
| | 0 | 417 | | await OrderCartServices.DeleteByOrderIds(orderIds); |
| | | 418 | | } |
| | | 419 | | |
| | | 420 | | // ВАЖНО: Загружаем заказы БЕЗ ТРЕКИНГА — изменения статусов для отображения |
| | | 421 | | // НЕ сохранятся в БД при отмене транзакции. CartItemStatus подгружается пакетно. |
| | | 422 | | // Репозиторий возвращает уникальные заказы (дедупликация по Id из-за вложенных моделей). |
| | 0 | 423 | | var orders = await OrderServices.GetOrdersNoTracking(orderIds); |
| | 0 | 424 | | var ordersById = orders.ToDictionary(o => o.Id); |
| | | 425 | | |
| | | 426 | | // Итерация по orderIds сохраняет порядок выбора и гарантирует одну строку на заказ |
| | 0 | 427 | | foreach (var orderId in orderIds) |
| | | 428 | | { |
| | 0 | 429 | | if (!ordersById.TryGetValue(orderId, out var order)) |
| | | 430 | | continue; |
| | | 431 | | |
| | 0 | 432 | | if (validSourceStatusIds != null && fromStatusToTarget != null) |
| | | 433 | | { |
| | 0 | 434 | | var fromId = order.AppStatusId ?? 0; |
| | 0 | 435 | | if (!validSourceStatusIds.Contains(fromId)) |
| | | 436 | | { |
| | 0 | 437 | | var allowedNames = string.Join("', '", |
| | 0 | 438 | | transitions!.Select(t => t.FromStatus?.Name).Where(n => !string.IsNullOrEmpty(n)) |
| | 0 | 439 | | .Distinct()); |
| | 0 | 440 | | errorReason = $"Заказ {order.ShipmentNumber} имеет статус '{order.AppStatus?.Name}', " + |
| | 0 | 441 | | $"а должен быть одним из: '{allowedNames}'."; |
| | 0 | 442 | | errorCode = TransactionErrorCode.InvalidSourceStatus; |
| | 0 | 443 | | continue; |
| | | 444 | | } |
| | | 445 | | |
| | 0 | 446 | | if (fromStatusToTarget.TryGetValue(fromId, out var target)) |
| | | 447 | | { |
| | | 448 | | // Устанавливаем целевой статус для ОТОБРАЖЕНИЯ в форме |
| | | 449 | | // Заказ без трекинга - изменения НЕ сохранятся в БД |
| | 0 | 450 | | order.AppStatusId = target.ToStatus.Id; |
| | 0 | 451 | | order.AppStatus = target.ToStatus; |
| | | 452 | | } |
| | | 453 | | } |
| | 0 | 454 | | else if (config.RequiredSourceStatuses.Any()) |
| | | 455 | | { |
| | 0 | 456 | | if (!config.RequiredSourceStatuses.Contains(order.AppStatus?.Name ?? "")) |
| | | 457 | | { |
| | 0 | 458 | | errorReason = $"Заказ {order.ShipmentNumber} имеет статус '{order.AppStatus?.Name}', " + |
| | 0 | 459 | | $"а должен быть '{string.Join("' или '", config.RequiredSourceStatuses)}'."; |
| | 0 | 460 | | errorCode = TransactionErrorCode.InvalidSourceStatus; |
| | 0 | 461 | | continue; |
| | | 462 | | } |
| | | 463 | | } |
| | | 464 | | |
| | 0 | 465 | | var orderFilter = config.OrderFilter ?? TransactionConfig.StandardOrderFilter; |
| | 0 | 466 | | if (!orderFilter(order)) |
| | | 467 | | continue; |
| | | 468 | | |
| | 0 | 469 | | order.UpdatedBy = User.Identity?.Name; |
| | 0 | 470 | | ordersToTransaction.Add(order); |
| | | 471 | | } |
| | | 472 | | |
| | 0 | 473 | | if (!ordersToTransaction.Any() && errorReason == null) |
| | | 474 | | { |
| | 0 | 475 | | var transactionName = config.TransactionType.GetTransactionDisplayName(); |
| | 0 | 476 | | List<string>? allowedFromStatuses = null; |
| | 0 | 477 | | if (transitions != null && transitions.Any()) |
| | | 478 | | { |
| | 0 | 479 | | allowedFromStatuses = transitions |
| | 0 | 480 | | .Select(t => t.FromStatus?.Name) |
| | 0 | 481 | | .OfType<string>() |
| | 0 | 482 | | .Where(n => !string.IsNullOrWhiteSpace(n)) |
| | 0 | 483 | | .Distinct() |
| | 0 | 484 | | .ToList(); |
| | | 485 | | } |
| | 0 | 486 | | else if (config.RequiredSourceStatuses.Any()) |
| | | 487 | | { |
| | 0 | 488 | | allowedFromStatuses = config.RequiredSourceStatuses; |
| | | 489 | | } |
| | | 490 | | |
| | | 491 | | // Заказы дошли до этой точки (не найдены в ordersToTransaction), значит все они были |
| | | 492 | | // отсеяны либо проверкой статуса, либо OrderFilter — показываем юзеру их РЕАЛЬНЫЙ |
| | | 493 | | // текущий статус, а не только абстрактный список допустимых (иначе непонятно, почему |
| | | 494 | | // заказ, чей статус вроде бы есть в списке разрешённых, всё равно не прошёл). |
| | 0 | 495 | | var currentStatusLines = ordersById.Values |
| | 0 | 496 | | .Take(10) |
| | 0 | 497 | | .Select(o => $"Заказ {o.ShipmentNumber} сейчас в статусе «{o.AppStatus?.Name ?? "не указан"}»") |
| | 0 | 498 | | .ToList(); |
| | 0 | 499 | | var moreCount = ordersById.Count - currentStatusLines.Count; |
| | | 500 | | |
| | 0 | 501 | | var reasonParts = new List<string>(); |
| | 0 | 502 | | if (currentStatusLines.Count > 0) |
| | | 503 | | { |
| | 0 | 504 | | var line = string.Join(", ", currentStatusLines); |
| | 0 | 505 | | if (moreCount > 0) |
| | 0 | 506 | | line += $" и ещё {moreCount}"; |
| | 0 | 507 | | reasonParts.Add(line + "."); |
| | | 508 | | } |
| | 0 | 509 | | reasonParts.Add(allowedFromStatuses is { Count: > 0 } |
| | 0 | 510 | | ? $"Допустимые исходные статусы для «{transactionName}»: «{string.Join("», «", allowedFromStatuses)} |
| | 0 | 511 | | : $"Проверьте настройки переходов статусов (StatusTransition) для «{transactionName}»."); |
| | | 512 | | |
| | 0 | 513 | | errorReason = string.Join(" ", reasonParts); |
| | 0 | 514 | | errorCode = TransactionErrorCode.NoOrdersForTransaction; |
| | | 515 | | } |
| | | 516 | | |
| | 0 | 517 | | SetTransactionErrorViewData(errorCode, errorReason); |
| | | 518 | | |
| | 0 | 519 | | var conditionsMessage = transitions != null && transitions.Any() |
| | 0 | 520 | | ? string.Join("<br/>", transitions |
| | 0 | 521 | | .Select(t => t.Conditions) |
| | 0 | 522 | | .Where(c => !string.IsNullOrWhiteSpace(c)) |
| | 0 | 523 | | .Select(c => System.Net.WebUtility.HtmlEncode(c!.Trim())) |
| | 0 | 524 | | .Distinct()) |
| | 0 | 525 | | : null; |
| | 0 | 526 | | ViewData["TransactionConditionsMessage"] = conditionsMessage; |
| | | 527 | | |
| | | 528 | | // Загружаем данные из Bitrix если нужно |
| | 0 | 529 | | if (config.LoadBitrixData && BitrixStockRepository != null) |
| | | 530 | | { |
| | 0 | 531 | | await BitrixStockRepository.SetPricesForOrdersAsyncFromBitrix(ordersToTransaction); |
| | 0 | 532 | | var stockData = await BitrixStockRepository.SetStocksForOrdersAsyncFromBitrix(ordersToTransaction); |
| | 0 | 533 | | ViewData["StockData"] = stockData; |
| | | 534 | | } |
| | | 535 | | |
| | | 536 | | // === ОПТИМИЗАЦИЯ: Параллелизуем non-DB вызовы, DB-запросы выполняем последовательно === |
| | | 537 | | // Non-DB вызовы (API, кэш) — можно запускать параллельно |
| | 0 | 538 | | var userTask = UserCacheService.GetCachedUserAsync(User); |
| | 0 | 539 | | var rateTasks = CurrencyRatesViewBagService.StartFetch(); |
| | 0 | 540 | | var zasylTask = OneCDataManager != null |
| | 0 | 541 | | ? OneCDataManager.GetWarehouseListAsync() |
| | 0 | 542 | | : Task.FromResult(new List<global::Services.ApiServices._1CApi.Models.WarehouseResponse>()); |
| | | 543 | | |
| | | 544 | | // DB-запросы — последовательно (один DbContext не поддерживает параллельные запросы) |
| | 0 | 545 | | var suppliers = await SupplierDataServices.GetSuppliers(); |
| | 0 | 546 | | var armtekSettings = await HttpContext.RequestServices |
| | 0 | 547 | | .GetRequiredService<ApiSupplierSettingsService>() |
| | 0 | 548 | | .GetArmtekAsync(HttpContext.RequestAborted); |
| | 0 | 549 | | var armtekBySettings = await HttpContext.RequestServices |
| | 0 | 550 | | .GetRequiredService<ApiSupplierSettingsService>() |
| | 0 | 551 | | .GetArmtekByAsync(HttpContext.RequestAborted); |
| | 0 | 552 | | var shateMSettings = await HttpContext.RequestServices |
| | 0 | 553 | | .GetRequiredService<ApiSupplierSettingsService>() |
| | 0 | 554 | | .GetShateMAsync(HttpContext.RequestAborted); |
| | 0 | 555 | | var avdSettings = await HttpContext.RequestServices |
| | 0 | 556 | | .GetRequiredService<ApiSupplierSettingsService>() |
| | 0 | 557 | | .GetAvdAsync(HttpContext.RequestAborted); |
| | 0 | 558 | | var bergSettings = await HttpContext.RequestServices |
| | 0 | 559 | | .GetRequiredService<ApiSupplierSettingsService>() |
| | 0 | 560 | | .GetBergAsync(HttpContext.RequestAborted); |
| | 0 | 561 | | var motexSettings = await HttpContext.RequestServices |
| | 0 | 562 | | .GetRequiredService<ApiSupplierSettingsService>() |
| | 0 | 563 | | .GetMotexAsync(HttpContext.RequestAborted); |
| | 0 | 564 | | var mlAutoSettings = await HttpContext.RequestServices |
| | 0 | 565 | | .GetRequiredService<ApiSupplierSettingsService>() |
| | 0 | 566 | | .GetMlAutoAsync(HttpContext.RequestAborted); |
| | 0 | 567 | | var mlAutoRuSettings = await HttpContext.RequestServices |
| | 0 | 568 | | .GetRequiredService<ApiSupplierSettingsService>() |
| | 0 | 569 | | .GetMlAutoRuAsync(HttpContext.RequestAborted); |
| | 0 | 570 | | var forumAutoSettings = await HttpContext.RequestServices |
| | 0 | 571 | | .GetRequiredService<ApiSupplierSettingsService>() |
| | 0 | 572 | | .GetForumAutoAsync(HttpContext.RequestAborted); |
| | 0 | 573 | | var forumAutoInterpartsSettings = await HttpContext.RequestServices |
| | 0 | 574 | | .GetRequiredService<ApiSupplierSettingsService>() |
| | 0 | 575 | | .GetForumAutoInterpartsAsync(HttpContext.RequestAborted); |
| | 0 | 576 | | var forumAutoPiterSettings = await HttpContext.RequestServices |
| | 0 | 577 | | .GetRequiredService<ApiSupplierSettingsService>() |
| | 0 | 578 | | .GetForumAutoPiterAsync(HttpContext.RequestAborted); |
| | 0 | 579 | | var forumAutoRostovSettings = await HttpContext.RequestServices |
| | 0 | 580 | | .GetRequiredService<ApiSupplierSettingsService>() |
| | 0 | 581 | | .GetForumAutoRostovAsync(HttpContext.RequestAborted); |
| | 0 | 582 | | var forumAutoIstraSettings = await HttpContext.RequestServices |
| | 0 | 583 | | .GetRequiredService<ApiSupplierSettingsService>() |
| | 0 | 584 | | .GetForumAutoIstraAsync(HttpContext.RequestAborted); |
| | 0 | 585 | | var favoritPartsSettings = await HttpContext.RequestServices |
| | 0 | 586 | | .GetRequiredService<ApiSupplierSettingsService>() |
| | 0 | 587 | | .GetFavoritPartsAsync(HttpContext.RequestAborted); |
| | 0 | 588 | | var favoritPartsIstraSettings = await HttpContext.RequestServices |
| | 0 | 589 | | .GetRequiredService<ApiSupplierSettingsService>() |
| | 0 | 590 | | .GetFavoritPartsIstraAsync(HttpContext.RequestAborted); |
| | 0 | 591 | | var favoritPartsRostovSettings = await HttpContext.RequestServices |
| | 0 | 592 | | .GetRequiredService<ApiSupplierSettingsService>() |
| | 0 | 593 | | .GetFavoritPartsRostovAsync(HttpContext.RequestAborted); |
| | 0 | 594 | | var moskvorechieIstraSettings = await HttpContext.RequestServices |
| | 0 | 595 | | .GetRequiredService<ApiSupplierSettingsService>() |
| | 0 | 596 | | .GetMoskvorechieIstraAsync(HttpContext.RequestAborted); |
| | 0 | 597 | | var profitLigaSettings = await HttpContext.RequestServices |
| | 0 | 598 | | .GetRequiredService<ApiSupplierSettingsService>() |
| | 0 | 599 | | .GetProfitLigaAsync(HttpContext.RequestAborted); |
| | 0 | 600 | | var nikeiSettings = await HttpContext.RequestServices |
| | 0 | 601 | | .GetRequiredService<ApiSupplierSettingsService>() |
| | 0 | 602 | | .GetNikeiAsync(HttpContext.RequestAborted); |
| | 0 | 603 | | var mikadoSettings = await HttpContext.RequestServices |
| | 0 | 604 | | .GetRequiredService<ApiSupplierSettingsService>() |
| | 0 | 605 | | .GetMikadoMskHod20Async(HttpContext.RequestAborted); |
| | 0 | 606 | | var zzapMoscowSettings = await HttpContext.RequestServices |
| | 0 | 607 | | .GetRequiredService<ApiSupplierSettingsService>() |
| | 0 | 608 | | .GetZZapMoscowAsync(HttpContext.RequestAborted); |
| | 0 | 609 | | var japartsSettings = await HttpContext.RequestServices |
| | 0 | 610 | | .GetRequiredService<ApiSupplierSettingsService>() |
| | 0 | 611 | | .GetJapartsAsync(HttpContext.RequestAborted); |
| | 0 | 612 | | var (uniqueArticles, uniqueCities, uniqueNumbers) = |
| | 0 | 613 | | await GetUniqueCountsForOrdersAsync(ordersToTransaction); |
| | 0 | 614 | | var warehouses = WarehouseDataServices != null |
| | 0 | 615 | | ? await WarehouseDataServices.GetWarehouses() |
| | 0 | 616 | | : new List<Warehouse>(); |
| | 0 | 617 | | suppliers = await FilterByOrderScopeIdsAsync(suppliers, s => s.Id, "SupplierId"); |
| | 0 | 618 | | warehouses = await FilterByOrderScopeIdsAsync(warehouses, w => w.Id, "ShipmentWarehouseId"); |
| | 0 | 619 | | var expenseTypes = ExpenseTypeDataServices != null |
| | 0 | 620 | | ? await ExpenseTypeDataServices.GetExpenseTypes() |
| | 0 | 621 | | : new List<ExpenseType>(); |
| | 0 | 622 | | var expenses = OrderExtraExpenseDataServices != null && ordersToTransaction.Any() |
| | 0 | 623 | | ? await OrderExtraExpenseDataServices.GetByOrderIds(ordersToTransaction.Select(o => o.Id).ToList()) |
| | 0 | 624 | | : new Dictionary<int, List<OrderExtraExpense>>(); |
| | | 625 | | |
| | | 626 | | // Ожидаем non-DB задачи (скорее всего уже завершились пока шли DB-запросы) |
| | 0 | 627 | | await Task.WhenAll(userTask, rateTasks.Usd, rateTasks.Eur, rateTasks.Byn, zasylTask); |
| | | 628 | | |
| | 0 | 629 | | var pageUser = userTask.Result!; |
| | 0 | 630 | | var rates = rateTasks.ToSnapshot(); |
| | 0 | 631 | | var pageViewModel = new MultiplayEditOrderViewModel |
| | 0 | 632 | | { |
| | 0 | 633 | | RedirectPage = page, |
| | 0 | 634 | | Orders = ordersToTransaction.OrderBy(ot => ot.ShipmentNumber).ToList(), |
| | 0 | 635 | | User = pageUser, |
| | 0 | 636 | | Suppliers = suppliers.OrderBy(s => s.Name).ToList(), |
| | 0 | 637 | | RateUSD = rates.RateUsd, |
| | 0 | 638 | | RateEUR = rates.RateEur, |
| | 0 | 639 | | RateBYN = rates.RateByn, |
| | 0 | 640 | | ArmtekQuotationEnabled = armtekSettings.IsQuotationEnabled, |
| | 0 | 641 | | ArmtekByQuotationEnabled = armtekBySettings.IsQuotationEnabled, |
| | 0 | 642 | | ShateMQuotationEnabled = shateMSettings.IsQuotationEnabled, |
| | 0 | 643 | | AvdQuotationEnabled = avdSettings.IsQuotationEnabled, |
| | 0 | 644 | | BergQuotationEnabled = bergSettings.IsQuotationEnabled, |
| | 0 | 645 | | MotexQuotationEnabled = motexSettings.IsQuotationEnabled, |
| | 0 | 646 | | MlAutoQuotationEnabled = mlAutoSettings.IsQuotationEnabled, |
| | 0 | 647 | | MlAutoRuQuotationEnabled = mlAutoRuSettings.IsQuotationEnabled, |
| | 0 | 648 | | ForumAutoQuotationEnabled = forumAutoSettings.IsQuotationEnabled, |
| | 0 | 649 | | ForumAutoInterpartsQuotationEnabled = forumAutoInterpartsSettings.IsQuotationEnabled, |
| | 0 | 650 | | ForumAutoPiterQuotationEnabled = forumAutoPiterSettings.IsQuotationEnabled, |
| | 0 | 651 | | ForumAutoRostovQuotationEnabled = forumAutoRostovSettings.IsQuotationEnabled, |
| | 0 | 652 | | ForumAutoIstraQuotationEnabled = forumAutoIstraSettings.IsQuotationEnabled, |
| | 0 | 653 | | ForumAutoDisplayName = forumAutoSettings.DisplayName ?? ApiSupplierSettingsService.GetDefaultDisplayName |
| | 0 | 654 | | ForumAutoInterpartsDisplayName = forumAutoInterpartsSettings.DisplayName ?? ApiSupplierSettingsService.G |
| | 0 | 655 | | ForumAutoPiterDisplayName = forumAutoPiterSettings.DisplayName ?? ApiSupplierSettingsService.GetDefaultD |
| | 0 | 656 | | ForumAutoRostovDisplayName = forumAutoRostovSettings.DisplayName ?? ApiSupplierSettingsService.GetDefaul |
| | 0 | 657 | | ForumAutoIstraDisplayName = forumAutoIstraSettings.DisplayName ?? ApiSupplierSettingsService.GetDefaultD |
| | 0 | 658 | | FavoritPartsQuotationEnabled = favoritPartsSettings.IsQuotationEnabled, |
| | 0 | 659 | | FavoritPartsIstraQuotationEnabled = favoritPartsIstraSettings.IsQuotationEnabled, |
| | 0 | 660 | | FavoritPartsRostovQuotationEnabled = favoritPartsRostovSettings.IsQuotationEnabled, |
| | 0 | 661 | | MoskvorechieIstraQuotationEnabled = moskvorechieIstraSettings.IsQuotationEnabled, |
| | 0 | 662 | | ProfitLigaQuotationEnabled = profitLigaSettings.IsQuotationEnabled, |
| | 0 | 663 | | NikeiQuotationEnabled = nikeiSettings.IsQuotationEnabled, |
| | 0 | 664 | | MikadoMskHod20QuotationEnabled = mikadoSettings.IsQuotationEnabled, |
| | 0 | 665 | | ZZapMoscowQuotationEnabled = zzapMoscowSettings.IsQuotationEnabled, |
| | 0 | 666 | | JapartsQuotationEnabled = japartsSettings.IsQuotationEnabled, |
| | 0 | 667 | | ApiSupplierAllowedWarehouses = HttpContext.RequestServices.GetService<ApiSupplierWarehouseService>() is |
| | 0 | 668 | | ? await whService.GetAllAllowedWarehouseIdsByProviderAsync(HttpContext.RequestAborted) |
| | 0 | 669 | | : new Dictionary<string, List<int>>(), |
| | 0 | 670 | | UniqueArticles = uniqueArticles, |
| | 0 | 671 | | UniqueDeliveryCitys = uniqueCities, |
| | 0 | 672 | | UniqueNumbers = uniqueNumbers, |
| | 0 | 673 | | AppStatus = primaryStatus ?? filteredStatuses.FirstOrDefault() ?? new AppStatus(), |
| | 0 | 674 | | ZasylValues = OneCDataManager != null |
| | 0 | 675 | | ? await FilterWarehouseNamesByOrderScopeAsync( |
| | 0 | 676 | | zasylTask.Result.Select(w => w.Name).Where(n => !string.IsNullOrWhiteSpace(n)).Select(n => n!)) |
| | 0 | 677 | | : new List<string>(), |
| | 0 | 678 | | Warehouses = warehouses |
| | 0 | 679 | | }; |
| | | 680 | | |
| | | 681 | | // Старт трекинга времени проведения: при открытии страницы создания транзакции. |
| | 0 | 682 | | var trackingService = HttpContext.RequestServices.GetService<ITransactionTimeTrackingService>(); |
| | 0 | 683 | | if (trackingService != null && pageViewModel.Orders.Any()) |
| | | 684 | | { |
| | 0 | 685 | | var accessProfileId = pageViewModel.User?.AccessProfileId; |
| | 0 | 686 | | var clientIp = HttpContext.Connection.RemoteIpAddress?.ToString(); |
| | 0 | 687 | | var userAgent = Request.Headers.UserAgent.ToString(); |
| | 0 | 688 | | var sourceUrl = $"{Request.Path}{Request.QueryString}"; |
| | 0 | 689 | | pageViewModel.TrackingKey = await trackingService.StartSessionAsync( |
| | 0 | 690 | | User.Identity?.Name ?? "unknown", |
| | 0 | 691 | | accessProfileId, |
| | 0 | 692 | | config.TransactionType, |
| | 0 | 693 | | pageViewModel.Orders.Select(o => o.Id), |
| | 0 | 694 | | sourceUrl, |
| | 0 | 695 | | clientIp, |
| | 0 | 696 | | userAgent); |
| | | 697 | | } |
| | | 698 | | |
| | 0 | 699 | | pageViewModel.OrderListFieldReadAccess = await BuildOrderListFieldReadMapAsync(pageUser.Id); |
| | | 700 | | |
| | | 701 | | // Типы расходов |
| | 0 | 702 | | if (ExpenseTypeDataServices != null) |
| | | 703 | | { |
| | 0 | 704 | | ViewBag.ExpenseTypes = expenseTypes.Where(et => et.IsActive).OrderBy(et => et.Name).ToList(); |
| | | 705 | | } |
| | | 706 | | |
| | | 707 | | // Существующие расходы для заказов |
| | 0 | 708 | | ViewBag.ExistingExpenses = expenses; |
| | | 709 | | |
| | | 710 | | // Применяем кастомную настройку ViewModel если задана |
| | 0 | 711 | | config.ConfigureViewModel?.Invoke(pageViewModel); |
| | 0 | 712 | | if (config.ConfigureViewModelAsync != null) |
| | 0 | 713 | | await config.ConfigureViewModelAsync(pageViewModel); |
| | | 714 | | |
| | | 715 | | // Панель "Отменить в Ozon", раскрывающаяся при выборе статуса "Отменен" в per-row select |
| | 0 | 716 | | if (config.SupportsOzonCancelPanel && OzonCancellationService != null && pageViewModel.Orders.Count > 0) |
| | | 717 | | { |
| | 0 | 718 | | pageViewModel.OzonPostingCancelReasons = |
| | 0 | 719 | | await OzonCancellationService.LoadCancelReasonsAsync(pageViewModel.Orders); |
| | 0 | 720 | | ViewBag.OzonCancelPanelLinkedToStatusSelect = true; |
| | | 721 | | } |
| | | 722 | | |
| | 0 | 723 | | if (config.LoadWarehouseListForSelector && OneCDataManager != null) |
| | | 724 | | { |
| | 0 | 725 | | var bitrixWarehouseNames = zasylTask.Result |
| | 0 | 726 | | .Select(w => w.Name) |
| | 0 | 727 | | .Where(n => !string.IsNullOrWhiteSpace(n)) |
| | 0 | 728 | | .Select(n => n!) |
| | 0 | 729 | | .OrderBy(n => n) |
| | 0 | 730 | | .ToList(); |
| | 0 | 731 | | ViewData["BitrixWarehousesList"] = await FilterBitrixWarehouseNamesByScope(bitrixWarehouseNames); |
| | | 732 | | } |
| | | 733 | | |
| | 0 | 734 | | if (config.ClearPurchasePriceOnLoad && pageViewModel.Orders.Count > 0) |
| | | 735 | | { |
| | 0 | 736 | | var warehouseC0ByOrderId = await LoadWarehouseStockC0MapAsync(pageViewModel.Orders); |
| | 0 | 737 | | pageViewModel.WarehouseStockC0ByOrderId = warehouseC0ByOrderId; |
| | 0 | 738 | | ApplyWarehouseStockC0ToOrders(pageViewModel.Orders, warehouseC0ByOrderId); |
| | | 739 | | } |
| | | 740 | | |
| | | 741 | | // Запускаем фоновое кэширование |
| | 0 | 742 | | RunBackgroundCacheTask(pageViewModel.Orders); |
| | | 743 | | |
| | 0 | 744 | | return View(config.ViewName, pageViewModel); |
| | 0 | 745 | | } |
| | | 746 | | |
| | | 747 | | /// <summary> |
| | | 748 | | /// Хук перед проведением транзакции. Возвращает текст ошибки или null при успехе. |
| | | 749 | | /// </summary> |
| | | 750 | | protected virtual Task<string?> ExecutePreTransactionAsync(List<Order> orders) => |
| | 0 | 751 | | Task.FromResult<string?>(null); |
| | | 752 | | |
| | | 753 | | /// <summary> |
| | | 754 | | /// Дополнительный HTML-блок к сообщению об успешной транзакции (TempData TransactionResult). |
| | | 755 | | /// </summary> |
| | | 756 | | protected virtual string? GetPostTransactionSuccessAppendix() => |
| | 0 | 757 | | OzonOrderCancellationService.BuildSuccessHtml(OzonCancelSuccessEntries); |
| | | 758 | | |
| | | 759 | | /// <summary> |
| | | 760 | | /// Дополнительный async-хук для обогащения сообщения об успешной транзакции. |
| | | 761 | | /// Выполняется только после успешного проведения в системе (changeCount > 0). |
| | | 762 | | /// </summary> |
| | | 763 | | protected virtual Task<string?> GetPostTransactionSuccessAppendixAsync( |
| | | 764 | | List<Order> ordersToTransaction, |
| | 0 | 765 | | TransactionConfig config) => Task.FromResult<string?>(null); |
| | | 766 | | |
| | | 767 | | /// <summary> |
| | | 768 | | /// Заказы, успешно отменённые в Ozon в рамках текущей транзакции |
| | | 769 | | /// (заполняется при <see cref="TransactionConfig.SupportsOzonCancelPanel"/>). |
| | | 770 | | /// </summary> |
| | 0 | 771 | | protected List<OzonCancelSuccessEntry> OzonCancelSuccessEntries { get; private set; } = new(); |
| | | 772 | | |
| | | 773 | | /// <summary> |
| | | 774 | | /// Сообщение об успехе последнего вызова CreateTransactionPostBase с suppressTempData=true. |
| | | 775 | | /// </summary> |
| | 0 | 776 | | protected string? LastTransactionSuccessMessage { get; private set; } |
| | | 777 | | |
| | | 778 | | /// <summary> |
| | | 779 | | /// Базовый POST метод для создания транзакции |
| | | 780 | | /// </summary> |
| | | 781 | | protected async Task<IActionResult> CreateTransactionPostBase( |
| | | 782 | | List<Order> orders, |
| | | 783 | | string userName, |
| | | 784 | | string? comment, |
| | | 785 | | int page, |
| | | 786 | | string? deletedOrders, |
| | | 787 | | Dictionary<int, string?>? orderSupplierMap = null, |
| | | 788 | | bool processIn1C = false, |
| | | 789 | | string? warehouseSender = null, |
| | | 790 | | string? warehouseRecipient = null, |
| | | 791 | | Dictionary<int, string>? recipientWarehouseByOrder = null, |
| | | 792 | | Dictionary<int, string>? senderWarehouseByOrder = null, |
| | | 793 | | TransactionConfig? configOverride = null, |
| | | 794 | | bool suppressTempData = false) |
| | | 795 | | { |
| | 0 | 796 | | var userCommentOnly = TransactionCommentNormalizer.NormalizeUserComment(comment); |
| | 0 | 797 | | comment = userCommentOnly; |
| | 0 | 798 | | var config = configOverride ?? GetTransactionConfig(); |
| | 0 | 799 | | var scheduleAiComment = string.IsNullOrEmpty(userCommentOnly); |
| | 0 | 800 | | TransactionCommentAiContextBuilder? aiCommentBuilder = null; |
| | 0 | 801 | | if (scheduleAiComment) |
| | | 802 | | { |
| | 0 | 803 | | var shipInOzon = Request.Form.TryGetValue("shipInOzon", out var shipValues) && |
| | 0 | 804 | | shipValues.Any(v => string.Equals(v, "true", StringComparison.OrdinalIgnoreCase)); |
| | 0 | 805 | | aiCommentBuilder = new TransactionCommentAiContextBuilder(config.TransactionType, userName, DateTime.Utc |
| | 0 | 806 | | .WithFlags(processIn1C, shipInOzon); |
| | 0 | 807 | | if (OzonCancelSuccessEntries is { Count: > 0 }) |
| | | 808 | | { |
| | 0 | 809 | | aiCommentBuilder.WithOzonCancelNotes(OzonCancelSuccessEntries.Select(e => |
| | 0 | 810 | | { |
| | 0 | 811 | | var note = e.ShipmentNumber; |
| | 0 | 812 | | if (!string.IsNullOrWhiteSpace(e.ReasonTitle)) |
| | 0 | 813 | | note += $", причина: {e.ReasonTitle}"; |
| | 0 | 814 | | if (!string.IsNullOrWhiteSpace(e.ReasonMessage)) |
| | 0 | 815 | | note += $" ({e.ReasonMessage})"; |
| | 0 | 816 | | return note; |
| | 0 | 817 | | })); |
| | | 818 | | } |
| | | 819 | | } |
| | | 820 | | |
| | 0 | 821 | | var trackingKey = Request.Form["TrackingKey"].FirstOrDefault(); |
| | 0 | 822 | | var oneCOrganizationInn = Request.Form["oneCOrganizationInn"].FirstOrDefault(); |
| | 0 | 823 | | var createdTransactionId = 0; |
| | 0 | 824 | | var stockCheck = StockCheckResult.Empty; |
| | 0 | 825 | | var createDraftsForStockShortage = false; |
| | | 826 | | |
| | 0 | 827 | | if (orders == null) |
| | | 828 | | { |
| | 0 | 829 | | SetTransactionErrorTempData( |
| | 0 | 830 | | TransactionErrorCode.NoOrdersForTransaction, |
| | 0 | 831 | | "Не удалось провести выбранные заказы.<br>Было передано слишком большое количество записей."); |
| | 0 | 832 | | return RedirectToAction("Index", "Orders"); |
| | | 833 | | } |
| | | 834 | | |
| | 0 | 835 | | orders = await FilterOrdersByDataScopeAsync(orders); |
| | | 836 | | |
| | 0 | 837 | | await ReportStageAsync("received", "Данные формы приняты", |
| | 0 | 838 | | orders.Count == 1 ? "1 заказ" : $"заказов: {orders.Count}"); |
| | | 839 | | |
| | | 840 | | // Определяем список удаленных заказов и снимаем трекинг для них |
| | | 841 | | // Это предотвращает сохранение изменений статуса и других полей для удаленных заказов |
| | 0 | 842 | | if (!string.IsNullOrEmpty(deletedOrders)) |
| | | 843 | | { |
| | 0 | 844 | | var deletedOrderIds = DeletedOrderIds.Parse(deletedOrders); |
| | | 845 | | |
| | | 846 | | // Снимаем трекинг для всех удаленных заказов, чтобы изменения не сохранялись |
| | 0 | 847 | | foreach (var deletedOrderId in deletedOrderIds) |
| | | 848 | | { |
| | | 849 | | // Ищем заказ в отслеживаемых сущностях контекста |
| | 0 | 850 | | var trackedOrder = Context.ChangeTracker.Entries<Order>() |
| | 0 | 851 | | .FirstOrDefault(e => e.Entity.Id == deletedOrderId)?.Entity; |
| | | 852 | | |
| | 0 | 853 | | if (trackedOrder != null) |
| | | 854 | | { |
| | | 855 | | // Снимаем трекинг для этого заказа |
| | 0 | 856 | | Context.Entry(trackedOrder).State = EntityState.Detached; |
| | | 857 | | } |
| | | 858 | | } |
| | | 859 | | |
| | | 860 | | // Удаляем исключенные заказы из списка для обработки |
| | 0 | 861 | | orders = orders.Where(o => !deletedOrderIds.Contains(o.Id)).ToList(); |
| | | 862 | | } |
| | | 863 | | |
| | | 864 | | // Очищаем пустые дополнительные расходы перед сохранением |
| | | 865 | | // Это гарантирует, что пустые элементы не попадут в базу данных |
| | 0 | 866 | | foreach (var order in orders) |
| | | 867 | | { |
| | 0 | 868 | | if (order.ExtraExpenses != null && order.ExtraExpenses.Any()) |
| | | 869 | | { |
| | | 870 | | // Удаляем все расходы, где не заполнены оба поля (тип и сумма) |
| | 0 | 871 | | order.ExtraExpenses = order.ExtraExpenses |
| | 0 | 872 | | .Where(e => e != null && e.ExpenseTypeId > 0 && e.Amount > 0) |
| | 0 | 873 | | .ToList(); |
| | | 874 | | |
| | | 875 | | // Если список стал пустым, устанавливаем null |
| | 0 | 876 | | if (!order.ExtraExpenses.Any()) |
| | | 877 | | { |
| | 0 | 878 | | order.ExtraExpenses = []; |
| | | 879 | | } |
| | | 880 | | } |
| | | 881 | | } |
| | | 882 | | |
| | | 883 | | try |
| | | 884 | | { |
| | 0 | 885 | | if (orders.Count != 0) |
| | | 886 | | { |
| | 0 | 887 | | if (config.SupportsOzonCancelPanel && OzonCancellationService != null) |
| | | 888 | | { |
| | 0 | 889 | | var cancelRequests = OzonOrderCancellationService.ParseFormRequests(Request.Form); |
| | 0 | 890 | | if (cancelRequests.Count > 0) |
| | | 891 | | { |
| | 0 | 892 | | var ordersById = orders.ToDictionary(o => o.Id); |
| | 0 | 893 | | await ReportStageAsync("ozon-cancel", "Отменяем отправления в Ozon", |
| | 0 | 894 | | cancelRequests.Count == 1 ? "1 отправление" : $"отправлений: {cancelRequests.Count}"); |
| | 0 | 895 | | var cancelResult = await OzonCancellationService.CancelOrdersAsync(cancelRequests, ordersByI |
| | 0 | 896 | | if (cancelResult.Errors.Count > 0) |
| | 0 | 897 | | return Json(new { error = string.Join("<br/>", cancelResult.Errors) }); |
| | | 898 | | |
| | 0 | 899 | | OzonCancelSuccessEntries = cancelResult.SuccessEntries; |
| | 0 | 900 | | } |
| | 0 | 901 | | } |
| | | 902 | | |
| | | 903 | | // Остатки спрашиваем до изменения статусов: при нехватке 1С создаст документ, |
| | | 904 | | // но откажет в проведении, не указав проблемные позиции. |
| | 0 | 905 | | if (processIn1C && config.CheckStockBefore1C) |
| | | 906 | | { |
| | 0 | 907 | | var stockOrders = await TransactionStockCheckLineBuilder.LoadForStockCheckAsync( |
| | 0 | 908 | | Context, orders.Select(o => o.Id)); |
| | | 909 | | |
| | 0 | 910 | | await ReportStageAsync("stock-check", "Проверяем остатки в 1С"); |
| | 0 | 911 | | stockCheck = await OneCStockGate.RunAsync( |
| | 0 | 912 | | HttpContext, |
| | 0 | 913 | | TransactionStockCheckLineBuilder.FromExplicitWarehouses( |
| | 0 | 914 | | stockOrders, |
| | 0 | 915 | | senderWarehouseByOrder, |
| | 0 | 916 | | warehouseSender, |
| | 0 | 917 | | TransactionStockCheckLineBuilder.QuantitiesFromForm(orders))); |
| | 0 | 918 | | await OneCStockGate.ReportAsync(HttpContext, stockCheck, "Проведение документа 1С"); |
| | | 919 | | |
| | | 920 | | // Для «Возврата поставщику» дефицитные позиции уходят отдельным черновиком. |
| | | 921 | | // Остальные типы транзакций сохраняют прежнее безопасное поведение: 1С не вызывается. |
| | 0 | 922 | | createDraftsForStockShortage = |
| | 0 | 923 | | config.TransactionType == TransactionType.ReturnToSupplier && stockCheck.HasBlocking; |
| | 0 | 924 | | if (!createDraftsForStockShortage && OneCStockGate.TrySkipOneC(TempData, stockCheck)) |
| | 0 | 925 | | processIn1C = false; |
| | 0 | 926 | | } |
| | | 927 | | |
| | 0 | 928 | | var preTransactionError = await ExecutePreTransactionAsync(orders); |
| | 0 | 929 | | if (!string.IsNullOrWhiteSpace(preTransactionError)) |
| | 0 | 930 | | return Json(new { error = preTransactionError }); |
| | | 931 | | |
| | 0 | 932 | | DateTime createAt = DateTime.Now; |
| | 0 | 933 | | int changeCount = 0; |
| | 0 | 934 | | string dateTime = ""; |
| | 0 | 935 | | List<Order> ordersToUpdate = new(); |
| | | 936 | | |
| | | 937 | | // Сохраняем статусы до перехода (из БД) для записи истории после успешного проведения |
| | 0 | 938 | | var orderIds = orders.Select(o => o.Id).Distinct().ToList(); |
| | 0 | 939 | | var fromStatusByOrderId = await Context.Orders.AsNoTracking() |
| | 0 | 940 | | .Where(o => orderIds.Contains(o.Id)) |
| | 0 | 941 | | .Select(o => new { o.Id, FromStatusId = o.AppStatusId ?? 0 }) |
| | 0 | 942 | | .ToDictionaryAsync(x => x.Id, x => x.FromStatusId); |
| | | 943 | | |
| | | 944 | | // Подготавливаем заказы для транзакции |
| | 0 | 945 | | await ReportStageAsync("orders", "Меняем статусы заказов", |
| | 0 | 946 | | orders.Count == 1 ? "1 заказ" : $"заказов: {orders.Count}"); |
| | 0 | 947 | | var submittedFields = SubmittedOrderFields.FromForm(Request.HasFormContentType ? Request.Form : null |
| | 0 | 948 | | foreach (var order in orders) |
| | | 949 | | { |
| | 0 | 950 | | ordersToUpdate.Add(await OrderServices.TransactOrder(order, submittedFields.For(order.Id))); |
| | | 951 | | } |
| | | 952 | | |
| | | 953 | | // Дополнительная пост-обработка заказов, специфичная для конкретного типа транзакции |
| | 0 | 954 | | if (config.AfterTransactOrdersAsync != null) |
| | | 955 | | { |
| | 0 | 956 | | await config.AfterTransactOrdersAsync(ordersToUpdate); |
| | | 957 | | } |
| | | 958 | | |
| | | 959 | | List<Order> ordersToTransaction; |
| | 0 | 960 | | string oneCResult = string.Empty; |
| | 0 | 961 | | IEnumerable<string> oneCHash = Array.Empty<string>(); |
| | 0 | 962 | | List<OneCResponse> oneCPostingResponses = new(); |
| | 0 | 963 | | List<(string Sender, string Recipient, List<Order> Orders, List<OneCResponse> Result)> oneCGroupedDe |
| | 0 | 964 | | var transitions = config.UseStatusTransitions && StatusTransitionDataServices != null |
| | 0 | 965 | | ? await StatusTransitionDataServices.GetByTransactionType(config.TransactionType) |
| | 0 | 966 | | : new List<StatusTransition>(); |
| | | 967 | | |
| | 0 | 968 | | if (transitions.Any()) |
| | | 969 | | { |
| | 0 | 970 | | var validTargetStatusIds = transitions.Select(t => t.ToStatusId).ToHashSet(); |
| | 0 | 971 | | ordersToTransaction = ordersToUpdate |
| | 0 | 972 | | .Where(o => o.AppStatus != null && validTargetStatusIds.Contains(o.AppStatus.Id)) |
| | 0 | 973 | | .ToList(); |
| | | 974 | | } |
| | | 975 | | else |
| | | 976 | | { |
| | 0 | 977 | | AppStatus? primaryStatus = null; |
| | 0 | 978 | | if (!string.IsNullOrEmpty(config.PrimaryStatusName)) |
| | 0 | 979 | | primaryStatus = await AppStatusServices.GetAppStatusAsync(new AppStatus |
| | 0 | 980 | | { Name = config.PrimaryStatusName }); |
| | 0 | 981 | | AppStatus? cancelledStatus = null; |
| | 0 | 982 | | if (config.IncludeCancelledOrders) |
| | 0 | 983 | | cancelledStatus = |
| | 0 | 984 | | await AppStatusServices.GetAppStatusAsync(new AppStatus { Name = "Отменен" }); |
| | | 985 | | |
| | 0 | 986 | | if (string.IsNullOrEmpty(config.PrimaryStatusName)) |
| | | 987 | | { |
| | 0 | 988 | | ordersToTransaction = ordersToUpdate.ToList(); |
| | | 989 | | } |
| | | 990 | | else |
| | | 991 | | { |
| | 0 | 992 | | ordersToTransaction = ordersToUpdate.Where(o => |
| | 0 | 993 | | (primaryStatus != null && o.AppStatus != null && o.AppStatus.Id == primaryStatus.Id) || |
| | 0 | 994 | | (cancelledStatus != null && o.AppStatus != null && |
| | 0 | 995 | | o.AppStatus.Id == cancelledStatus.Id)).ToList(); |
| | | 996 | | } |
| | 0 | 997 | | } |
| | | 998 | | |
| | | 999 | | // 1С: перемещение с указанными складами (напр. «Возврат поставщику») |
| | 0 | 1000 | | if (processIn1C && config.UseCustomWarehousesFor1C && OneCTransferManager != null) |
| | | 1001 | | { |
| | 0 | 1002 | | await ReportStageAsync("onec", "Создаём документы в 1С"); |
| | 0 | 1003 | | var hasPerOrderSender = senderWarehouseByOrder is { Count: > 0 }; |
| | 0 | 1004 | | var hasPerOrderRecipient = recipientWarehouseByOrder is { Count: > 0 }; |
| | 0 | 1005 | | if (string.IsNullOrWhiteSpace(warehouseSender) && !hasPerOrderSender) |
| | | 1006 | | { |
| | 0 | 1007 | | throw new TransactionException( |
| | 0 | 1008 | | TransactionErrorCode.OneCSenderWarehouseMissing, |
| | 0 | 1009 | | "Не указан склад отправителя."); |
| | | 1010 | | } |
| | | 1011 | | |
| | 0 | 1012 | | if (string.IsNullOrWhiteSpace(warehouseRecipient) && !hasPerOrderRecipient) |
| | | 1013 | | { |
| | 0 | 1014 | | throw new TransactionException( |
| | 0 | 1015 | | TransactionErrorCode.OneCRecipientWarehouseMissing, |
| | 0 | 1016 | | "Не указан склад получателя."); |
| | | 1017 | | } |
| | | 1018 | | |
| | 0 | 1019 | | if (!hasPerOrderSender && !hasPerOrderRecipient && |
| | 0 | 1020 | | !string.IsNullOrWhiteSpace(warehouseRecipient) && |
| | 0 | 1021 | | string.Equals(warehouseSender!.Trim(), warehouseRecipient.Trim(), |
| | 0 | 1022 | | StringComparison.OrdinalIgnoreCase)) |
| | | 1023 | | { |
| | 0 | 1024 | | throw new TransactionException( |
| | 0 | 1025 | | TransactionErrorCode.OneCWarehousesMustDiffer, |
| | 0 | 1026 | | "Склад отправителя и получателя не могут совпадать."); |
| | | 1027 | | } |
| | | 1028 | | |
| | | 1029 | | try |
| | | 1030 | | { |
| | 0 | 1031 | | var transferResult = new List<OneCResponse>(); |
| | 0 | 1032 | | var groupedMode = config.TransactionType == TransactionType.CustomerReturn && hasPerOrderRec |
| | 0 | 1033 | | config.TransactionType == TransactionType.ReturnToSupplier && |
| | 0 | 1034 | | hasPerOrderSender && hasPerOrderRecipient; |
| | | 1035 | | |
| | 0 | 1036 | | if (groupedMode) |
| | | 1037 | | { |
| | 0 | 1038 | | var recipientMap = recipientWarehouseByOrder!; |
| | 0 | 1039 | | var blockingOrderIds = createDraftsForStockShortage |
| | 0 | 1040 | | ? OneCStockGate.GetBlockingOrderIds(stockCheck) |
| | 0 | 1041 | | : []; |
| | 0 | 1042 | | var ordersWithoutRecipient = ordersToTransaction |
| | 0 | 1043 | | .Where(o => !recipientMap.TryGetValue(o.Id, out var recipient) || string.IsNullOrWhi |
| | 0 | 1044 | | .Select(o => o.ShipmentNumber) |
| | 0 | 1045 | | .ToList(); |
| | 0 | 1046 | | if (ordersWithoutRecipient.Count > 0) |
| | | 1047 | | { |
| | 0 | 1048 | | var noRecipientMessage = |
| | 0 | 1049 | | $"Не удалось определить склад получателя для заказов: {string.Join(", ", ordersW |
| | 0 | 1050 | | throw new TransactionException( |
| | 0 | 1051 | | TransactionErrorCode.OneCRecipientWarehouseMissing, |
| | 0 | 1052 | | noRecipientMessage); |
| | | 1053 | | } |
| | | 1054 | | |
| | 0 | 1055 | | var ordersWithoutSender = hasPerOrderSender |
| | 0 | 1056 | | ? ordersToTransaction |
| | 0 | 1057 | | .Where(o => !senderWarehouseByOrder!.TryGetValue(o.Id, out var sender) || string |
| | 0 | 1058 | | .Select(o => o.ShipmentNumber) |
| | 0 | 1059 | | .ToList() |
| | 0 | 1060 | | : []; |
| | 0 | 1061 | | if (ordersWithoutSender.Count > 0) |
| | | 1062 | | { |
| | 0 | 1063 | | throw new TransactionException( |
| | 0 | 1064 | | TransactionErrorCode.OneCSenderWarehouseMissing, |
| | 0 | 1065 | | $"Не удалось определить склад отправителя для заказов: {string.Join(", ", orders |
| | | 1066 | | } |
| | | 1067 | | |
| | 0 | 1068 | | var groupedOrders = ordersToTransaction |
| | 0 | 1069 | | .GroupBy(o => $"{(hasPerOrderSender ? senderWarehouseByOrder![o.Id] : warehouseSende |
| | 0 | 1070 | | .ToList(); |
| | | 1071 | | |
| | 0 | 1072 | | foreach (var group in groupedOrders) |
| | | 1073 | | { |
| | 0 | 1074 | | var firstOrder = group.First(); |
| | 0 | 1075 | | var sender = (hasPerOrderSender ? senderWarehouseByOrder![firstOrder.Id] : warehouse |
| | 0 | 1076 | | var recipient = recipientMap[firstOrder.Id].Trim(); |
| | 0 | 1077 | | if (string.Equals(sender, recipient, StringComparison.OrdinalIgnoreCase)) |
| | | 1078 | | { |
| | 0 | 1079 | | var sameWarehouseMessage = |
| | 0 | 1080 | | $"Склад отправителя и получателя не могут совпадать (направление '{sender} → |
| | 0 | 1081 | | throw new TransactionException( |
| | 0 | 1082 | | TransactionErrorCode.OneCWarehousesMustDiffer, |
| | 0 | 1083 | | sameWarehouseMessage); |
| | | 1084 | | } |
| | | 1085 | | |
| | 0 | 1086 | | var groupOrders = group.ToList(); |
| | 0 | 1087 | | var groupResult = new List<OneCResponse>(); |
| | 0 | 1088 | | var ordersToPost = groupOrders |
| | 0 | 1089 | | .Where(order => !blockingOrderIds.Contains(order.Id)) |
| | 0 | 1090 | | .ToList(); |
| | 0 | 1091 | | if (ordersToPost.Count > 0) |
| | | 1092 | | { |
| | 0 | 1093 | | groupResult.AddRange(await OneCTransferManager.TransferStockWithCustomWarehouses |
| | 0 | 1094 | | ordersToPost, |
| | 0 | 1095 | | sender, |
| | 0 | 1096 | | recipient, |
| | 0 | 1097 | | config.TransactionType, |
| | 0 | 1098 | | userName: userName, |
| | 0 | 1099 | | orgInnOverride: oneCOrganizationInn)); |
| | | 1100 | | } |
| | | 1101 | | |
| | 0 | 1102 | | var ordersForDraft = groupOrders |
| | 0 | 1103 | | .Where(order => blockingOrderIds.Contains(order.Id)) |
| | 0 | 1104 | | .ToList(); |
| | 0 | 1105 | | if (ordersForDraft.Count > 0) |
| | | 1106 | | { |
| | 0 | 1107 | | groupResult.AddRange(await OneCTransferManager.TransferStockWithCustomWarehouses |
| | 0 | 1108 | | ordersForDraft, |
| | 0 | 1109 | | sender, |
| | 0 | 1110 | | recipient, |
| | 0 | 1111 | | config.TransactionType, |
| | 0 | 1112 | | userName: userName, |
| | 0 | 1113 | | orgInnOverride: oneCOrganizationInn, |
| | 0 | 1114 | | createAsDraft: true, |
| | 0 | 1115 | | draftReason: "Недостаточно остатка в 1С. Проведите после оприходования товар |
| | | 1116 | | } |
| | 0 | 1117 | | oneCGroupedDetails.Add((sender, recipient, groupOrders, groupResult)); |
| | 0 | 1118 | | transferResult.AddRange(groupResult); |
| | 0 | 1119 | | } |
| | | 1120 | | } |
| | | 1121 | | else |
| | | 1122 | | { |
| | 0 | 1123 | | var singleResult = await OneCTransferManager.TransferStockWithCustomWarehouses( |
| | 0 | 1124 | | ordersToTransaction, |
| | 0 | 1125 | | warehouseSender!.Trim(), |
| | 0 | 1126 | | warehouseRecipient!.Trim(), |
| | 0 | 1127 | | config.TransactionType, |
| | 0 | 1128 | | userName: userName, |
| | 0 | 1129 | | orgInnOverride: oneCOrganizationInn); |
| | 0 | 1130 | | transferResult.AddRange(singleResult); |
| | | 1131 | | } |
| | | 1132 | | |
| | 0 | 1133 | | oneCPostingResponses = transferResult; |
| | 0 | 1134 | | var successfulTransfers = transferResult.Where(tr => tr.Success).ToList(); |
| | 0 | 1135 | | var failedTransfers = transferResult.Where(tr => !tr.Success).ToList(); |
| | | 1136 | | |
| | 0 | 1137 | | if (failedTransfers.Count > 0) |
| | | 1138 | | { |
| | 0 | 1139 | | var errorMessages = failedTransfers.Select(tr => tr.Message).Distinct().Take(3); |
| | 0 | 1140 | | var errorCount = failedTransfers.Count; |
| | 0 | 1141 | | var successCount = successfulTransfers.Count; |
| | 0 | 1142 | | var totalCount = transferResult.Count; |
| | | 1143 | | |
| | 0 | 1144 | | var encodedErrors = string.Join("; ", errorMessages.Select(System.Net.WebUtility.HtmlEnc |
| | 0 | 1145 | | TempData["TransactionResult"] = $"<div class='alert alert-warning'>" + |
| | 0 | 1146 | | $"<strong>⚠️ Частичный результат обработки в 1С</strong> |
| | 0 | 1147 | | $"Успешно: {successCount} из {totalCount} документов<br/ |
| | 0 | 1148 | | $"С ошибками: {errorCount} документов<br/>" + |
| | 0 | 1149 | | $"<small>Ошибки: {encodedErrors}" + |
| | 0 | 1150 | | (errorCount > 3 ? "..." : "") + "</small>" + |
| | 0 | 1151 | | "</div>"; |
| | 0 | 1152 | | return Json(new |
| | 0 | 1153 | | { |
| | 0 | 1154 | | redirectTo = Url.Action("Index", "Orders", |
| | 0 | 1155 | | new { sortOrder = GetSortStateCookie(), page }) |
| | 0 | 1156 | | }); |
| | | 1157 | | } |
| | | 1158 | | |
| | 0 | 1159 | | oneCHash = successfulTransfers |
| | 0 | 1160 | | .Select(h => h.TransactionId) |
| | 0 | 1161 | | .Where(id => id != null) |
| | 0 | 1162 | | .Select(id => id!); |
| | 0 | 1163 | | var effectiveWarehouseSender = oneCGroupedDetails.Count > 0 |
| | 0 | 1164 | | ? string.Join(", ", oneCGroupedDetails.Select(group => group.Sender) |
| | 0 | 1165 | | .Distinct(StringComparer.OrdinalIgnoreCase)) |
| | 0 | 1166 | | : warehouseSender ?? string.Empty; |
| | 0 | 1167 | | var effectiveWarehouseRecipient = oneCGroupedDetails.Count > 0 |
| | 0 | 1168 | | ? string.Join(", ", oneCGroupedDetails.Select(group => group.Recipient) |
| | 0 | 1169 | | .Distinct(StringComparer.OrdinalIgnoreCase)) |
| | 0 | 1170 | | : warehouseRecipient ?? string.Empty; |
| | 0 | 1171 | | oneCResult = OneCTransactionSummaryHelper.BuildAlertSummary( |
| | 0 | 1172 | | ordersToTransaction, transferResult, effectiveWarehouseSender, effectiveWarehouseRecipie |
| | 0 | 1173 | | + OneCStockGate.PartialPostingNotice( |
| | 0 | 1174 | | stockCheck, |
| | 0 | 1175 | | ordersToTransaction.Count - OneCStockGate.GetBlockingOrderIds(stockCheck).Count, |
| | 0 | 1176 | | oneCPostingResponses.Any(response => response.Success && response.IsDraft)) |
| | 0 | 1177 | | + OneCStockGate.SkippedNotice(stockCheck); |
| | 0 | 1178 | | if (scheduleAiComment && aiCommentBuilder != null) |
| | | 1179 | | { |
| | 0 | 1180 | | aiCommentBuilder.WithOneCResults( |
| | 0 | 1181 | | transferResult, |
| | 0 | 1182 | | effectiveWarehouseSender, |
| | 0 | 1183 | | effectiveWarehouseRecipient); |
| | | 1184 | | } |
| | | 1185 | | else |
| | | 1186 | | { |
| | 0 | 1187 | | comment += OneCTransactionSummaryHelper.BuildCommentDetails( |
| | 0 | 1188 | | ordersToTransaction, transferResult, effectiveWarehouseSender, effectiveWarehouseRec |
| | | 1189 | | } |
| | | 1190 | | |
| | 0 | 1191 | | if (oneCGroupedDetails.Count > 0) |
| | | 1192 | | { |
| | 0 | 1193 | | var lines = oneCGroupedDetails.Select(g => |
| | 0 | 1194 | | { |
| | 0 | 1195 | | var encodedRoute = System.Net.WebUtility.HtmlEncode($"{g.Sender} → {g.Recipient}"); |
| | 0 | 1196 | | var encodedNumbers = string.Join(", ", g.Orders.Select(o => System.Net.WebUtility.Ht |
| | 0 | 1197 | | var documents = g.Result |
| | 0 | 1198 | | .Where(response => response.Success) |
| | 0 | 1199 | | .Select(response => |
| | 0 | 1200 | | { |
| | 0 | 1201 | | var label = string.IsNullOrWhiteSpace(response.DocumentNumber) |
| | 0 | 1202 | | ? "Документ 1С" |
| | 0 | 1203 | | : $"№{response.DocumentNumber}"; |
| | 0 | 1204 | | var encodedLabel = System.Net.WebUtility.HtmlEncode(label); |
| | 0 | 1205 | | var documentLink = !string.IsNullOrWhiteSpace(response.TransactionId) && |
| | 0 | 1206 | | Guid.TryParse(response.TransactionId, out _) |
| | 0 | 1207 | | ? $"<a href='/Studio2/OneCDocument/Details?refKey={System.Net.WebUtility |
| | 0 | 1208 | | : encodedLabel; |
| | 0 | 1209 | | var status = response.IsDraft |
| | 0 | 1210 | | ? "<span class='badge bg-warning-subtle text-warning'>черновик</span>" |
| | 0 | 1211 | | : response.IsPosted |
| | 0 | 1212 | | ? "<span class='badge bg-success-subtle text-success'>проведён</span |
| | 0 | 1213 | | : "<span class='badge bg-warning-subtle text-warning'>не проведён</s |
| | 0 | 1214 | | var reason = response.IsDraft |
| | 0 | 1215 | | ? " — недостаточно остатка, проведите после оприходования" |
| | 0 | 1216 | | : !string.IsNullOrWhiteSpace(response.PostingError) |
| | 0 | 1217 | | ? $" — {System.Net.WebUtility.HtmlEncode(Services.ApiServices._1CApi |
| | 0 | 1218 | | : string.Empty; |
| | 0 | 1219 | | return $"<li>{status} {documentLink}{reason}</li>"; |
| | 0 | 1220 | | }) |
| | 0 | 1221 | | .ToList(); |
| | 0 | 1222 | | return $"<li class='mb-2'><strong>{encodedRoute}</strong>: {g.Orders.Count} заказ(ов |
| | 0 | 1223 | | $"({encodedNumbers})" + |
| | 0 | 1224 | | (documents.Count > 0 ? $"<ul class='mb-0 mt-1'>{string.Join(string.Empty, doc |
| | 0 | 1225 | | "</li>"; |
| | 0 | 1226 | | }); |
| | | 1227 | | |
| | 0 | 1228 | | oneCResult += "<div class='alert alert-info mt-2'><strong>Что создано в 1С по направлени |
| | 0 | 1229 | | $"<ul class='mb-0'>{string.Join("", lines)}</ul></div>"; |
| | | 1230 | | } |
| | 0 | 1231 | | } |
| | 0 | 1232 | | catch (Exception ex1C) |
| | | 1233 | | { |
| | 0 | 1234 | | ReportErrorToTelegram("1C processing error", ex1C); |
| | 0 | 1235 | | throw new TransactionException( |
| | 0 | 1236 | | TransactionErrorCode.OneCIntegrationFailed, |
| | 0 | 1237 | | $"Ошибка обработки в 1С: {ex1C.Message}", |
| | 0 | 1238 | | ex1C); |
| | | 1239 | | } |
| | 0 | 1240 | | } |
| | | 1241 | | |
| | | 1242 | | // Создаем транзакцию через TransactionManager |
| | 0 | 1243 | | var changedFieldsByOrder = ordersToTransaction |
| | 0 | 1244 | | .ToDictionary( |
| | 0 | 1245 | | o => o.Id, |
| | 0 | 1246 | | o => (o.UpdatedColumns ?? new List<string>()) |
| | 0 | 1247 | | .Where(c => !string.IsNullOrWhiteSpace(c)) |
| | 0 | 1248 | | .Distinct() |
| | 0 | 1249 | | .ToList()); |
| | | 1250 | | |
| | 0 | 1251 | | var historyWindowStart = DateTime.Now; |
| | 0 | 1252 | | var commentForDb = scheduleAiComment |
| | 0 | 1253 | | ? TransactionCommentAiConstants.DefaultPlaceholder |
| | 0 | 1254 | | : comment; |
| | 0 | 1255 | | await ReportStageAsync("document", "Записываем документ в Studio2"); |
| | 0 | 1256 | | if (config.CreateTransactionMethod != null) |
| | | 1257 | | { |
| | 0 | 1258 | | (changeCount, dateTime, createdTransactionId) = await config.CreateTransactionMethod( |
| | 0 | 1259 | | TransactionManager, |
| | 0 | 1260 | | ordersToTransaction, |
| | 0 | 1261 | | userName, |
| | 0 | 1262 | | createAt, |
| | 0 | 1263 | | commentForDb); |
| | | 1264 | | } |
| | | 1265 | | |
| | 0 | 1266 | | int cancelCount = ordersToTransaction.Count(o => o.AppStatus?.Name == "Отменен"); |
| | | 1267 | | |
| | 0 | 1268 | | if (changeCount > 0) |
| | | 1269 | | { |
| | 0 | 1270 | | if (processIn1C && oneCPostingResponses.Count > 0) |
| | | 1271 | | { |
| | 0 | 1272 | | if (oneCGroupedDetails.Count > 0) |
| | | 1273 | | { |
| | 0 | 1274 | | foreach (var group in oneCGroupedDetails) |
| | | 1275 | | { |
| | 0 | 1276 | | await OrderHistoryOneCPostingRecorder.TryRecordMovementAsync( |
| | 0 | 1277 | | HttpContext.RequestServices, |
| | 0 | 1278 | | group.Orders, |
| | 0 | 1279 | | group.Result, |
| | 0 | 1280 | | userName, |
| | 0 | 1281 | | createdTransactionId > 0 ? (int?)createdTransactionId : null, |
| | 0 | 1282 | | historyWindowStart, |
| | 0 | 1283 | | group.Sender, |
| | 0 | 1284 | | group.Recipient); |
| | | 1285 | | } |
| | | 1286 | | } |
| | | 1287 | | else |
| | | 1288 | | { |
| | 0 | 1289 | | await OrderHistoryOneCPostingRecorder.TryRecordMovementAsync( |
| | 0 | 1290 | | HttpContext.RequestServices, |
| | 0 | 1291 | | ordersToTransaction, |
| | 0 | 1292 | | oneCPostingResponses, |
| | 0 | 1293 | | userName, |
| | 0 | 1294 | | createdTransactionId > 0 ? (int?)createdTransactionId : null, |
| | 0 | 1295 | | historyWindowStart, |
| | 0 | 1296 | | warehouseSender, |
| | 0 | 1297 | | warehouseRecipient); |
| | | 1298 | | } |
| | | 1299 | | } |
| | | 1300 | | |
| | | 1301 | | // Записываем историю переходов статусов только после успешного проведения транзакции |
| | 0 | 1302 | | var statusTransitionHistoryService = |
| | 0 | 1303 | | HttpContext.RequestServices.GetService<IStatusTransitionHistoryService>(); |
| | 0 | 1304 | | if (transitions.Any() && statusTransitionHistoryService != null) |
| | | 1305 | | { |
| | 0 | 1306 | | var user = await UserCacheService.GetCachedUserAsync(User); |
| | 0 | 1307 | | await statusTransitionHistoryService.RecordStatusTransitionHistoryAsync( |
| | 0 | 1308 | | config.TransactionType, |
| | 0 | 1309 | | ordersToTransaction, |
| | 0 | 1310 | | fromStatusByOrderId, |
| | 0 | 1311 | | transitions, |
| | 0 | 1312 | | commentForDb, |
| | 0 | 1313 | | user?.AccessProfileId, |
| | 0 | 1314 | | createAt, |
| | 0 | 1315 | | null); |
| | | 1316 | | } |
| | | 1317 | | |
| | 0 | 1318 | | await CacheUpdater.Update(OrderCache); |
| | | 1319 | | |
| | | 1320 | | // Имя статуса для сообщения: фактический новый статус после проведения (из заказов или из перех |
| | 0 | 1321 | | var statusNameForMessage = string.Join(", ", ordersToTransaction |
| | 0 | 1322 | | .Select(o => o.AppStatus?.Name) |
| | 0 | 1323 | | .Where(n => !string.IsNullOrEmpty(n)) |
| | 0 | 1324 | | .Distinct()); |
| | 0 | 1325 | | if (string.IsNullOrEmpty(statusNameForMessage) && transitions.Any()) |
| | 0 | 1326 | | statusNameForMessage = string.Join(", ", |
| | 0 | 1327 | | transitions.Select(t => t.ToStatus?.Name).Where(n => !string.IsNullOrEmpty(n)) |
| | 0 | 1328 | | .Distinct()); |
| | 0 | 1329 | | if (string.IsNullOrEmpty(statusNameForMessage)) |
| | 0 | 1330 | | statusNameForMessage = config.PrimaryStatusName ?? ""; |
| | | 1331 | | |
| | 0 | 1332 | | string msg = config.SuccessMessageBuilder != null |
| | 0 | 1333 | | ? config.SuccessMessageBuilder(changeCount, cancelCount, dateTime, userName, commentForDb, |
| | 0 | 1334 | | orders) |
| | 0 | 1335 | | : TransactionConfig.BuildStandardSuccessMessage(changeCount, cancelCount, dateTime, |
| | 0 | 1336 | | userName, commentForDb, orders, statusNameForMessage ?? "", oneCResult); |
| | | 1337 | | |
| | 0 | 1338 | | var changedFieldsMessage = |
| | 0 | 1339 | | BuildChangedFieldsMessage(config.TransactionType, ordersToTransaction, changedFieldsByOrder) |
| | 0 | 1340 | | if (!string.IsNullOrWhiteSpace(changedFieldsMessage)) |
| | | 1341 | | { |
| | 0 | 1342 | | msg += "<br/>" + changedFieldsMessage; |
| | | 1343 | | } |
| | | 1344 | | |
| | 0 | 1345 | | if (config.SuccessMessageBuilder != null && !string.IsNullOrWhiteSpace(oneCResult)) |
| | | 1346 | | { |
| | 0 | 1347 | | msg += "<br/>" + oneCResult; |
| | | 1348 | | } |
| | | 1349 | | |
| | 0 | 1350 | | var successAppendix = GetPostTransactionSuccessAppendix(); |
| | 0 | 1351 | | if (!string.IsNullOrWhiteSpace(successAppendix)) |
| | 0 | 1352 | | msg += "<br/>" + successAppendix; |
| | | 1353 | | |
| | 0 | 1354 | | var asyncSuccessAppendix = |
| | 0 | 1355 | | await GetPostTransactionSuccessAppendixAsync(ordersToTransaction, config); |
| | 0 | 1356 | | if (!string.IsNullOrWhiteSpace(asyncSuccessAppendix)) |
| | 0 | 1357 | | msg += "<br/>" + asyncSuccessAppendix; |
| | | 1358 | | |
| | 0 | 1359 | | if (scheduleAiComment && aiCommentBuilder != null && createdTransactionId > 0) |
| | | 1360 | | { |
| | 0 | 1361 | | if (HttpContext.Items.TryGetValue(TransactionCommentAiHttpContextKeys.OzonShipResult, |
| | 0 | 1362 | | out var ozonObj) && |
| | 0 | 1363 | | ozonObj is OrderToSupplierOzonShipResult ozonShipResult) |
| | | 1364 | | { |
| | 0 | 1365 | | aiCommentBuilder.WithOzonShip( |
| | 0 | 1366 | | TransactionCommentAiOzonShipMapper.ToSummary(ozonShipResult)); |
| | | 1367 | | } |
| | | 1368 | | |
| | 0 | 1369 | | await TransactionCommentAiSchedulingHelper.TryScheduleAsync( |
| | 0 | 1370 | | HttpContext.RequestServices, |
| | 0 | 1371 | | aiCommentBuilder, |
| | 0 | 1372 | | scheduleAiComment, |
| | 0 | 1373 | | createdTransactionId, |
| | 0 | 1374 | | ordersToTransaction, |
| | 0 | 1375 | | fromStatusByOrderId, |
| | 0 | 1376 | | Context, |
| | 0 | 1377 | | changedFieldsByOrder, |
| | 0 | 1378 | | HttpContext.RequestAborted); |
| | | 1379 | | } |
| | | 1380 | | |
| | 0 | 1381 | | await NotificationService.NotifyAllAsync(msg); |
| | 0 | 1382 | | if (suppressTempData) |
| | 0 | 1383 | | LastTransactionSuccessMessage = msg; |
| | | 1384 | | else |
| | 0 | 1385 | | TempData["TransactionResult"] = msg; |
| | | 1386 | | |
| | | 1387 | | // Финализация трекинга только после успешного проведения. |
| | 0 | 1388 | | var trackingService = HttpContext.RequestServices.GetService<ITransactionTimeTrackingService>(); |
| | 0 | 1389 | | if (trackingService != null && |
| | 0 | 1390 | | !string.IsNullOrWhiteSpace(trackingKey) && |
| | 0 | 1391 | | createdTransactionId > 0) |
| | | 1392 | | { |
| | 0 | 1393 | | var transactionUrl = $"/Studio2/Transaction/GetTransaction?transactionId={createdTransaction |
| | 0 | 1394 | | await trackingService.CompleteSessionAsync( |
| | 0 | 1395 | | trackingKey, |
| | 0 | 1396 | | createdTransactionId, |
| | 0 | 1397 | | ordersToTransaction.Count, |
| | 0 | 1398 | | commentForDb, |
| | 0 | 1399 | | transactionUrl); |
| | | 1400 | | } |
| | 0 | 1401 | | } |
| | | 1402 | | |
| | 0 | 1403 | | await TransactionCache.Update(); |
| | 0 | 1404 | | } |
| | | 1405 | | |
| | 0 | 1406 | | ClearSelectedIdsSession(); |
| | | 1407 | | |
| | | 1408 | | // Очищаем cookies для заказов |
| | 0 | 1409 | | foreach (var order in orders) |
| | | 1410 | | { |
| | 0 | 1411 | | var cookieKey = $"PurchasePrice_{order.Id}"; |
| | 0 | 1412 | | if (Request.Cookies.ContainsKey(cookieKey)) |
| | | 1413 | | { |
| | 0 | 1414 | | Response.Cookies.Delete(cookieKey); |
| | | 1415 | | } |
| | | 1416 | | } |
| | | 1417 | | |
| | | 1418 | | // Сохраняем засылы если есть |
| | 0 | 1419 | | if (orderSupplierMap != null && orderSupplierMap.Any()) |
| | | 1420 | | { |
| | 0 | 1421 | | await OrderServices.SetZasylAsync(orderSupplierMap); |
| | | 1422 | | } |
| | | 1423 | | |
| | | 1424 | | |
| | | 1425 | | // Сохраняем дополнительные расходы через слой сервиса заказов |
| | 0 | 1426 | | if (OrderServices != null) |
| | | 1427 | | { |
| | 0 | 1428 | | await OrderServices.SaveExtraExpensesForOrders(orders); |
| | | 1429 | | } |
| | | 1430 | | |
| | 0 | 1431 | | var calculationSnapshots = HttpContext.RequestServices.GetService<OrderCalculationSnapshotService>(); |
| | 0 | 1432 | | if (calculationSnapshots != null) |
| | | 1433 | | { |
| | 0 | 1434 | | await calculationSnapshots.CaptureAsync( |
| | 0 | 1435 | | orders.Select(x => x.Id), Guid.NewGuid(), "Transaction", |
| | 0 | 1436 | | User.Identity?.Name, HttpContext.RequestAborted); |
| | | 1437 | | } |
| | | 1438 | | |
| | 0 | 1439 | | await ReportProgressFinishedAsync(true); |
| | | 1440 | | |
| | 0 | 1441 | | return Json(new |
| | 0 | 1442 | | { |
| | 0 | 1443 | | redirectTo = Url.Action("Index", "Orders", new { sortOrder = GetSortStateCookie(), page }) |
| | 0 | 1444 | | }); |
| | | 1445 | | } |
| | 0 | 1446 | | catch (Exception ex) |
| | | 1447 | | { |
| | 0 | 1448 | | await ReportProgressFinishedAsync(false, ex.Message); |
| | 0 | 1449 | | ReportErrorToTelegram("Transaction processing error", ex); |
| | 0 | 1450 | | if (ex is TransactionException transactionException) |
| | | 1451 | | { |
| | 0 | 1452 | | SetTransactionErrorTempData(transactionException.Code, transactionException.Message); |
| | | 1453 | | } |
| | | 1454 | | else |
| | | 1455 | | { |
| | 0 | 1456 | | SetTransactionErrorTempData( |
| | 0 | 1457 | | TransactionErrorCode.Unknown, |
| | 0 | 1458 | | $"Не удалось провести выбранные заказы ({ex.Message})"); |
| | | 1459 | | } |
| | | 1460 | | |
| | | 1461 | | // Сохраняем засылы даже при ошибке |
| | 0 | 1462 | | if (orderSupplierMap != null && orderSupplierMap.Any()) |
| | | 1463 | | { |
| | 0 | 1464 | | await OrderServices.SetZasylAsync(orderSupplierMap); |
| | | 1465 | | } |
| | | 1466 | | |
| | 0 | 1467 | | return Json(new |
| | 0 | 1468 | | { |
| | 0 | 1469 | | redirectTo = Url.Action("Index", "Orders", new { sortOrder = GetSortStateCookie(), page }) |
| | 0 | 1470 | | }); |
| | | 1471 | | } |
| | 0 | 1472 | | } |
| | | 1473 | | |
| | | 1474 | | protected void SetTransactionErrorTempData(TransactionErrorCode code, string message) |
| | | 1475 | | { |
| | 0 | 1476 | | TempData[TransactionErrorCodeTempDataKey] = code.ToString(); |
| | 0 | 1477 | | TempData["ErorrResult"] = message; |
| | 0 | 1478 | | } |
| | | 1479 | | |
| | | 1480 | | |
| | | 1481 | | /// <summary>Сообщает в Telegram об исключении при проведении транзакции; не ждёт отправки и не бросает исключен |
| | | 1482 | | private void ReportErrorToTelegram(string stage, Exception ex) |
| | | 1483 | | { |
| | | 1484 | | try |
| | | 1485 | | { |
| | 0 | 1486 | | var notifier = HttpContext.RequestServices.GetService<ITelegramNotifier>(); |
| | 0 | 1487 | | if (notifier is not { IsConfigured: true }) return; |
| | | 1488 | | |
| | 0 | 1489 | | var message = TelegramMessageFactory.Exception("Транзакция: " + stage, ex, |
| | 0 | 1490 | | new Dictionary<string, string?> |
| | 0 | 1491 | | { |
| | 0 | 1492 | | ["URL"] = $"{Request.Method} {Request.Path}{Request.QueryString}", |
| | 0 | 1493 | | ["Пользователь"] = User?.Identity?.Name ?? "Анонимный", |
| | 0 | 1494 | | ["UserId"] = User?.FindFirstValue(ClaimTypes.NameIdentifier), |
| | 0 | 1495 | | ["TraceId"] = HttpContext.TraceIdentifier |
| | 0 | 1496 | | }); |
| | 0 | 1497 | | _ = notifier.SendAsync(message); |
| | 0 | 1498 | | } |
| | 0 | 1499 | | catch |
| | | 1500 | | { |
| | | 1501 | | // Ошибка отправки в Telegram не должна ломать основной поток выполнения |
| | 0 | 1502 | | } |
| | 0 | 1503 | | } |
| | | 1504 | | |
| | | 1505 | | private static string BuildChangedFieldsMessage( |
| | | 1506 | | TransactionType transactionType, |
| | | 1507 | | IReadOnlyCollection<Order> ordersToTransaction, |
| | | 1508 | | IReadOnlyDictionary<int, List<string>> changedFieldsByOrder) |
| | | 1509 | | { |
| | 0 | 1510 | | if (transactionType != TransactionType.WarehouseChange || changedFieldsByOrder.Count == 0) |
| | | 1511 | | { |
| | 0 | 1512 | | return string.Empty; |
| | | 1513 | | } |
| | | 1514 | | |
| | 0 | 1515 | | var technicalFieldsToHide = new HashSet<string>(StringComparer.OrdinalIgnoreCase) |
| | 0 | 1516 | | { |
| | 0 | 1517 | | "EtProducerId", |
| | 0 | 1518 | | "EtProducer", |
| | 0 | 1519 | | "TransactionId", |
| | 0 | 1520 | | "OzonClientId", |
| | 0 | 1521 | | "ExcelFileDataId", |
| | 0 | 1522 | | "ExcelFileData", |
| | 0 | 1523 | | "DeliveryId", |
| | 0 | 1524 | | "Delivery", |
| | 0 | 1525 | | "ExtraExpenses", |
| | 0 | 1526 | | "OrderExtensions", |
| | 0 | 1527 | | "UpdatedColumns", |
| | 0 | 1528 | | "UpdatedBy", |
| | 0 | 1529 | | "IsVerified", |
| | 0 | 1530 | | "IsAccepted" |
| | 0 | 1531 | | }; |
| | | 1532 | | |
| | 0 | 1533 | | static string GetFieldDisplayName(string fieldName) => fieldName switch |
| | 0 | 1534 | | { |
| | 0 | 1535 | | "ShipmentWarehouseId" => "Склад отгрузки", |
| | 0 | 1536 | | "AppStatusId" => "Статус", |
| | 0 | 1537 | | "SupplierId" => "Поставщик", |
| | 0 | 1538 | | "OrderNumberToSupplier" => "Номер заказа поставщику", |
| | 0 | 1539 | | "OriginalPurchasePrice" => "Цена закупки", |
| | 0 | 1540 | | "PurchasePrice" => "Цена закупки (RUB)", |
| | 0 | 1541 | | "Quantity" => "Количество", |
| | 0 | 1542 | | _ => GetDisplayNameFromOrderModel(fieldName) |
| | 0 | 1543 | | }; |
| | | 1544 | | |
| | | 1545 | | static string GetDisplayNameFromOrderModel(string fieldName) |
| | | 1546 | | { |
| | 0 | 1547 | | var property = typeof(Order).GetProperty( |
| | 0 | 1548 | | fieldName, |
| | 0 | 1549 | | BindingFlags.Public | BindingFlags.Instance | BindingFlags.IgnoreCase); |
| | | 1550 | | |
| | 0 | 1551 | | if (property != null) |
| | | 1552 | | { |
| | 0 | 1553 | | var display = property.GetCustomAttribute<DisplayAttribute>(); |
| | 0 | 1554 | | if (!string.IsNullOrWhiteSpace(display?.Name)) |
| | | 1555 | | { |
| | 0 | 1556 | | return display.Name!; |
| | | 1557 | | } |
| | | 1558 | | |
| | 0 | 1559 | | var displayName = property.GetCustomAttribute<DisplayNameAttribute>(); |
| | 0 | 1560 | | if (!string.IsNullOrWhiteSpace(displayName?.DisplayName)) |
| | | 1561 | | { |
| | 0 | 1562 | | return displayName.DisplayName!; |
| | | 1563 | | } |
| | | 1564 | | } |
| | | 1565 | | |
| | | 1566 | | // Fallback: humanize camelCase/PascalCase and underscores. |
| | 0 | 1567 | | var normalized = fieldName.Replace("_", " ").Trim(); |
| | 0 | 1568 | | if (string.IsNullOrWhiteSpace(normalized)) |
| | | 1569 | | { |
| | 0 | 1570 | | return fieldName; |
| | | 1571 | | } |
| | | 1572 | | |
| | 0 | 1573 | | var humanized = System.Text.RegularExpressions.Regex.Replace( |
| | 0 | 1574 | | normalized, |
| | 0 | 1575 | | "(?<!^)([A-Z])", |
| | 0 | 1576 | | " $1"); |
| | | 1577 | | |
| | 0 | 1578 | | return humanized.Trim(); |
| | | 1579 | | } |
| | | 1580 | | |
| | 0 | 1581 | | var lines = ordersToTransaction |
| | 0 | 1582 | | .Select(order => |
| | 0 | 1583 | | { |
| | 0 | 1584 | | if (!changedFieldsByOrder.TryGetValue(order.Id, out var fields) || fields.Count == 0) |
| | 0 | 1585 | | { |
| | 0 | 1586 | | return null; |
| | 0 | 1587 | | } |
| | 0 | 1588 | | |
| | 0 | 1589 | | var displayFields = fields |
| | 0 | 1590 | | .Where(field => !technicalFieldsToHide.Contains(field)) |
| | 0 | 1591 | | .Select(GetFieldDisplayName) |
| | 0 | 1592 | | .Distinct() |
| | 0 | 1593 | | .ToList(); |
| | 0 | 1594 | | |
| | 0 | 1595 | | if (displayFields.Count == 0) |
| | 0 | 1596 | | { |
| | 0 | 1597 | | return null; |
| | 0 | 1598 | | } |
| | 0 | 1599 | | |
| | 0 | 1600 | | var shipmentNumber = string.IsNullOrWhiteSpace(order.ShipmentNumber) |
| | 0 | 1601 | | ? $"ID {order.Id}" |
| | 0 | 1602 | | : order.ShipmentNumber; |
| | 0 | 1603 | | |
| | 0 | 1604 | | return $"<li><b>{System.Net.WebUtility.HtmlEncode(shipmentNumber)}</b>: {string.Join(", ", displayFi |
| | 0 | 1605 | | }) |
| | 0 | 1606 | | .Where(line => !string.IsNullOrWhiteSpace(line)) |
| | 0 | 1607 | | .ToList(); |
| | | 1608 | | |
| | 0 | 1609 | | if (lines.Count == 0) |
| | | 1610 | | { |
| | 0 | 1611 | | return string.Empty; |
| | | 1612 | | } |
| | | 1613 | | |
| | 0 | 1614 | | return $"Измененные поля:<ul>{string.Join("", lines)}</ul>"; |
| | | 1615 | | } |
| | | 1616 | | |
| | | 1617 | | |
| | | 1618 | | /// <summary> |
| | | 1619 | | /// Получает существующий статус приложения или создает новый |
| | | 1620 | | /// </summary> |
| | | 1621 | | protected async Task<AppStatus> GetOrCreateAppStatus(string statusName) |
| | | 1622 | | { |
| | 0 | 1623 | | var status = await AppStatusServices.GetAppStatusAsync(new AppStatus { Name = statusName }); |
| | 0 | 1624 | | if (status == null) |
| | | 1625 | | { |
| | 0 | 1626 | | status = new AppStatus { Name = statusName }; |
| | 0 | 1627 | | Context.AppStatuses.Add(status); |
| | 0 | 1628 | | await Context.SaveChangesAsync(); |
| | | 1629 | | } |
| | | 1630 | | |
| | 0 | 1631 | | return status; |
| | 0 | 1632 | | } |
| | | 1633 | | |
| | | 1634 | | /// <summary> |
| | | 1635 | | /// Запускает фоновое кэширование данных из внешних API |
| | | 1636 | | /// </summary> |
| | | 1637 | | protected void RunBackgroundCacheTask(List<Order> orders) |
| | | 1638 | | { |
| | 0 | 1639 | | if (ProxyHttpClientService == null) return; |
| | | 1640 | | |
| | 0 | 1641 | | _ = Task.Run(async () => |
| | 0 | 1642 | | { |
| | 0 | 1643 | | foreach (var order in orders) |
| | 0 | 1644 | | { |
| | 0 | 1645 | | try |
| | 0 | 1646 | | { |
| | 0 | 1647 | | if (order.EtProducer != null && !string.IsNullOrEmpty(order.Article)) |
| | 0 | 1648 | | { |
| | 0 | 1649 | | await ProxyHttpClientService.GetJsonAsync( |
| | 0 | 1650 | | $"https://api.interparts.ru/detail-full-info/{order.EtProducer.Name}/{order.Article}"); |
| | 0 | 1651 | | await ProxyHttpClientService.GetJsonAsync( |
| | 0 | 1652 | | $"https://api.interparts.ru/product-information/product/?article_number={order.Article}& |
| | 0 | 1653 | | } |
| | 0 | 1654 | | } |
| | 0 | 1655 | | catch |
| | 0 | 1656 | | { |
| | 0 | 1657 | | /* Игнорируем ошибки фонового кэширования */ |
| | 0 | 1658 | | } |
| | 0 | 1659 | | } |
| | 0 | 1660 | | }); |
| | 0 | 1661 | | } |
| | | 1662 | | |
| | | 1663 | | /// <summary> |
| | | 1664 | | /// Очищает выбранные ID из сессии |
| | | 1665 | | /// </summary> |
| | | 1666 | | [HttpPost] |
| | | 1667 | | public IActionResult ClearSelectedIdsSession() |
| | | 1668 | | { |
| | 0 | 1669 | | HttpContext.Session.SetString("selectedIds", JsonConvert.SerializeObject(new List<int>())); |
| | 0 | 1670 | | return Ok(); |
| | | 1671 | | } |
| | | 1672 | | |
| | | 1673 | | /// <summary> |
| | | 1674 | | /// Получает состояние сортировки из cookie |
| | | 1675 | | /// </summary> |
| | | 1676 | | protected OrderSortState GetSortStateCookie() |
| | | 1677 | | { |
| | 0 | 1678 | | var sortStateCookie = Request.Cookies["SortState"]; |
| | 0 | 1679 | | if (!string.IsNullOrEmpty(sortStateCookie) && |
| | 0 | 1680 | | Enum.TryParse<OrderSortState>(sortStateCookie, out var savedSortState)) |
| | | 1681 | | { |
| | 0 | 1682 | | return savedSortState; |
| | | 1683 | | } |
| | | 1684 | | |
| | 0 | 1685 | | return OrderSortState.StandardState; |
| | | 1686 | | } |
| | | 1687 | | |
| | | 1688 | | protected async Task<List<string>> FilterBitrixWarehouseNamesByScope(IEnumerable<string> warehouseNames) |
| | | 1689 | | { |
| | 0 | 1690 | | var names = warehouseNames |
| | 0 | 1691 | | .Where(n => !string.IsNullOrWhiteSpace(n)) |
| | 0 | 1692 | | .Distinct(StringComparer.OrdinalIgnoreCase) |
| | 0 | 1693 | | .OrderBy(n => n) |
| | 0 | 1694 | | .ToList(); |
| | | 1695 | | |
| | 0 | 1696 | | if (ScopeFilterValuesService == null) |
| | | 1697 | | { |
| | 0 | 1698 | | return names; |
| | | 1699 | | } |
| | | 1700 | | |
| | 0 | 1701 | | var userId = User?.FindFirstValue(ClaimTypes.NameIdentifier); |
| | 0 | 1702 | | if (string.IsNullOrWhiteSpace(userId)) |
| | | 1703 | | { |
| | 0 | 1704 | | return names; |
| | | 1705 | | } |
| | | 1706 | | |
| | 0 | 1707 | | var allowed = await ScopeFilterValuesService.GetAllowedValuesAsync( |
| | 0 | 1708 | | userId, |
| | 0 | 1709 | | "BitrixWarehouse", |
| | 0 | 1710 | | HttpContext.RequestAborted); |
| | | 1711 | | |
| | 0 | 1712 | | if (!allowed.TryGetValue("Name", out var allowedNames) || allowedNames.Count == 0) |
| | | 1713 | | { |
| | 0 | 1714 | | return names; |
| | | 1715 | | } |
| | | 1716 | | |
| | 0 | 1717 | | return names |
| | 0 | 1718 | | .Where(n => allowedNames.Contains(n)) |
| | 0 | 1719 | | .OrderBy(n => n) |
| | 0 | 1720 | | .ToList(); |
| | 0 | 1721 | | } |
| | | 1722 | | |
| | | 1723 | | protected async Task<List<T>> FilterByOrderScopeIdsAsync<T>( |
| | | 1724 | | IEnumerable<T> source, |
| | | 1725 | | Func<T, int> idSelector, |
| | | 1726 | | string scopeFieldName) |
| | | 1727 | | { |
| | 0 | 1728 | | var list = source?.ToList() ?? new List<T>(); |
| | 0 | 1729 | | if (list.Count == 0 || ScopeFilterValuesService == null) |
| | | 1730 | | { |
| | 0 | 1731 | | return list; |
| | | 1732 | | } |
| | | 1733 | | |
| | 0 | 1734 | | var userId = User?.FindFirstValue(ClaimTypes.NameIdentifier); |
| | 0 | 1735 | | if (string.IsNullOrWhiteSpace(userId)) |
| | | 1736 | | { |
| | 0 | 1737 | | return list; |
| | | 1738 | | } |
| | | 1739 | | |
| | 0 | 1740 | | var allowed = await ScopeFilterValuesService.GetAllowedValuesAsync( |
| | 0 | 1741 | | userId, |
| | 0 | 1742 | | "Order", |
| | 0 | 1743 | | HttpContext.RequestAborted); |
| | | 1744 | | |
| | 0 | 1745 | | if (!allowed.TryGetValue(scopeFieldName, out var rawIds) || rawIds.Count == 0) |
| | | 1746 | | { |
| | 0 | 1747 | | return list; |
| | | 1748 | | } |
| | | 1749 | | |
| | 0 | 1750 | | var allowedIds = rawIds |
| | 0 | 1751 | | .Select(v => int.TryParse(v, out var id) ? id : (int?)null) |
| | 0 | 1752 | | .Where(v => v.HasValue) |
| | 0 | 1753 | | .Select(v => v!.Value) |
| | 0 | 1754 | | .ToHashSet(); |
| | | 1755 | | |
| | 0 | 1756 | | if (allowedIds.Count == 0) |
| | | 1757 | | { |
| | 0 | 1758 | | return list; |
| | | 1759 | | } |
| | | 1760 | | |
| | 0 | 1761 | | return list.Where(item => allowedIds.Contains(idSelector(item))).ToList(); |
| | 0 | 1762 | | } |
| | | 1763 | | |
| | | 1764 | | protected async Task<List<string>> FilterWarehouseNamesByOrderScopeAsync(IEnumerable<string> names) |
| | | 1765 | | { |
| | 0 | 1766 | | var normalized = names |
| | 0 | 1767 | | .Where(n => !string.IsNullOrWhiteSpace(n)) |
| | 0 | 1768 | | .Distinct(StringComparer.OrdinalIgnoreCase) |
| | 0 | 1769 | | .OrderBy(n => n) |
| | 0 | 1770 | | .ToList(); |
| | | 1771 | | |
| | 0 | 1772 | | if (normalized.Count == 0 || ScopeFilterValuesService == null || WarehouseDataServices == null) |
| | | 1773 | | { |
| | 0 | 1774 | | return normalized; |
| | | 1775 | | } |
| | | 1776 | | |
| | 0 | 1777 | | var userId = User?.FindFirstValue(ClaimTypes.NameIdentifier); |
| | 0 | 1778 | | if (string.IsNullOrWhiteSpace(userId)) |
| | | 1779 | | { |
| | 0 | 1780 | | return normalized; |
| | | 1781 | | } |
| | | 1782 | | |
| | 0 | 1783 | | var allowed = await ScopeFilterValuesService.GetAllowedValuesAsync(userId, "Order", HttpContext.RequestAbort |
| | 0 | 1784 | | if (!allowed.TryGetValue("ShipmentWarehouseId", out var rawIds) || rawIds.Count == 0) |
| | | 1785 | | { |
| | 0 | 1786 | | return normalized; |
| | | 1787 | | } |
| | | 1788 | | |
| | 0 | 1789 | | var allowedIds = rawIds |
| | 0 | 1790 | | .Select(v => int.TryParse(v, out var id) ? id : (int?)null) |
| | 0 | 1791 | | .Where(v => v.HasValue) |
| | 0 | 1792 | | .Select(v => v!.Value) |
| | 0 | 1793 | | .ToHashSet(); |
| | | 1794 | | |
| | 0 | 1795 | | if (allowedIds.Count == 0) |
| | | 1796 | | { |
| | 0 | 1797 | | return normalized; |
| | | 1798 | | } |
| | | 1799 | | |
| | 0 | 1800 | | var allWarehouses = await WarehouseDataServices.GetWarehouses(); |
| | 0 | 1801 | | var allowedNames = allWarehouses |
| | 0 | 1802 | | .Where(w => allowedIds.Contains(w.Id)) |
| | 0 | 1803 | | .Select(w => w.Name) |
| | 0 | 1804 | | .Where(n => !string.IsNullOrWhiteSpace(n)) |
| | 0 | 1805 | | .ToHashSet(StringComparer.OrdinalIgnoreCase); |
| | | 1806 | | |
| | 0 | 1807 | | return normalized.Where(n => allowedNames.Contains(n)).OrderBy(n => n).ToList(); |
| | 0 | 1808 | | } |
| | | 1809 | | |
| | | 1810 | | protected async Task<int[]> FilterOrderIdsByDataScopeAsync(IEnumerable<int> ids) |
| | | 1811 | | { |
| | 0 | 1812 | | var distinctIds = ids? |
| | 0 | 1813 | | .Where(id => id > 0) |
| | 0 | 1814 | | .Distinct() |
| | 0 | 1815 | | .ToArray() ?? Array.Empty<int>(); |
| | | 1816 | | |
| | 0 | 1817 | | if (distinctIds.Length == 0) |
| | | 1818 | | { |
| | 0 | 1819 | | return Array.Empty<int>(); |
| | | 1820 | | } |
| | | 1821 | | |
| | 0 | 1822 | | var scopedQuery = Context.Orders |
| | 0 | 1823 | | .AsNoTracking() |
| | 0 | 1824 | | .Where(o => distinctIds.Contains(o.Id)); |
| | | 1825 | | |
| | 0 | 1826 | | scopedQuery = await ApplyOrderDataScopeAsync(scopedQuery); |
| | | 1827 | | |
| | 0 | 1828 | | return await scopedQuery |
| | 0 | 1829 | | .Select(o => o.Id) |
| | 0 | 1830 | | .ToArrayAsync(HttpContext.RequestAborted); |
| | 0 | 1831 | | } |
| | | 1832 | | |
| | | 1833 | | protected async Task<List<Order>> FilterOrdersByDataScopeAsync(IEnumerable<Order> orders) |
| | | 1834 | | { |
| | 0 | 1835 | | var list = orders?.ToList() ?? new List<Order>(); |
| | 0 | 1836 | | if (list.Count == 0) |
| | | 1837 | | { |
| | 0 | 1838 | | return list; |
| | | 1839 | | } |
| | | 1840 | | |
| | 0 | 1841 | | var allowedIds = await FilterOrderIdsByDataScopeAsync(list.Select(o => o.Id)); |
| | 0 | 1842 | | if (allowedIds.Length == 0) |
| | | 1843 | | { |
| | 0 | 1844 | | return new List<Order>(); |
| | | 1845 | | } |
| | | 1846 | | |
| | 0 | 1847 | | var allowedSet = allowedIds.ToHashSet(); |
| | 0 | 1848 | | return list.Where(o => allowedSet.Contains(o.Id)).ToList(); |
| | 0 | 1849 | | } |
| | | 1850 | | |
| | | 1851 | | private async Task<IQueryable<Order>> ApplyOrderDataScopeAsync(IQueryable<Order> ordersQuery) |
| | | 1852 | | { |
| | 0 | 1853 | | if (DataScopeService == null) |
| | | 1854 | | { |
| | 0 | 1855 | | return ordersQuery; |
| | | 1856 | | } |
| | | 1857 | | |
| | 0 | 1858 | | var userId = User?.FindFirstValue(ClaimTypes.NameIdentifier); |
| | 0 | 1859 | | if (string.IsNullOrWhiteSpace(userId)) |
| | | 1860 | | { |
| | 0 | 1861 | | return ordersQuery; |
| | | 1862 | | } |
| | | 1863 | | |
| | 0 | 1864 | | return await DataScopeService.ApplyDataScopeAsync( |
| | 0 | 1865 | | ordersQuery, |
| | 0 | 1866 | | userId, |
| | 0 | 1867 | | entityName: "Order", |
| | 0 | 1868 | | extraParameters: null, |
| | 0 | 1869 | | ct: HttpContext.RequestAborted); |
| | 0 | 1870 | | } |
| | | 1871 | | |
| | | 1872 | | private async Task<IReadOnlyDictionary<string, bool>> BuildOrderListFieldReadMapAsync(string userId) |
| | | 1873 | | { |
| | 0 | 1874 | | var permissionService = HttpContext.RequestServices.GetService<IPermissionService>(); |
| | 0 | 1875 | | if (permissionService == null) |
| | | 1876 | | { |
| | 0 | 1877 | | return OrderListColumnCatalog.All.ToDictionary(c => c.FieldName, _ => true, StringComparer.OrdinalIgnore |
| | | 1878 | | } |
| | | 1879 | | |
| | 0 | 1880 | | var bundle = await permissionService.GetUserPermissionsAsync(userId, HttpContext.RequestAborted); |
| | 0 | 1881 | | if (bundle.IsSuperAdmin) |
| | | 1882 | | { |
| | 0 | 1883 | | return OrderListColumnCatalog.All.ToDictionary(c => c.FieldName, _ => true, StringComparer.OrdinalIgnore |
| | | 1884 | | } |
| | | 1885 | | |
| | 0 | 1886 | | var fromBundle = bundle.Fields |
| | 0 | 1887 | | .Where(f => string.Equals(f.EntityName, OrderListColumnCatalog.EntityName, StringComparison.OrdinalIgnor |
| | 0 | 1888 | | .ToDictionary(f => f.FieldName, f => f.CanRead, StringComparer.OrdinalIgnoreCase); |
| | | 1889 | | |
| | 0 | 1890 | | var dict = new Dictionary<string, bool>(StringComparer.OrdinalIgnoreCase); |
| | 0 | 1891 | | foreach (var col in OrderListColumnCatalog.All) |
| | 0 | 1892 | | dict[col.FieldName] = fromBundle.TryGetValue(col.FieldName, out var r) ? r : true; |
| | 0 | 1893 | | return dict; |
| | 0 | 1894 | | } |
| | | 1895 | | |
| | | 1896 | | private static void EnsureOrderColumnsVisibilityProfile(CustomIdentityUser? user) |
| | | 1897 | | { |
| | 0 | 1898 | | if (user == null) |
| | | 1899 | | { |
| | 0 | 1900 | | return; |
| | | 1901 | | } |
| | | 1902 | | |
| | 0 | 1903 | | if (user.AccessProfileId != null && user.AccessProfile?.AvailableOrderColumns?.Any() == true) |
| | | 1904 | | { |
| | 0 | 1905 | | return; |
| | | 1906 | | } |
| | | 1907 | | |
| | 0 | 1908 | | user.AccessProfileId ??= -1; |
| | 0 | 1909 | | user.AccessProfile ??= new AccessProfile(); |
| | 0 | 1910 | | user.AccessProfile.AvailableOrderColumns = OrderListColumnCatalog.All |
| | 0 | 1911 | | .Select(c => c.LegacyAccessKey) |
| | 0 | 1912 | | .Where(k => !string.IsNullOrWhiteSpace(k)) |
| | 0 | 1913 | | .Distinct(StringComparer.OrdinalIgnoreCase) |
| | 0 | 1914 | | .ToList()!; |
| | 0 | 1915 | | } |
| | | 1916 | | |
| | | 1917 | | protected void SetTransactionErrorViewData(TransactionErrorCode code, string? reason) |
| | | 1918 | | { |
| | 0 | 1919 | | ViewData["TransactionErrorCode"] = code.ToString(); |
| | 0 | 1920 | | ViewData["ErrorReason"] = reason; |
| | 0 | 1921 | | } |
| | | 1922 | | |
| | | 1923 | | protected async Task<Dictionary<int, decimal>> LoadWarehouseStockC0MapAsync(IReadOnlyList<Order> orders) |
| | | 1924 | | { |
| | 0 | 1925 | | if (orders.Count == 0) |
| | 0 | 1926 | | return new Dictionary<int, decimal>(); |
| | | 1927 | | |
| | 0 | 1928 | | var lookupService = HttpContext.RequestServices.GetService<IWarehouseStockLookupService>(); |
| | 0 | 1929 | | if (lookupService == null) |
| | 0 | 1930 | | return new Dictionary<int, decimal>(); |
| | | 1931 | | |
| | 0 | 1932 | | return await lookupService.BuildC0MapAsync(orders, HttpContext.RequestAborted); |
| | 0 | 1933 | | } |
| | | 1934 | | |
| | | 1935 | | protected static void ApplyWarehouseStockC0ToOrders( |
| | | 1936 | | IEnumerable<Order> orders, |
| | | 1937 | | IReadOnlyDictionary<int, decimal> c0ByOrderId) |
| | | 1938 | | { |
| | 0 | 1939 | | foreach (var order in orders) |
| | | 1940 | | { |
| | 0 | 1941 | | if (c0ByOrderId.TryGetValue(order.Id, out var c0)) |
| | 0 | 1942 | | order.OriginalPurchasePrice = c0; |
| | | 1943 | | } |
| | 0 | 1944 | | } |
| | | 1945 | | |
| | | 1946 | | /// <summary>Счётчики (N) у артикула/города/номера — только по ключам видимых заказов, без GROUP BY всей таблицы |
| | | 1947 | | protected async Task<(Dictionary<string, int> Articles, Dictionary<string, int> Cities, Dictionary<string, int> |
| | | 1948 | | GetUniqueCountsForOrdersAsync(IEnumerable<Order> orders, bool includeArchived = false) |
| | | 1949 | | { |
| | 0 | 1950 | | return ( |
| | 0 | 1951 | | await OrderServices.GetArticleCountsForOrdersAsync(orders, includeArchived), |
| | 0 | 1952 | | await OrderServices.GetDeliveryCityCountsForOrdersAsync(orders, includeArchived), |
| | 0 | 1953 | | await OrderServices.GetShipmentBaseCountsForOrdersAsync(orders, includeArchived)); |
| | 0 | 1954 | | } |
| | | 1955 | | } |
| | | 1956 | | } |