| | | 1 | | using Microsoft.EntityFrameworkCore; |
| | | 2 | | using OzonDomains.Models; |
| | | 3 | | using OzonDomains.Models.ArmtekEtp; |
| | | 4 | | using OzonRepositories.Context; |
| | | 5 | | using Services.ApiServices._1CApi.OData; |
| | | 6 | | |
| | | 7 | | namespace OzonOrdersWeb.WebServices.Transactions; |
| | | 8 | | |
| | | 9 | | public sealed record ReceiptOriginRow(int OrderId, string Article, string? SourceCountry, |
| | | 10 | | string? Country, string? SourceGtd, string? Gtd, string? Error); |
| | 0 | 11 | | public sealed record ReceiptOriginPreview(bool Applicable, string? Error, IReadOnlyList<ReceiptOriginRow> Rows) |
| | | 12 | | { |
| | 0 | 13 | | public bool Ready => Applicable && Error == null && Rows.Count > 0 && Rows.All(r => r.Error == null); |
| | | 14 | | } |
| | | 15 | | |
| | | 16 | | public sealed class ArmtekReceiptOriginService(ApplicationDbContext db, IOneCODataReferenceService references) |
| | | 17 | | { |
| | | 18 | | /// <summary>Номер ГТД для товара без декларации (страна ЕАЭС, внутренние поставки): в фактуре «- -».</summary> |
| | | 19 | | public const string DefaultGtd = OneCODataConstants.DefaultGtdNumber; |
| | | 20 | | |
| | | 21 | | /// <param name="manualDefaultGtd"> |
| | | 22 | | /// Заказы, где пользователь сам вписал номер по умолчанию: он важнее фактуры и проверки справочника не требует. |
| | | 23 | | /// </param> |
| | | 24 | | public async Task<ReceiptOriginPreview> PreviewAsync(string? number, int[] ids, CancellationToken ct, |
| | | 25 | | IReadOnlySet<int>? manualDefaultGtd = null) |
| | | 26 | | { |
| | | 27 | | var links = await db.ArmtekOrderItems.AsNoTracking().Include(x => x.ArmtekOrder) |
| | | 28 | | .Where(x => ids.Contains(x.StudioOrderId)).ToListAsync(ct); |
| | | 29 | | if (links.Count == 0) return new(false, null, []); |
| | | 30 | | if (string.IsNullOrWhiteSpace(number)) return new(true, "Введите номер фактуры, УПД или ТТН Армтека.", []); |
| | | 31 | | var key = number.Trim(); |
| | | 32 | | var invoices = await db.ArmtekInvoices.AsNoTracking().Include(x => x.Items) |
| | | 33 | | .Where(x => x.Vbeln == key || x.UpdNumber == key || x.TtnNumber == key).ToListAsync(ct); |
| | | 34 | | var rows = new List<ReceiptOriginRow>(); |
| | | 35 | | foreach (var id in ids.Distinct()) |
| | | 36 | | { |
| | | 37 | | var orderLinks = links.Where(x => x.StudioOrderId == id).ToList(); |
| | | 38 | | var matches = invoices.SelectMany(invoice => invoice.Items.Where(item => |
| | | 39 | | orderLinks.Any(link => Matches(link, invoice, item)))).ToList(); |
| | | 40 | | if (matches.Count != 1) |
| | | 41 | | { |
| | | 42 | | rows.Add(new(id, orderLinks.FirstOrDefault()?.Pin ?? id.ToString(), null, null, null, null, |
| | | 43 | | matches.Count == 0 ? "Позиция заказа не найдена в фактуре. Проверьте номер и загрузку фактуры." |
| | | 44 | | : "В фактуре несколько подходящих позиций. Требуется уточнить партию.")); |
| | | 45 | | continue; |
| | | 46 | | } |
| | | 47 | | var item = matches[0]; |
| | | 48 | | var country = CountryName(item.Country); |
| | | 49 | | var declaredGtd = manualDefaultGtd?.Contains(id) == true ? null : DeclarationNumber(item.Gtd); |
| | | 50 | | var gtd = declaredGtd ?? DefaultGtd; |
| | | 51 | | var countryKey = await references.GetCountryGuidByNameAsync(country); |
| | | 52 | | var gtdKey = await references.GetGtdGuidByNumberAsync(gtd); |
| | | 53 | | // «Без ГТД» не требует записи в справочнике: приходная получит пустую ссылку. |
| | | 54 | | var gtdResolved = gtdKey is { } found && found != Guid.Empty || declaredGtd == null; |
| | | 55 | | var errors = new List<string>(); |
| | | 56 | | if (countryKey == null || countryKey == Guid.Empty) |
| | | 57 | | errors.Add($"Страна «{item.Country}» не найдена в справочнике 1С"); |
| | | 58 | | if (!gtdResolved) |
| | | 59 | | errors.Add($"ГТД «{gtd}» не найдена в справочнике 1С"); |
| | | 60 | | rows.Add(new(id, item.Article ?? string.Empty, item.Country, |
| | | 61 | | countryKey.HasValue && countryKey != Guid.Empty ? country : null, item.Gtd, |
| | | 62 | | gtdResolved ? gtd : null, |
| | | 63 | | errors.Count == 0 ? null : string.Join(". ", errors))); |
| | | 64 | | } |
| | | 65 | | return new(true, null, rows); |
| | | 66 | | } |
| | | 67 | | |
| | | 68 | | public static bool Matches(ArmtekOrderItem link, ArmtekInvoice invoice, ArmtekInvoiceItem item) |
| | | 69 | | => link.ArmtekOrder != null |
| | | 70 | | && Digits(link.ArmtekOrder.Kunrg) == Digits(invoice.Kunnr) |
| | | 71 | | && Digits(link.ArmtekOrder.Vbeln).Length > 0 |
| | | 72 | | && Digits(link.ArmtekOrder.Vbeln) == Digits(item.OrderNumber) |
| | | 73 | | && Normalize(link.Pin).Length > 0 && Normalize(link.Pin) == Normalize(item.Article) |
| | | 74 | | && Normalize(link.Brand).Length > 0 && Normalize(link.Brand) == Normalize(item.Brand) |
| | | 75 | | && (string.IsNullOrWhiteSpace(link.Posnr) || string.IsNullOrWhiteSpace(item.OrderPosition) |
| | | 76 | | || Digits(link.Posnr) == Digits(item.OrderPosition)); |
| | | 77 | | |
| | | 78 | | public static string CountryName(string? value) |
| | | 79 | | { |
| | | 80 | | var text = value?.Trim() ?? string.Empty; |
| | | 81 | | // Формат Армтека: (CN, 156) Китай. Без названия код страны не угадываем. |
| | | 82 | | return text.StartsWith('(') && text.IndexOf(')') is var end && end >= 0 |
| | | 83 | | ? text[(end + 1)..].Trim() : text; |
| | | 84 | | } |
| | | 85 | | |
| | | 86 | | public static string? DeclarationNumber(string? value) |
| | | 87 | | => string.IsNullOrWhiteSpace(value) || !value.Any(char.IsDigit) ? null : value.Trim(); |
| | | 88 | | private static string Digits(string? value) => (value ?? string.Empty).Trim().TrimStart('0'); |
| | | 89 | | private static string Normalize(string? value) |
| | | 90 | | => string.Concat((value ?? string.Empty).Where(char.IsLetterOrDigit)).ToUpperInvariant(); |
| | | 91 | | } |