| | | 1 | | using System.Security.Claims; |
| | | 2 | | using Microsoft.EntityFrameworkCore; |
| | | 3 | | using OzonDomains; |
| | | 4 | | using OzonDomains.Models; |
| | | 5 | | using OzonOrdersWeb.Areas.Studio2.Controllers.Base; |
| | | 6 | | using OzonOrdersWeb.WebServices.Currency; |
| | | 7 | | using OzonOrdersWeb.WebServices.Orders.Grid; |
| | | 8 | | using OzonOrdersWeb.WebServices.Transactions.Grid.Contracts; |
| | | 9 | | using OzonOrdersWeb.WebServices.Transactions.Grid.Definitions; |
| | | 10 | | using OzonRepositories.Context; |
| | | 11 | | using Services.ApiServices; |
| | | 12 | | using Services.ApiServices._1CApi; |
| | | 13 | | using Services.BackgroundJobsServices; |
| | | 14 | | using Services.ApiServices.OzonApi; |
| | | 15 | | using Services.ApiSupplierSettingsServices; |
| | | 16 | | using Services.Authorization.Services; |
| | | 17 | | using Services.CacheServices.Cache.UserCacheService; |
| | | 18 | | using Services.DataServices; |
| | | 19 | | using Services.StockSyncServices; |
| | | 20 | | using Services.TransactionUtilsServices; |
| | | 21 | | |
| | | 22 | | namespace OzonOrdersWeb.WebServices.Transactions.Grid.Load; |
| | | 23 | | |
| | | 24 | | public sealed record TransactionGridLoadRequest( |
| | | 25 | | IReadOnlyList<int> Ids, |
| | | 26 | | ClaimsPrincipal Principal, |
| | | 27 | | string? SourceUrl, |
| | | 28 | | string? ClientIp, |
| | | 29 | | string? UserAgent); |
| | | 30 | | |
| | | 31 | | public interface ITransactionGridLoadService |
| | | 32 | | { |
| | | 33 | | Task<TransactionGridPageDto> LoadAsync(TransactionDefinition definition, TransactionGridLoadRequest request, Cancell |
| | | 34 | | } |
| | | 35 | | |
| | | 36 | | /// <summary> |
| | | 37 | | /// Страница проведения: строки через конвейер OrdersGrid (RLS, гейтинг колонок), редактируемая часть из заказов |
| | | 38 | | /// БД, справочники по определению. Повторяет GET классических страниц (<c>CreateTransactionBase</c>), но отдаёт JSON. |
| | | 39 | | /// </summary> |
| | | 40 | | public sealed class TransactionGridLoadService : ITransactionGridLoadService |
| | | 41 | | { |
| | | 42 | | private readonly ApplicationDbContext _context; |
| | | 43 | | private readonly IOrdersGridRowHydrator _hydrator; |
| | | 44 | | private readonly IOrdersGridScopedQuery _scoped; |
| | | 45 | | private readonly OrdersDataServices _orders; |
| | | 46 | | private readonly AppStatusDataServices _appStatuses; |
| | | 47 | | private readonly SupplierDataServices _suppliers; |
| | | 48 | | private readonly WarehouseDataServices _warehouses; |
| | | 49 | | private readonly ExpenseTypeDataServices _expenseTypes; |
| | | 50 | | private readonly StatusTransitionDataServices _transitions; |
| | | 51 | | private readonly ICurrencyRatesViewBagService _rates; |
| | | 52 | | private readonly IUserCacheService _userCache; |
| | | 53 | | private readonly IScopeFilterValuesService? _scopeValues; |
| | | 54 | | private readonly IDataScopeService? _dataScope; |
| | | 55 | | private readonly IWarehouseStockLookupService? _stockLookup; |
| | | 56 | | private readonly ApiSupplierWarehouseService? _apiSupplierWarehouses; |
| | | 57 | | private readonly OzonOrderCancellationService? _cancellation; |
| | | 58 | | private readonly OneCDataManager? _oneC; |
| | | 59 | | private readonly ITransactionTimeTrackingService? _tracking; |
| | | 60 | | private readonly WarehouseSelectionResolver? _warehouseResolver; |
| | | 61 | | private readonly OneCReceiptWarehouseHistoryResolver? _receiptHistory; |
| | | 62 | | private readonly ILogger<TransactionGridLoadService> _logger; |
| | | 63 | | |
| | 0 | 64 | | public TransactionGridLoadService( |
| | 0 | 65 | | ApplicationDbContext context, |
| | 0 | 66 | | IOrdersGridRowHydrator hydrator, |
| | 0 | 67 | | IOrdersGridScopedQuery scoped, |
| | 0 | 68 | | OrdersDataServices orders, |
| | 0 | 69 | | AppStatusDataServices appStatuses, |
| | 0 | 70 | | SupplierDataServices suppliers, |
| | 0 | 71 | | WarehouseDataServices warehouses, |
| | 0 | 72 | | ExpenseTypeDataServices expenseTypes, |
| | 0 | 73 | | StatusTransitionDataServices transitions, |
| | 0 | 74 | | ICurrencyRatesViewBagService rates, |
| | 0 | 75 | | IUserCacheService userCache, |
| | 0 | 76 | | ILogger<TransactionGridLoadService> logger, |
| | 0 | 77 | | IScopeFilterValuesService? scopeValues = null, |
| | 0 | 78 | | IDataScopeService? dataScope = null, |
| | 0 | 79 | | IWarehouseStockLookupService? stockLookup = null, |
| | 0 | 80 | | ApiSupplierWarehouseService? apiSupplierWarehouses = null, |
| | 0 | 81 | | OzonOrderCancellationService? cancellation = null, |
| | 0 | 82 | | OneCDataManager? oneC = null, |
| | 0 | 83 | | ITransactionTimeTrackingService? tracking = null, |
| | 0 | 84 | | WarehouseSelectionResolver? warehouseResolver = null, |
| | 0 | 85 | | OneCReceiptWarehouseHistoryResolver? receiptHistory = null) |
| | | 86 | | { |
| | 0 | 87 | | _context = context; |
| | 0 | 88 | | _hydrator = hydrator; |
| | 0 | 89 | | _scoped = scoped; |
| | 0 | 90 | | _orders = orders; |
| | 0 | 91 | | _appStatuses = appStatuses; |
| | 0 | 92 | | _suppliers = suppliers; |
| | 0 | 93 | | _warehouses = warehouses; |
| | 0 | 94 | | _expenseTypes = expenseTypes; |
| | 0 | 95 | | _transitions = transitions; |
| | 0 | 96 | | _rates = rates; |
| | 0 | 97 | | _userCache = userCache; |
| | 0 | 98 | | _logger = logger; |
| | 0 | 99 | | _scopeValues = scopeValues; |
| | 0 | 100 | | _dataScope = dataScope; |
| | 0 | 101 | | _stockLookup = stockLookup; |
| | 0 | 102 | | _apiSupplierWarehouses = apiSupplierWarehouses; |
| | 0 | 103 | | _cancellation = cancellation; |
| | 0 | 104 | | _oneC = oneC; |
| | 0 | 105 | | _tracking = tracking; |
| | 0 | 106 | | _warehouseResolver = warehouseResolver; |
| | 0 | 107 | | _receiptHistory = receiptHistory; |
| | 0 | 108 | | } |
| | | 109 | | |
| | | 110 | | public async Task<TransactionGridPageDto> LoadAsync(TransactionDefinition d, TransactionGridLoadRequest request, Can |
| | | 111 | | { |
| | 0 | 112 | | var principal = request.Principal; |
| | 0 | 113 | | var userId = principal.FindFirstValue(ClaimTypes.NameIdentifier) ?? string.Empty; |
| | 0 | 114 | | var isAdmin = principal.IsInRole("Admin"); |
| | 0 | 115 | | var warnings = new List<string>(); |
| | 0 | 116 | | var skipped = new List<int>(); |
| | | 117 | | |
| | | 118 | | // 1. Ids → data scope → RLS → строки грида (в порядке ссылки). |
| | 0 | 119 | | var ids = request.Ids.Where(i => i > 0).Distinct().ToList(); |
| | 0 | 120 | | var scopedIds = await ApplyDataScopeAsync(ids, userId, ct); |
| | 0 | 121 | | var rows = await _hydrator.HydrateAsync(scopedIds, principal, ct); |
| | 0 | 122 | | var rowById = rows.ToDictionary(r => r.Id); |
| | 0 | 123 | | skipped.AddRange(ids.Where(id => !rowById.ContainsKey(id))); |
| | 0 | 124 | | if (skipped.Count > 0) |
| | | 125 | | { |
| | 0 | 126 | | warnings.Add(skipped.Count == 1 ? $"Заказ #{skipped[0]} недоступен и пропущен." : $"Недоступных заказов проп |
| | | 127 | | } |
| | | 128 | | |
| | | 129 | | // 2. Заказы БД (без трекинга) — для редактируемых полей и предупреждений. |
| | 0 | 130 | | var orders = rowById.Count > 0 ? await _orders.GetOrdersNoTracking(rowById.Keys.ToArray()) : new List<Order>(); |
| | 0 | 131 | | var orderById = orders.ToDictionary(o => o.Id); |
| | 0 | 132 | | orders = ids.Where(orderById.ContainsKey).Select(id => orderById[id]).ToList(); |
| | | 133 | | |
| | | 134 | | // 3. Статусы: переходы из БД или дефолты определения. |
| | 0 | 135 | | var (statuses, targetStatusId, transitions) = await BuildStatusesAsync(d, userId, ct); |
| | | 136 | | |
| | | 137 | | // 4. Правки по строкам. |
| | 0 | 138 | | var c0 = d.ClearPurchasePriceOnLoad && _stockLookup != null && orders.Count > 0 |
| | 0 | 139 | | ? await SafeC0Async(orders, ct) |
| | 0 | 140 | | : new Dictionary<int, decimal>(); |
| | | 141 | | // В выпадающем списке показываем обе стороны переходов и дополнительные статусы. |
| | | 142 | | // Та же совокупность считается допустимой для строки, иначе интерфейс предлагал статус, который тут же блокиров |
| | 0 | 143 | | var selectableStatusIds = statuses.Select(s => s.Id).ToHashSet(); |
| | 0 | 144 | | var allowedNames = d.AllowedSourceStatuses.ToHashSet(StringComparer.OrdinalIgnoreCase); |
| | 0 | 145 | | var statusOnly = d.StatusOnlyWhenAlreadyIn.ToHashSet(StringComparer.OrdinalIgnoreCase); |
| | 0 | 146 | | var disallowMarket = d.DisallowMarketplaceStatuses.ToHashSet(StringComparer.OrdinalIgnoreCase); |
| | 0 | 147 | | var fromToTarget = transitions.GroupBy(t => t.FromStatusId).ToDictionary(g => g.Key, g => g.First().ToStatusId); |
| | | 148 | | |
| | 0 | 149 | | var edits = new List<TransactionGridRowDto>(orders.Count); |
| | 0 | 150 | | foreach (var o in orders) |
| | | 151 | | { |
| | 0 | 152 | | var edit = new TransactionGridRowEdit |
| | 0 | 153 | | { |
| | 0 | 154 | | Id = o.Id, |
| | 0 | 155 | | SupplierId = o.SupplierId, |
| | 0 | 156 | | ShipmentWarehouseId = o.ShipmentWarehouseId, |
| | 0 | 157 | | Zasyl = TransactionWarehouseHelper.GetZasylValue(o), |
| | 0 | 158 | | OriginalPurchasePrice = o.PurchasePrice, |
| | 0 | 159 | | // Данные заказа — стартовое состояние транзакции. C0 остаётся только подсказкой, |
| | 0 | 160 | | // но не должен стирать уже заполненную цену закупки при переходе из OrdersGrid. |
| | 0 | 161 | | PurchasePrice = o.PurchasePrice, |
| | 0 | 162 | | WarehouseStockC0 = c0.TryGetValue(o.Id, out var c0Value) ? c0Value : null, |
| | 0 | 163 | | Price = o.Price, |
| | 0 | 164 | | CurrentPriceWithDiscount = o.ProductInfo?.CurrentPriceWithDiscount, |
| | 0 | 165 | | Quantity = o.Quantity, |
| | 0 | 166 | | OriginalQuantity = o.Quantity, |
| | 0 | 167 | | TrueQuantity = o.TrueQuantity, |
| | 0 | 168 | | ProductKey = o.ProductKey, |
| | 0 | 169 | | OrderNumberToSupplier = o.OrderNumberToSupplier, |
| | 0 | 170 | | Weight = o.ProductInfo?.Weight, |
| | 0 | 171 | | Manufacturer = o.Manufacturer?.Name, |
| | 0 | 172 | | ExtraExpenses = (o.ExtraExpenses ?? new List<OrderExtraExpense>()) |
| | 0 | 173 | | .Select(e => new TransactionGridExtraExpenseEdit { ExpenseTypeId = e.ExpenseTypeId, Amount = e.Amoun |
| | 0 | 174 | | .ToList() |
| | 0 | 175 | | }; |
| | | 176 | | |
| | 0 | 177 | | var statusName = o.AppStatus?.Name ?? string.Empty; |
| | 0 | 178 | | if (d.PriceEditingOnly) |
| | | 179 | | { |
| | 0 | 180 | | edit.AppStatusId = o.AppStatusId; |
| | | 181 | | } |
| | 0 | 182 | | else if (statusOnly.Contains(statusName)) |
| | | 183 | | { |
| | 0 | 184 | | edit.StatusOnly = true; |
| | 0 | 185 | | edit.AppStatusId = targetStatusId ?? o.AppStatusId; |
| | | 186 | | } |
| | | 187 | | // Для транзакций с явными переходами сохраняем их семантику: исходный статус |
| | | 188 | | // сопоставляется с целевым. Для status-only сценариев список содержит только |
| | | 189 | | // доступные для выбора целевые статусы, поэтому исходный разрешённый статус |
| | | 190 | | // (например «Не указан») должен сразу получать TargetStatusName. |
| | 0 | 191 | | else if (transitions.Count > 0 && selectableStatusIds.Count > 0) |
| | | 192 | | { |
| | 0 | 193 | | if (selectableStatusIds.Contains(o.AppStatusId ?? 0)) |
| | | 194 | | { |
| | 0 | 195 | | edit.AppStatusId = fromToTarget.TryGetValue(o.AppStatusId ?? 0, out var to) ? to : targetStatusId; |
| | | 196 | | } |
| | | 197 | | else |
| | | 198 | | { |
| | 0 | 199 | | edit.AppStatusId = o.AppStatusId; |
| | 0 | 200 | | edit.Warnings.Add($"Статус «{statusName}» не входит в допустимые для транзакции."); |
| | | 201 | | } |
| | | 202 | | } |
| | 0 | 203 | | else if (allowedNames.Count > 0 && !allowedNames.Contains(statusName)) |
| | | 204 | | { |
| | 0 | 205 | | edit.AppStatusId = o.AppStatusId; |
| | 0 | 206 | | edit.Warnings.Add($"Статус «{statusName}», а должен быть: {string.Join(" / ", allowedNames)}."); |
| | | 207 | | } |
| | 0 | 208 | | else if (targetStatusId.HasValue) |
| | | 209 | | { |
| | 0 | 210 | | edit.AppStatusId = targetStatusId; |
| | | 211 | | } |
| | | 212 | | else |
| | | 213 | | { |
| | 0 | 214 | | edit.AppStatusId = targetStatusId ?? o.AppStatusId; |
| | | 215 | | } |
| | | 216 | | |
| | 0 | 217 | | if (!string.IsNullOrEmpty(o.Status) && disallowMarket.Contains(o.Status)) |
| | | 218 | | { |
| | 0 | 219 | | edit.Warnings.Add($"Статус маркетплейса «{o.Status}» — проведение недоступно."); |
| | | 220 | | } |
| | 0 | 221 | | else if (!d.PriceEditingOnly && d.TargetStatusName != null && !TransactionConfig.StandardOrderFilter(o)) |
| | | 222 | | { |
| | 0 | 223 | | edit.Warnings.Add("Заказ отменён маркетплейсом."); |
| | | 224 | | } |
| | | 225 | | |
| | 0 | 226 | | edits.Add(new TransactionGridRowDto { Base = rowById[o.Id], Edit = edit }); |
| | | 227 | | } |
| | | 228 | | |
| | 0 | 229 | | if (d.RequireSingleSupplierAndWarehouse && orders.Count > 1) |
| | | 230 | | { |
| | 0 | 231 | | var inns = orders.Select(o => o.Supplier?.INNCode?.Trim()).Where(i => !string.IsNullOrEmpty(i)).Distinct().T |
| | 0 | 232 | | if (inns.Count > 1) |
| | | 233 | | { |
| | 0 | 234 | | warnings.Add("Заказы от разных поставщиков (по ИНН) — приходная накладная требует одного поставщика."); |
| | | 235 | | } |
| | | 236 | | |
| | 0 | 237 | | var whs = orders.Select(TransactionWarehouseHelper.GetEffectiveWarehouseName).Where(w => !string.IsNullOrWhi |
| | 0 | 238 | | if (whs.Count > 1) |
| | | 239 | | { |
| | 0 | 240 | | warnings.Add($"Заказы с разных складов: {string.Join(", ", whs)}."); |
| | | 241 | | } |
| | | 242 | | } |
| | | 243 | | |
| | | 244 | | // 5. Справочники. |
| | 0 | 245 | | var lookups = await BuildLookupsAsync(d, orders, statuses, userId, isAdmin, ct); |
| | | 246 | | |
| | | 247 | | // 5a. Подсказки складов 1С по режиму определения (как на GET классических страниц). |
| | 0 | 248 | | await ApplyWarehouseSuggestionsAsync(d, orders, edits, lookups, warnings, isAdmin); |
| | | 249 | | |
| | | 250 | | // 6. Учёт времени. |
| | 0 | 251 | | string? trackingKey = null; |
| | 0 | 252 | | if (_tracking != null && orders.Count > 0) |
| | | 253 | | { |
| | | 254 | | try |
| | | 255 | | { |
| | 0 | 256 | | var cached = await _userCache.GetCachedUserAsync(principal); |
| | 0 | 257 | | trackingKey = await _tracking.StartSessionAsync( |
| | 0 | 258 | | principal.Identity?.Name ?? string.Empty, cached?.AccessProfileId, d.Type, |
| | 0 | 259 | | orders.Select(o => o.Id), request.SourceUrl, request.ClientIp, request.UserAgent); |
| | 0 | 260 | | } |
| | 0 | 261 | | catch (OperationCanceledException) when (ct.IsCancellationRequested) |
| | | 262 | | { |
| | 0 | 263 | | throw; |
| | | 264 | | } |
| | 0 | 265 | | catch (OperationCanceledException ex) |
| | | 266 | | { |
| | 0 | 267 | | _logger.LogInformation("TransactionsGrid: старт сессии учёта времени отменён ({Message})", ex.Message); |
| | 0 | 268 | | } |
| | 0 | 269 | | catch (Exception ex) |
| | | 270 | | { |
| | 0 | 271 | | _logger.LogWarning(ex, "TransactionsGrid: не удалось начать сессию учёта времени"); |
| | 0 | 272 | | } |
| | | 273 | | } |
| | | 274 | | |
| | 0 | 275 | | return new TransactionGridPageDto |
| | 0 | 276 | | { |
| | 0 | 277 | | Definition = TransactionDefinitionDto.From(d, targetStatusId), |
| | 0 | 278 | | Rows = edits, |
| | 0 | 279 | | Lookups = lookups, |
| | 0 | 280 | | Warnings = warnings, |
| | 0 | 281 | | SkippedIds = skipped, |
| | 0 | 282 | | TrackingKey = trackingKey, |
| | 0 | 283 | | IsAdmin = isAdmin |
| | 0 | 284 | | }; |
| | 0 | 285 | | } |
| | | 286 | | |
| | | 287 | | /// <summary> |
| | | 288 | | /// GlobalPair (WarehouseTransfer): отправитель/получатели по WarehouseSelectionResolver, не-админу отправитель фикс |
| | | 289 | | /// PerOrderRecipient (CustomerReturn): получатель по заказу. PerOrderBoth (ReturnToSupplier): отправитель по маппин |
| | | 290 | | /// возврата, получатель — по последнему проведённому приходу 1С для артикула. |
| | | 291 | | /// </summary> |
| | | 292 | | private async Task ApplyWarehouseSuggestionsAsync(TransactionDefinition d, List<Order> orders, List<TransactionGridR |
| | | 293 | | { |
| | 0 | 294 | | if (orders.Count == 0 || _warehouseResolver == null || d.OneCMode == OneCMode.None) |
| | | 295 | | { |
| | 0 | 296 | | return; |
| | | 297 | | } |
| | | 298 | | |
| | | 299 | | try |
| | | 300 | | { |
| | 0 | 301 | | switch (d.WarehouseMode) |
| | | 302 | | { |
| | | 303 | | case WarehouseMode.GlobalPair: |
| | | 304 | | { |
| | 0 | 305 | | var sender = await _warehouseResolver.ResolveSenderWarehouseAsync(orders); |
| | 0 | 306 | | var recipient = await _warehouseResolver.ResolveRecipientWarehousesAsync(orders); |
| | 0 | 307 | | lookups.SuggestedSenderWarehouse = sender.SenderWarehouseName; |
| | 0 | 308 | | lookups.SuggestedRecipientWarehouses.AddRange(recipient.PreferredRecipientWarehouses); |
| | 0 | 309 | | if (!isAdmin && sender.HasErrors) |
| | | 310 | | { |
| | 0 | 311 | | warnings.AddRange(sender.Errors); |
| | | 312 | | } |
| | | 313 | | |
| | 0 | 314 | | warnings.AddRange(recipient.Warnings); |
| | 0 | 315 | | break; |
| | | 316 | | } |
| | | 317 | | case WarehouseMode.PerOrderRecipient: |
| | | 318 | | { |
| | 0 | 319 | | var res = await _warehouseResolver.ResolveCustomerReturnRecipientByOrderAsync(orders); |
| | 0 | 320 | | foreach (var row in rows) |
| | | 321 | | { |
| | 0 | 322 | | if (res.RecipientByOrderId.TryGetValue(row.Edit.Id, out var wh)) |
| | | 323 | | { |
| | 0 | 324 | | row.Edit.RecipientWarehouse = wh; |
| | 0 | 325 | | row.Edit.RecipientSuggestions.Add(new TransactionGridWarehouseSuggestion(wh, null, res.Recip |
| | | 326 | | } |
| | | 327 | | } |
| | | 328 | | |
| | 0 | 329 | | if (res.HasBlockingErrors) |
| | | 330 | | { |
| | 0 | 331 | | warnings.AddRange(res.BlockingErrors); |
| | | 332 | | } |
| | | 333 | | |
| | 0 | 334 | | break; |
| | | 335 | | } |
| | | 336 | | case WarehouseMode.PerOrderBoth: |
| | | 337 | | { |
| | 0 | 338 | | var senders = await _warehouseResolver.ResolveCustomerReturnRecipientByOrderAsync(orders); |
| | 0 | 339 | | foreach (var row in rows) |
| | | 340 | | { |
| | 0 | 341 | | if (senders.RecipientByOrderId.TryGetValue(row.Edit.Id, out var wh)) |
| | | 342 | | { |
| | 0 | 343 | | row.Edit.SenderWarehouse = wh; |
| | | 344 | | } |
| | | 345 | | } |
| | | 346 | | |
| | 0 | 347 | | if (_receiptHistory != null && lookups.OneCWarehouses.Count > 0) |
| | | 348 | | { |
| | 0 | 349 | | var history = await _receiptHistory.ResolveByArticlesAsync(orders.Select(o => o.Article), lookup |
| | 0 | 350 | | foreach (var row in rows) |
| | | 351 | | { |
| | 0 | 352 | | var article = orders.First(o => o.Id == row.Edit.Id).Article?.Trim(); |
| | 0 | 353 | | if (string.IsNullOrEmpty(article) || !history.TryGetValue(article, out var list) || list.Cou |
| | | 354 | | { |
| | | 355 | | continue; |
| | | 356 | | } |
| | | 357 | | |
| | 0 | 358 | | row.Edit.RecipientWarehouse = list[0].Warehouse; |
| | 0 | 359 | | row.Edit.RecipientSuggestions.AddRange(list.Select(x => new TransactionGridWarehouseSuggesti |
| | | 360 | | } |
| | | 361 | | } |
| | | 362 | | |
| | | 363 | | break; |
| | | 364 | | } |
| | | 365 | | } |
| | 0 | 366 | | } |
| | 0 | 367 | | catch (OperationCanceledException ex) |
| | | 368 | | { |
| | 0 | 369 | | _logger.LogInformation("TransactionsGrid: подсказки складов отменены ({Message})", ex.Message); |
| | 0 | 370 | | } |
| | 0 | 371 | | catch (Exception ex) |
| | | 372 | | { |
| | 0 | 373 | | _logger.LogWarning(ex, "TransactionsGrid: подсказки складов недоступны"); |
| | 0 | 374 | | warnings.Add("Подсказки складов 1С недоступны — выберите склады вручную."); |
| | 0 | 375 | | } |
| | 0 | 376 | | } |
| | | 377 | | |
| | | 378 | | private async Task<List<int>> ApplyDataScopeAsync(List<int> ids, string userId, CancellationToken ct) |
| | | 379 | | { |
| | 0 | 380 | | if (ids.Count == 0) |
| | | 381 | | { |
| | 0 | 382 | | return ids; |
| | | 383 | | } |
| | | 384 | | |
| | 0 | 385 | | IQueryable<Order> q = _context.Orders.AsNoTracking().Where(o => ids.Contains(o.Id)); |
| | 0 | 386 | | if (_dataScope != null && !string.IsNullOrWhiteSpace(userId)) |
| | | 387 | | { |
| | 0 | 388 | | q = await _dataScope.ApplyDataScopeAsync(q, userId, "Order", null, ct); |
| | | 389 | | } |
| | | 390 | | |
| | 0 | 391 | | var allowed = (await q.Select(o => o.Id).ToListAsync(ct)).ToHashSet(); |
| | 0 | 392 | | return ids.Where(allowed.Contains).ToList(); |
| | 0 | 393 | | } |
| | | 394 | | |
| | | 395 | | /// <summary>Список статусов для выпадающего списка строки — как <c>CreateTransactionBase</c>: переходы ⊕ дополнител |
| | | 396 | | private async Task<(List<StatusOption> Statuses, int? TargetStatusId, List<StatusTransition> Transitions)> BuildStat |
| | | 397 | | { |
| | 0 | 398 | | var transitions = d.UseStatusTransitions ? await _transitions.GetByTransactionType(d.Type) : new List<StatusTran |
| | 0 | 399 | | var list = new List<AppStatus>(); |
| | 0 | 400 | | AppStatus? target = null; |
| | | 401 | | |
| | 0 | 402 | | if (transitions.Count > 0) |
| | | 403 | | { |
| | 0 | 404 | | list = transitions.Select(t => t.ToStatus).Concat(transitions.Select(t => t.FromStatus)) |
| | 0 | 405 | | .Where(s => s != null).Select(s => s!).GroupBy(s => s.Id).Select(g => g.First()).OrderBy(s => s.Name).To |
| | 0 | 406 | | target = transitions.Select(t => t.ToStatus).FirstOrDefault(s => s != null); |
| | | 407 | | } |
| | 0 | 408 | | else if (!string.IsNullOrEmpty(d.TargetStatusName)) |
| | | 409 | | { |
| | 0 | 410 | | target = await _appStatuses.GetAppStatusAsync(new AppStatus { Name = d.TargetStatusName }); |
| | 0 | 411 | | if (target != null) |
| | | 412 | | { |
| | 0 | 413 | | list.Add(target); |
| | | 414 | | } |
| | | 415 | | } |
| | 0 | 416 | | else if (!d.PriceEditingOnly) |
| | | 417 | | { |
| | 0 | 418 | | list = (await _appStatuses.GetAppStatuses()).OrderBy(s => s.Name).ToList(); |
| | | 419 | | } |
| | | 420 | | |
| | 0 | 421 | | foreach (var extra in d.ExtraStatusNames) |
| | | 422 | | { |
| | 0 | 423 | | var s = await _appStatuses.GetAppStatusAsync(new AppStatus { Name = extra }); |
| | 0 | 424 | | if (s != null && list.All(x => x.Id != s.Id)) |
| | | 425 | | { |
| | 0 | 426 | | list.Add(s); |
| | | 427 | | } |
| | 0 | 428 | | } |
| | | 429 | | |
| | 0 | 430 | | list = list.GroupBy(s => s.Id).Select(g => g.First()).ToList(); |
| | 0 | 431 | | list = await FilterByOrderScopeAsync(list, s => s.Id, "AppStatusId", userId, ct); |
| | | 432 | | |
| | 0 | 433 | | var options = list.Select(s => new StatusOption(s.Id, s.Name ?? string.Empty, s.GetStatusColor(), s.IsFinal)).To |
| | 0 | 434 | | return (options, target?.Id, transitions); |
| | 0 | 435 | | } |
| | | 436 | | |
| | | 437 | | private async Task<TransactionGridLookups> BuildLookupsAsync(TransactionDefinition d, List<Order> orders, List<Statu |
| | | 438 | | { |
| | 0 | 439 | | var rateTasks = _rates.StartFetch(); |
| | 0 | 440 | | var suppliers = await FilterByOrderScopeAsync(await _suppliers.GetSuppliers(), s => s.Id, "SupplierId", userId, |
| | 0 | 441 | | var warehouses = await FilterByOrderScopeAsync(await _warehouses.GetWarehouses(), w => w.Id, "ShipmentWarehouseI |
| | 0 | 442 | | var expenseTypes = (await _expenseTypes.GetExpenseTypes()).Where(e => e.IsActive).OrderBy(e => e.Name).ToList(); |
| | | 443 | | |
| | 0 | 444 | | var oneCWarehouseNames = new List<string>(); |
| | 0 | 445 | | var needsOneC = d.OneCMode != OneCMode.None || d.EditableFields.Contains("zasyl"); |
| | 0 | 446 | | if (needsOneC && _oneC != null) |
| | | 447 | | { |
| | | 448 | | try |
| | | 449 | | { |
| | 0 | 450 | | oneCWarehouseNames = (await _oneC.GetWarehouseListAsync()).Select(w => w.Name).Where(n => !string.IsNull |
| | 0 | 451 | | } |
| | 0 | 452 | | catch (OperationCanceledException) when (ct.IsCancellationRequested) |
| | | 453 | | { |
| | 0 | 454 | | throw; |
| | | 455 | | } |
| | 0 | 456 | | catch (OperationCanceledException ex) |
| | | 457 | | { |
| | 0 | 458 | | _logger.LogInformation("TransactionsGrid: получение списка складов 1С отменено ({Message})", ex.Message) |
| | 0 | 459 | | } |
| | 0 | 460 | | catch (Exception ex) |
| | | 461 | | { |
| | 0 | 462 | | _logger.LogWarning(ex, "TransactionsGrid: список складов 1С недоступен"); |
| | 0 | 463 | | } |
| | | 464 | | } |
| | | 465 | | |
| | 0 | 466 | | var zasylValues = await FilterWarehouseNamesByOrderScopeAsync(oneCWarehouseNames, userId, ct); |
| | 0 | 467 | | var bitrixWarehouses = d.OneCMode != OneCMode.None |
| | 0 | 468 | | ? await FilterBitrixWarehouseNamesByScopeAsync(oneCWarehouseNames, userId, ct) |
| | 0 | 469 | | : new List<string>(); |
| | | 470 | | |
| | 0 | 471 | | var cancelReasons = new Dictionary<int, OzonOrderCancelReasonsInfo>(); |
| | 0 | 472 | | if (d.OzonCancelPanel && _cancellation != null && orders.Count > 0) |
| | | 473 | | { |
| | | 474 | | try |
| | | 475 | | { |
| | 0 | 476 | | cancelReasons = await _cancellation.LoadCancelReasonsAsync(orders); |
| | 0 | 477 | | } |
| | 0 | 478 | | catch (OperationCanceledException) when (ct.IsCancellationRequested) |
| | | 479 | | { |
| | 0 | 480 | | throw; |
| | | 481 | | } |
| | 0 | 482 | | catch (OperationCanceledException ex) |
| | | 483 | | { |
| | 0 | 484 | | _logger.LogInformation("TransactionsGrid: загрузка причин отмены Ozon отменена ({Message})", ex.Message) |
| | 0 | 485 | | } |
| | 0 | 486 | | catch (Exception ex) |
| | | 487 | | { |
| | 0 | 488 | | _logger.LogWarning(ex, "TransactionsGrid: причины отмены Ozon недоступны"); |
| | 0 | 489 | | } |
| | | 490 | | } |
| | | 491 | | |
| | 0 | 492 | | var allowedWarehouses = new Dictionary<string, List<int>>(); |
| | 0 | 493 | | if (d.SupplierOffers && _apiSupplierWarehouses != null) |
| | | 494 | | { |
| | 0 | 495 | | allowedWarehouses = await _apiSupplierWarehouses.GetAllAllowedWarehouseIdsByProviderAsync(ct); |
| | | 496 | | } |
| | | 497 | | |
| | 0 | 498 | | var rates = await rateTasks.ToSnapshotAsync(); |
| | | 499 | | |
| | | 500 | | // ИНН для договоров 1С: приход — поставщик заказов, расход — клиент; сами договоры грузятся лениво (Lookups?kin |
| | 0 | 501 | | string? treatiesInn = null; |
| | 0 | 502 | | if (d.ReceiptDocumentFields) |
| | | 503 | | { |
| | 0 | 504 | | treatiesInn = orders.Select(o => o.Supplier?.INNCode?.Trim()).FirstOrDefault(i => !string.IsNullOrEmpty(i)); |
| | | 505 | | } |
| | 0 | 506 | | else if (d.ExpenseDocumentFields) |
| | | 507 | | { |
| | 0 | 508 | | treatiesInn = orders.Select(o => o.OzonClient?.INNCode?.Trim()).FirstOrDefault(i => !string.IsNullOrEmpty(i) |
| | | 509 | | } |
| | | 510 | | |
| | 0 | 511 | | return new TransactionGridLookups |
| | 0 | 512 | | { |
| | 0 | 513 | | Statuses = statuses, |
| | 0 | 514 | | Suppliers = suppliers.OrderBy(s => s.Name).Select(s => new SupplierOption(s.Id, s.Name ?? string.Empty, s.Si |
| | 0 | 515 | | Warehouses = warehouses.OrderBy(w => w.Name).Select(w => new IdName(w.Id, w.Name ?? string.Empty)).ToList(), |
| | 0 | 516 | | ExpenseTypes = expenseTypes.Select(e => new IdName(e.Id, e.Name ?? string.Empty)).ToList(), |
| | 0 | 517 | | ZasylValues = zasylValues, |
| | 0 | 518 | | CancelReasonsByOrder = cancelReasons, |
| | 0 | 519 | | ApiSupplierAllowedWarehouses = allowedWarehouses, |
| | 0 | 520 | | RateUsd = rates.RateUsd, |
| | 0 | 521 | | RateEur = rates.RateEur, |
| | 0 | 522 | | RateByn = rates.RateByn, |
| | 0 | 523 | | OneCWarehouses = bitrixWarehouses, |
| | 0 | 524 | | ForceSenderWarehouse = d.WarehouseMode == WarehouseMode.GlobalPair && !isAdmin, |
| | 0 | 525 | | TreatiesInn = treatiesInn |
| | 0 | 526 | | }; |
| | 0 | 527 | | } |
| | | 528 | | |
| | | 529 | | private async Task<Dictionary<int, decimal>> SafeC0Async(List<Order> orders, CancellationToken ct) |
| | | 530 | | { |
| | | 531 | | try |
| | | 532 | | { |
| | 0 | 533 | | return await _stockLookup!.BuildC0MapAsync(orders, ct); |
| | | 534 | | } |
| | 0 | 535 | | catch (OperationCanceledException) when (ct.IsCancellationRequested) |
| | | 536 | | { |
| | 0 | 537 | | throw; |
| | | 538 | | } |
| | 0 | 539 | | catch (OperationCanceledException ex) |
| | | 540 | | { |
| | 0 | 541 | | _logger.LogInformation("TransactionsGrid: операция получения остатков C0 отменена ({Message})", ex.Message); |
| | 0 | 542 | | return new Dictionary<int, decimal>(); |
| | | 543 | | } |
| | 0 | 544 | | catch (Exception ex) |
| | | 545 | | { |
| | 0 | 546 | | _logger.LogWarning(ex, "TransactionsGrid: остатки C0 недоступны"); |
| | 0 | 547 | | return new Dictionary<int, decimal>(); |
| | | 548 | | } |
| | 0 | 549 | | } |
| | | 550 | | |
| | | 551 | | private async Task<List<T>> FilterByOrderScopeAsync<T>(List<T> source, Func<T, int> idSelector, string scopeField, s |
| | | 552 | | { |
| | 0 | 553 | | if (source.Count == 0 || _scopeValues == null || string.IsNullOrWhiteSpace(userId)) |
| | | 554 | | { |
| | 0 | 555 | | return source; |
| | | 556 | | } |
| | | 557 | | |
| | 0 | 558 | | var allowed = await _scopeValues.GetAllowedValuesAsync(userId, "Order", ct); |
| | 0 | 559 | | if (!allowed.TryGetValue(scopeField, out var raw) || raw.Count == 0) |
| | | 560 | | { |
| | 0 | 561 | | return source; |
| | | 562 | | } |
| | | 563 | | |
| | 0 | 564 | | var ids = raw.Select(v => int.TryParse(v, out var id) ? id : (int?)null).Where(v => v.HasValue).Select(v => v!.V |
| | 0 | 565 | | return ids.Count == 0 ? source : source.Where(x => ids.Contains(idSelector(x))).ToList(); |
| | 0 | 566 | | } |
| | | 567 | | |
| | | 568 | | private async Task<List<string>> FilterWarehouseNamesByOrderScopeAsync(IEnumerable<string> names, string userId, Can |
| | | 569 | | { |
| | 0 | 570 | | var normalized = names.Where(n => !string.IsNullOrWhiteSpace(n)).Distinct(StringComparer.OrdinalIgnoreCase).Orde |
| | 0 | 571 | | if (normalized.Count == 0 || _scopeValues == null || string.IsNullOrWhiteSpace(userId)) |
| | | 572 | | { |
| | 0 | 573 | | return normalized; |
| | | 574 | | } |
| | | 575 | | |
| | 0 | 576 | | var allowed = await _scopeValues.GetAllowedValuesAsync(userId, "Order", ct); |
| | 0 | 577 | | if (!allowed.TryGetValue("ShipmentWarehouseId", out var raw) || raw.Count == 0) |
| | | 578 | | { |
| | 0 | 579 | | return normalized; |
| | | 580 | | } |
| | | 581 | | |
| | 0 | 582 | | var ids = raw.Select(v => int.TryParse(v, out var id) ? id : (int?)null).Where(v => v.HasValue).Select(v => v!.V |
| | 0 | 583 | | if (ids.Count == 0) |
| | | 584 | | { |
| | 0 | 585 | | return normalized; |
| | | 586 | | } |
| | | 587 | | |
| | 0 | 588 | | var allowedNames = (await _warehouses.GetWarehouses()).Where(w => ids.Contains(w.Id)).Select(w => w.Name) |
| | 0 | 589 | | .Where(n => !string.IsNullOrWhiteSpace(n)).Select(n => n!).ToHashSet(StringComparer.OrdinalIgnoreCase); |
| | 0 | 590 | | return normalized.Where(allowedNames.Contains).OrderBy(n => n).ToList(); |
| | 0 | 591 | | } |
| | | 592 | | |
| | | 593 | | private async Task<List<string>> FilterBitrixWarehouseNamesByScopeAsync(IEnumerable<string> names, string userId, Ca |
| | | 594 | | { |
| | 0 | 595 | | var normalized = names.Where(n => !string.IsNullOrWhiteSpace(n)).Distinct(StringComparer.OrdinalIgnoreCase).Orde |
| | 0 | 596 | | if (_scopeValues == null || string.IsNullOrWhiteSpace(userId)) |
| | | 597 | | { |
| | 0 | 598 | | return normalized; |
| | | 599 | | } |
| | | 600 | | |
| | 0 | 601 | | var allowed = await _scopeValues.GetAllowedValuesAsync(userId, "BitrixWarehouse", ct); |
| | 0 | 602 | | if (!allowed.TryGetValue("Name", out var allowedNames) || allowedNames.Count == 0) |
| | | 603 | | { |
| | 0 | 604 | | return normalized; |
| | | 605 | | } |
| | | 606 | | |
| | 0 | 607 | | return normalized.Where(allowedNames.Contains).OrderBy(n => n).ToList(); |
| | 0 | 608 | | } |
| | | 609 | | } |