| | | 1 | | using Microsoft.Extensions.Logging; |
| | | 2 | | using Services.ApiServices._1CApi.Models; |
| | | 3 | | |
| | | 4 | | namespace Services.ApiServices._1CApi; |
| | | 5 | | |
| | | 6 | | /// <summary> |
| | | 7 | | /// Подбирает склады-получатели для возврата поставщику по последним проведённым приходам 1С. |
| | | 8 | | /// </summary> |
| | | 9 | | public sealed class OneCReceiptWarehouseHistoryResolver( |
| | | 10 | | OneCDataManager oneCDataManager, |
| | | 11 | | ILogger<OneCReceiptWarehouseHistoryResolver> logger) |
| | | 12 | | { |
| | | 13 | | private const int MaxConcurrentRequests = 4; |
| | | 14 | | |
| | | 15 | | public async Task<IReadOnlyDictionary<string, IReadOnlyList<OneCReceiptWarehouseSuggestion>>> ResolveByArticlesAsync |
| | | 16 | | IEnumerable<string?> articles, |
| | | 17 | | IEnumerable<string?> availableWarehouses) |
| | | 18 | | { |
| | | 19 | | var normalizedArticles = articles |
| | | 20 | | .Where(article => !string.IsNullOrWhiteSpace(article)) |
| | | 21 | | .Select(article => article!.Trim()) |
| | | 22 | | .Distinct(StringComparer.OrdinalIgnoreCase) |
| | | 23 | | .ToList(); |
| | | 24 | | var allowedWarehouses = new HashSet<string>( |
| | | 25 | | availableWarehouses.Where(warehouse => !string.IsNullOrWhiteSpace(warehouse)).Select(warehouse => warehouse! |
| | | 26 | | StringComparer.OrdinalIgnoreCase); |
| | | 27 | | var result = new Dictionary<string, IReadOnlyList<OneCReceiptWarehouseSuggestion>>(StringComparer.OrdinalIgnoreC |
| | | 28 | | |
| | | 29 | | using var semaphore = new SemaphoreSlim(MaxConcurrentRequests); |
| | | 30 | | var tasks = normalizedArticles.Select(async article => |
| | | 31 | | { |
| | | 32 | | await semaphore.WaitAsync(); |
| | | 33 | | try |
| | | 34 | | { |
| | | 35 | | var movementMap = await oneCDataManager.GetMovementMapAsync(article); |
| | | 36 | | var suggestions = SelectLatestPostedReceipts(movementMap?.Movements ?? [], allowedWarehouses); |
| | | 37 | | lock (result) |
| | | 38 | | result[article] = suggestions; |
| | | 39 | | } |
| | | 40 | | catch (Exception ex) |
| | | 41 | | { |
| | | 42 | | logger.LogWarning(ex, "[ReturnToSupplier] Не удалось получить историю приходов 1С для {Article}", articl |
| | | 43 | | lock (result) |
| | | 44 | | result[article] = []; |
| | | 45 | | } |
| | | 46 | | finally |
| | | 47 | | { |
| | | 48 | | semaphore.Release(); |
| | | 49 | | } |
| | | 50 | | }); |
| | | 51 | | |
| | | 52 | | await Task.WhenAll(tasks); |
| | | 53 | | return result; |
| | | 54 | | } |
| | | 55 | | |
| | | 56 | | /// <summary> |
| | | 57 | | /// Оставляет по одному самому свежему проведённому приходу для каждого доступного склада 1С. |
| | | 58 | | /// </summary> |
| | | 59 | | public static IReadOnlyList<OneCReceiptWarehouseSuggestion> SelectLatestPostedReceipts( |
| | | 60 | | IEnumerable<MovementItem> movements, |
| | | 61 | | ISet<string> availableWarehouses) |
| | | 62 | | { |
| | | 63 | | return movements |
| | | 64 | | .Where(movement => movement.Kind == MovementKind.Receipt && movement.IsPosted && !movement.IsDeleted) |
| | | 65 | | .Where(movement => !string.IsNullOrWhiteSpace(movement.WarehouseTo)) |
| | | 66 | | .Where(movement => availableWarehouses.Contains(movement.WarehouseTo!)) |
| | | 67 | | .OrderByDescending(movement => movement.Date) |
| | | 68 | | .ThenByDescending(movement => movement.DocNumber, StringComparer.OrdinalIgnoreCase) |
| | | 69 | | .GroupBy(movement => movement.WarehouseTo!, StringComparer.OrdinalIgnoreCase) |
| | | 70 | | .Select(group => group.First()) |
| | | 71 | | .Select(movement => new OneCReceiptWarehouseSuggestion( |
| | | 72 | | movement.WarehouseTo!, movement.Date, movement.DocNumber, movement.RefKey)) |
| | | 73 | | .ToList(); |
| | | 74 | | } |
| | | 75 | | } |
| | | 76 | | |
| | 5 | 77 | | public sealed record OneCReceiptWarehouseSuggestion( |
| | 4 | 78 | | string Warehouse, |
| | 0 | 79 | | DateTime Date, |
| | 3 | 80 | | string DocumentNumber, |
| | 5 | 81 | | Guid DocumentRefKey); |