< Summary

Information
Class: OzonOrdersWeb.WebServices.Transactions.InvoiceNumberSuggestion
Assembly: OzonOrdersWeb
File(s): /home/runner/work/Studio2Prod/Studio2Prod/OzonOrdersWeb/WebServices/Transactions/ArmtekInvoiceNumberSuggestService.cs
Line coverage
100%
Covered lines: 1
Uncovered lines: 0
Coverable lines: 1
Total lines: 161
Line coverage: 100%
Branch coverage
N/A
Covered branches: 0
Total branches: 0
Branch coverage: N/A
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
get_Number()100%11100%

File(s)

/home/runner/work/Studio2Prod/Studio2Prod/OzonOrdersWeb/WebServices/Transactions/ArmtekInvoiceNumberSuggestService.cs

#LineLine coverage
 1using System.Linq.Expressions;
 2using Microsoft.EntityFrameworkCore;
 3using OzonDomains.Models.ArmtekEtp;
 4using OzonOrdersWeb.WebServices.Grid;
 5using OzonRepositories.Context;
 6
 7namespace OzonOrdersWeb.WebServices.Transactions;
 8
 9/// <param name="Number">Значение для поля «Номер вх. документа».</param>
 10/// <param name="Kind">Что это за номер: фактура, УПД или ТТН.</param>
 11/// <param name="Matched">Сколько артикулов заказов найдено в этой фактуре.</param>
 12/// <param name="Total">Сколько разных артикулов в выбранных заказах.</param>
 1713public sealed record InvoiceNumberSuggestion(string Number, string Kind, DateTime? InvoiceDate, int Matched, int Total, 
 14
 15/// <param name="Country">Страна в написании справочника 1С; null — в 1С такой страны нет (причина в <paramref name="Err
 16public sealed record OrderCountrySuggestion(int OrderId, string Article, string? SourceCountry, string? Country, string?
 17
 18/// <summary>
 19/// Подсказки для поля «Номер вх. документа» формы прихода: по набору артикулов выбранных заказов ищет позиции фактур
 20/// Армтека (артикул и код Армтек сравниваются без разделителей: 6025-2309-SX = 60252309_SX = 60252309SX) и предлагает
 21/// номера найденных фактур, начиная с тех, что покрывают больше артикулов.
 22/// </summary>
 23public sealed class ArmtekInvoiceNumberSuggestService(ApplicationDbContext db)
 24{
 25    private const int MaxInvoices = 8;
 26    private const int MaxItems = 3000;
 27
 28    public sealed record FoundItem(long InvoiceId, string? Article, string? ArmtekCode, string Vbeln, string? UpdNumber,
 29        string? TtnNumber, DateTime? InvoiceDate, string? Brand = null, string? Country = null);
 30
 31    public sealed record OrderRef(int Id, string? Article, string? ProductKey, string? Brand);
 32
 33    public async Task<IReadOnlyList<InvoiceNumberSuggestion>> SuggestAsync(int[] orderIds, CancellationToken ct)
 34    {
 35        var orders = await LoadOrdersAsync(orderIds, ct);
 36        var keys = OrderArticleKeys(orders.Select(o => (o.Article, o.ProductKey)));
 37        if (keys.Count == 0) return [];
 38        return Build(await FindItemsAsync(keys, ct), keys);
 39    }
 40
 41    /// <summary>
 42    /// Страна происхождения по артикулу заказа из позиций фактур Армтека, сразу в написании справочника 1С:
 43    /// «(CN, 156) Китай» → «КИТАЙ». Заказы, по которым в фактурах ничего нет, в ответ не попадают.
 44    /// </summary>
 45    public async Task<IReadOnlyList<OrderCountrySuggestion>> SuggestCountriesAsync(
 46        int[] orderIds, IReadOnlyCollection<string> oneCCountries, CancellationToken ct)
 47    {
 48        var orders = await LoadOrdersAsync(orderIds, ct);
 49        var keys = OrderArticleKeys(orders.Select(o => (o.Article, o.ProductKey)));
 50        if (keys.Count == 0) return [];
 51        return PickCountries(orders, await FindItemsAsync(keys, ct), oneCCountries);
 52    }
 53
 54    private async Task<List<OrderRef>> LoadOrdersAsync(int[] orderIds, CancellationToken ct)
 55        => await db.Orders.AsNoTracking().Where(o => orderIds.Contains(o.Id))
 56            .Select(o => new OrderRef(o.Id, o.Article, o.ProductKey, o.EtProducer != null ? o.EtProducer.Name : null))
 57            .ToListAsync(ct);
 58
 59    private async Task<List<FoundItem>> FindItemsAsync(List<string> keys, CancellationToken ct)
 60    {
 61        Expression<Func<ArmtekInvoiceItem, string?>> article = x => x.Article;
 62        Expression<Func<ArmtekInvoiceItem, string?>> code = x => x.ArmtekCode;
 63        var parameter = Expression.Parameter(typeof(ArmtekInvoiceItem), "x");
 64        var predicate = Expression.Lambda<Func<ArmtekInvoiceItem, bool>>(
 65            Expression.OrElse(KeyIn(keys, article, parameter), KeyIn(keys, code, parameter)), parameter);
 66
 67        return await db.ArmtekInvoiceItems.AsNoTracking().Where(predicate)
 68            .OrderByDescending(x => x.Invoice!.InvoiceDate).Take(MaxItems)
 69            .Select(x => new FoundItem(x.InvoiceId, x.Article, x.ArmtekCode, x.Invoice!.Vbeln, x.Invoice.UpdNumber,
 70                x.Invoice.TtnNumber, x.Invoice.InvoiceDate, x.Brand, x.Country))
 71            .ToListAsync(ct);
 72    }
 73
 74    /// <summary>
 75    /// Для каждого заказа берётся самая свежая позиция фактуры с тем же артикулом (сначала с тем же брендом) и страной,
 76    /// название страны сверяется со справочником 1С без учёта регистра и «ё».
 77    /// </summary>
 78    public static IReadOnlyList<OrderCountrySuggestion> PickCountries(
 79        IEnumerable<OrderRef> orders, IReadOnlyCollection<FoundItem> items, IReadOnlyCollection<string> oneCCountries)
 80    {
 81        var result = new List<OrderCountrySuggestion>();
 82        foreach (var order in orders)
 83        {
 84            var key = OrderArticleKeys([(order.Article, order.ProductKey)]).FirstOrDefault();
 85            if (key == null) continue;
 86            var brand = ServerGridQuery.ArticleKey(order.Brand);
 87            var pick = items
 88                .Where(i => (ServerGridQuery.ArticleKey(i.Article) == key || ServerGridQuery.ArticleKey(i.ArmtekCode) ==
 89                            && HasCountry(i.Country))
 90                .OrderByDescending(i => brand.Length > 0 && ServerGridQuery.ArticleKey(i.Brand) == brand)
 91                .ThenByDescending(i => i.InvoiceDate)
 92                .FirstOrDefault();
 93            if (pick == null) continue;
 94
 95            var name = ArmtekReceiptOriginService.CountryName(pick.Country);
 96            var canonical = oneCCountries.FirstOrDefault(c => CountryKey(c) == CountryKey(name));
 97            result.Add(new OrderCountrySuggestion(order.Id, order.Article ?? string.Empty, pick.Country, canonical,
 98                canonical == null ? $"Страна «{name}» не найдена в справочнике 1С" : null));
 99        }
 100
 101        return result;
 102    }
 103
 104    /// <summary>Страна в фактуре указана: «(CN, 156) Китай» — да, «(JP, 392)» без названия и «- -» — нет.</summary>
 105    private static bool HasCountry(string? value) => ArmtekReceiptOriginService.CountryName(value).Any(char.IsLetter);
 106
 107    private static string CountryKey(string? value)
 108        => ServerGridQuery.ArticleKey(value?.Replace('ё', 'е').Replace('Ё', 'Е'));
 109
 110    /// <summary>Уникальные ключи артикулов заказов; без артикула берётся часть ключа товара до «=».</summary>
 111    public static List<string> OrderArticleKeys(IEnumerable<(string? Article, string? ProductKey)> orders)
 112        => orders.Select(o => ServerGridQuery.ArticleKey(!string.IsNullOrWhiteSpace(o.Article) ? o.Article : ProductKeyA
 113            .Where(k => k.Length > 0).Distinct().ToList();
 114
 115    private static string? ProductKeyArticle(string? productKey)
 116        => productKey?.Split('=')[0];
 117
 118    /// <summary>Фактуры по числу покрытых артикулов (по убыванию), затем по дате; номера: фактура, УПД, ТТН.</summary>
 119    public static IReadOnlyList<InvoiceNumberSuggestion> Build(IEnumerable<FoundItem> items, IReadOnlyCollection<string>
 120    {
 121        var result = new List<InvoiceNumberSuggestion>();
 122        var invoices = items.GroupBy(i => i.InvoiceId)
 123            .Select(g => new
 124            {
 125                First = g.First(),
 126                Matched = keys.Count(k => g.Any(i => ServerGridQuery.ArticleKey(i.Article) == k || ServerGridQuery.Artic
 127            })
 128            .Where(x => x.Matched > 0)
 129            .OrderByDescending(x => x.Matched).ThenByDescending(x => x.First.InvoiceDate)
 130            .Take(MaxInvoices);
 131
 132        var seen = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
 133        foreach (var invoice in invoices)
 134        {
 135            var first = invoice.First;
 136            var coverage = $"артикулов {invoice.Matched} из {keys.Count}";
 137            var date = first.InvoiceDate?.ToString("dd.MM.yyyy");
 138            foreach (var (number, kind) in new[] { (first.Vbeln, "фактура"), (first.UpdNumber, "УПД"), (first.TtnNumber,
 139            {
 140                if (string.IsNullOrWhiteSpace(number) || !seen.Add(number.Trim())) continue;
 141                var label = $"{kind}{(date != null ? $" от {date}" : string.Empty)} · {coverage}";
 142                result.Add(new InvoiceNumberSuggestion(number.Trim(), kind, first.InvoiceDate, invoice.Matched, keys.Cou
 143            }
 144        }
 145
 146        return result;
 147    }
 148
 149    /// <summary><c>keys.Contains(REPLACE(…поле…))</c> — поле сравнивается без разделителей на стороне БД.</summary>
 150    private static Expression KeyIn(List<string> keys, Expression<Func<ArmtekInvoiceItem, string?>> field, ParameterExpr
 151    {
 152        var body = new ParameterReplacer(field.Parameters[0], parameter).Visit(field.Body)!;
 153        return Expression.Call(typeof(Enumerable), nameof(Enumerable.Contains), new[] { typeof(string) },
 154            Expression.Constant(keys), ServerGridQuery.StripSeparatorsExpression(body));
 155    }
 156
 157    private sealed class ParameterReplacer(ParameterExpression from, ParameterExpression to) : ExpressionVisitor
 158    {
 159        protected override Expression VisitParameter(ParameterExpression node) => node == from ? to : base.VisitParamete
 160    }
 161}

Methods/Properties

get_Number()