| | | 1 | | using Microsoft.Extensions.Logging; |
| | | 2 | | using Newtonsoft.Json; |
| | | 3 | | using Services.ApiServices._1CApi.OData.Models; |
| | | 4 | | |
| | | 5 | | namespace Services.ApiServices._1CApi.OData; |
| | | 6 | | |
| | | 7 | | /// <summary>Приходная накладная, уже существующая в 1С и совпавшая с создаваемой.</summary> |
| | 3 | 8 | | public sealed record ExistingReceipt(Guid RefKey, string? Number, bool Posted); |
| | | 9 | | |
| | | 10 | | /// <summary> |
| | | 11 | | /// Ищет в 1С приходную накладную, которую собираются создать повторно: повтор проведения транзакции, |
| | | 12 | | /// ретрай после таймаута. Ключ — «НомерВходящегоДокумента + контрагент»; склад-получатель и состав строк |
| | | 13 | | /// уточняют совпадение, иначе под защиту попадали бы законные документы: |
| | | 14 | | /// <list type="bullet"> |
| | | 15 | | /// <item>одна транзакция режется по клиентам Ozon, и у всех документов один входящий номер и один поставщик;</item> |
| | | 16 | | /// <item>одну фактуру поставщика проводят частями — другими позициями или другим количеством.</item> |
| | | 17 | | /// </list> |
| | | 18 | | /// Отбор в 1С Fresh ограничен: <c>eq</c> по guid отдаёт 500, а guid-условие после |
| | | 19 | | /// <c>and</c> молча отбрасывается. Поэтому в запросе только <c>substringof</c> по номеру и булев <c>DeletionMark</c>, |
| | | 20 | | /// а контрагента, склад и точное равенство номера проверяет код по ответу. |
| | | 21 | | /// </summary> |
| | | 22 | | public sealed class OneCReceiptDuplicateFinder |
| | | 23 | | { |
| | | 24 | | private const string ReceiptEndpoint = "Document_ПриходнаяНакладная"; |
| | | 25 | | private const int ProbeTop = 100; |
| | | 26 | | |
| | | 27 | | private const string Select = |
| | | 28 | | "Ref_Key,Number,Date,Posted,DeletionMark,НомерВходящегоДокумента,Контрагент_Key,СтруктурнаяЕдиница_Key," |
| | | 29 | | + "Запасы/Номенклатура_Key,Запасы/Количество"; |
| | | 30 | | |
| | | 31 | | private readonly IOneCODataClient _client; |
| | | 32 | | private readonly ILogger _logger; |
| | | 33 | | |
| | | 34 | | public OneCReceiptDuplicateFinder(IOneCODataClient client, ILogger logger) |
| | | 35 | | { |
| | | 36 | | _client = client; |
| | | 37 | | _logger = logger; |
| | | 38 | | } |
| | | 39 | | |
| | | 40 | | /// <summary> |
| | | 41 | | /// Возвращает уже существующую накладную или null. Сбой поиска дубль не подтверждает: документ создаётся как раньше |
| | | 42 | | /// иначе недоступный запрос отбора остановил бы весь приход, хотя сам POST мог бы пройти. |
| | | 43 | | /// </summary> |
| | | 44 | | public async Task<ExistingReceipt?> FindAsync( |
| | | 45 | | string? incomingNumber, |
| | | 46 | | Guid counterpartyKey, |
| | | 47 | | Guid warehouseKey, |
| | | 48 | | IReadOnlyCollection<ODataReceiptLine> lines) |
| | | 49 | | { |
| | | 50 | | var number = incomingNumber?.Trim(); |
| | | 51 | | if (string.IsNullOrEmpty(number) || lines.Count == 0) |
| | | 52 | | return null; |
| | | 53 | | |
| | | 54 | | List<ReceiptCandidate> candidates; |
| | | 55 | | try |
| | | 56 | | { |
| | | 57 | | candidates = await _client.GetSinglePageAsync<ReceiptCandidate>(ReceiptEndpoint, ProbeTop, new Dictionary<st |
| | | 58 | | { |
| | | 59 | | ["$filter"] = $"substringof('{number.Replace("'", "''")}', НомерВходящегоДокумента) and DeletionMark eq |
| | | 60 | | ["$select"] = Select, |
| | | 61 | | ["$orderby"] = "Date desc" |
| | | 62 | | }); |
| | | 63 | | } |
| | | 64 | | catch (Exception ex) |
| | | 65 | | { |
| | | 66 | | _logger.LogWarning(ex, "[OData] Не удалось проверить дубль приходной по входящему номеру {Number}; документ |
| | | 67 | | return null; |
| | | 68 | | } |
| | | 69 | | |
| | | 70 | | var duplicate = candidates.FirstOrDefault(c => IsDuplicate(c, number, counterpartyKey, warehouseKey, lines)); |
| | | 71 | | return duplicate == null ? null : new ExistingReceipt(duplicate.RefKey, duplicate.Number, duplicate.Posted); |
| | | 72 | | } |
| | | 73 | | |
| | | 74 | | /// <summary> |
| | | 75 | | /// Дубль — тот же входящий номер (точно, без регистра), поставщик и склад, и каждая новая строка уже есть в докумен |
| | | 76 | | /// с тем же количеством. Если 1С не вернула строки, совпадение шапки считается дублем. |
| | | 77 | | /// </summary> |
| | | 78 | | public static bool IsDuplicate( |
| | | 79 | | ReceiptCandidate candidate, |
| | | 80 | | string incomingNumber, |
| | | 81 | | Guid counterpartyKey, |
| | | 82 | | Guid warehouseKey, |
| | | 83 | | IReadOnlyCollection<ODataReceiptLine> newLines) |
| | | 84 | | { |
| | | 85 | | if (candidate.DeletionMark |
| | | 86 | | || candidate.CounterpartyKey != counterpartyKey |
| | | 87 | | || candidate.WarehouseKey != warehouseKey |
| | | 88 | | || !string.Equals(candidate.IncomingDocNumber?.Trim(), incomingNumber.Trim(), StringComparison.OrdinalIgnore |
| | | 89 | | return false; |
| | | 90 | | |
| | | 91 | | if (candidate.Lines.Count == 0) |
| | | 92 | | return true; |
| | | 93 | | |
| | | 94 | | var unused = candidate.Lines.ToList(); |
| | | 95 | | foreach (var line in newLines) |
| | | 96 | | { |
| | | 97 | | var index = unused.FindIndex(x => x.NomenclatureKey == line.NomenclatureKey && x.Quantity == line.Quantity); |
| | | 98 | | if (index < 0) |
| | | 99 | | return false; |
| | | 100 | | unused.RemoveAt(index); |
| | | 101 | | } |
| | | 102 | | |
| | | 103 | | return true; |
| | | 104 | | } |
| | | 105 | | |
| | | 106 | | public sealed class ReceiptCandidate |
| | | 107 | | { |
| | | 108 | | [JsonProperty("Ref_Key")] public Guid RefKey { get; set; } |
| | | 109 | | [JsonProperty("Number")] public string? Number { get; set; } |
| | | 110 | | [JsonProperty("Posted")] public bool Posted { get; set; } |
| | | 111 | | [JsonProperty("DeletionMark")] public bool DeletionMark { get; set; } |
| | | 112 | | [JsonProperty("НомерВходящегоДокумента")] public string? IncomingDocNumber { get; set; } |
| | | 113 | | [JsonProperty("Контрагент_Key")] public Guid CounterpartyKey { get; set; } |
| | | 114 | | [JsonProperty("СтруктурнаяЕдиница_Key")] public Guid WarehouseKey { get; set; } |
| | | 115 | | [JsonProperty("Запасы")] public List<ReceiptCandidateLine> Lines { get; set; } = []; |
| | | 116 | | } |
| | | 117 | | |
| | | 118 | | public sealed class ReceiptCandidateLine |
| | | 119 | | { |
| | | 120 | | [JsonProperty("Номенклатура_Key")] public Guid NomenclatureKey { get; set; } |
| | | 121 | | [JsonProperty("Количество")] public decimal Quantity { get; set; } |
| | | 122 | | } |
| | | 123 | | } |