< Summary

Information
Class: OzonOrdersWeb.WebServices.Transactions.Grid.Commit.Steps.OneCDocumentStep
Assembly: OzonOrdersWeb
File(s): /home/runner/work/Studio2Prod/Studio2Prod/OzonOrdersWeb/WebServices/Transactions/Grid/Commit/Steps/OneCDocumentStep.cs
Line coverage
0%
Covered lines: 0
Uncovered lines: 211
Coverable lines: 211
Total lines: 427
Line coverage: 0%
Branch coverage
0%
Covered branches: 0
Total branches: 156
Branch coverage: 0%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%210%
get_Key()100%210%
ExecuteAsync()0%420200%
TransferAsync()0%3660600%
SenderOf()0%620%
RecipientOf()0%620%
TransferByWarehousesAsync()0%4260%
TransferReversedAsync()0%110100%
PostWithDraftsAsync()0%2040%
ReceiptAsync()0%110100%
ExpenseAsync()0%620%
RestoreStatusesAsync()0%7280%
AppendSummary(...)0%7280%
AppendCommentDetails(...)0%4260%
GroupsHtml(...)0%342180%

File(s)

/home/runner/work/Studio2Prod/Studio2Prod/OzonOrdersWeb/WebServices/Transactions/Grid/Commit/Steps/OneCDocumentStep.cs

#LineLine coverage
 1using System.Net;
 2using OzonDomains;
 3using OzonDomains.Models;
 4using OzonOrdersWeb.Areas.Studio2.Controllers.Base;
 5using OzonOrdersWeb.Areas.Studio2.Helpers;
 6using OzonOrdersWeb.Areas.Studio2.Models.Transactions;
 7using OzonOrdersWeb.WebServices.Transactions.Grid.Definitions;
 8using Services.ApiServices._1CApi;
 9using Services.ApiServices._1CApi.DTO;
 10using Services.ApiServices._1CApi.Models;
 11using Services.BackgroundJobsServices;
 12using Services.DataServices;
 13
 14namespace OzonOrdersWeb.WebServices.Transactions.Grid.Commit.Steps;
 15
 16/// <summary>
 17/// Документ 1С по <see cref="TransactionDefinition.OneCDocument"/>:
 18/// перемещение (склады на документ / на заказ / по маппингу засыла / «перевёрнутое»), приходная, расходная накладная.
 19/// Дефицитные позиции (после OneCStockCheckStep) — отдельным черновиком. Поведение при ошибке повторяет классические
 20/// контроллеры: перемещение/расходная откатывают статусы, приходная — нет (заказ отгружен, документа просто нет).
 21/// </summary>
 22public sealed class OneCDocumentStep : ITransactionCommitStep
 23{
 24    private const string DraftReason = "Недостаточно остатка в 1С. Проведите после оприходования товара.";
 25
 26    private readonly OrdersDataServices _orders;
 27    private readonly OneCTransferManager? _transfer;
 28    private readonly OneCReceiptManager? _receipt;
 29    private readonly OneCExpenseManager? _expense;
 30    private readonly WarehouseMappingDataServices? _mappings;
 31    private readonly WarehouseSelectionResolver? _warehouseResolver;
 32    private readonly ILogger<OneCDocumentStep> _logger;
 33
 034    public OneCDocumentStep(
 035        OrdersDataServices orders,
 036        ILogger<OneCDocumentStep> logger,
 037        OneCTransferManager? transfer = null,
 038        OneCReceiptManager? receipt = null,
 039        OneCExpenseManager? expense = null,
 040        WarehouseMappingDataServices? mappings = null,
 041        WarehouseSelectionResolver? warehouseResolver = null)
 42    {
 043        _orders = orders;
 044        _logger = logger;
 045        _transfer = transfer;
 046        _receipt = receipt;
 047        _expense = expense;
 048        _mappings = mappings;
 049        _warehouseResolver = warehouseResolver;
 050    }
 51
 052    public string Key => TransactionCommitStepKeys.OneCDocument;
 53
 54    public async Task ExecuteAsync(TransactionCommitContext ctx)
 55    {
 056        var d = ctx.Definition;
 057        if (!ctx.ProcessIn1C || ctx.OneCHandledExternally || d.OneCDocument == OneCDocumentKind.None)
 58        {
 059            return;
 60        }
 61
 062        var forOneC = ctx.OrdersToTransaction
 63            .Where(o => !ctx.StatusOnlyOrderIds.Contains(o.Id) && (o.Quantity ?? 0) > 0)
 064            .ToList();
 065        if (forOneC.Count == 0)
 66        {
 067            if (ctx.OrdersToTransaction.Count > 0)
 68            {
 069                ctx.SummaryBlocks.Add("<div class='alert alert-info mb-0'><strong>Документ 1С не создавался</strong><br/
 70            }
 71
 072            return;
 73        }
 74
 075        await ctx.StageAsync("onec", "Создаём документы в 1С", forOneC.Count == 1 ? "1 заказ" : $"заказов: {forOneC.Coun
 76        var originalStatuses = forOneC.ToDictionary(o => o.Id, o => ctx.FromStatusByOrderId.TryGetValue(o.Id, out var s)
 77
 78        try
 79        {
 080            switch (d.OneCDocument)
 81            {
 82                case OneCDocumentKind.Transfer:
 083                    await TransferAsync(ctx, forOneC);
 084                    break;
 85                case OneCDocumentKind.TransferReversed:
 086                    await TransferReversedAsync(ctx, forOneC);
 087                    break;
 88                case OneCDocumentKind.TransferByWarehouses:
 089                    await TransferByWarehousesAsync(ctx, forOneC);
 090                    break;
 91                case OneCDocumentKind.Receipt:
 092                    await ReceiptAsync(ctx, forOneC);
 093                    return; // приход не откатывает статусы и не прерывает проведение
 94                case OneCDocumentKind.Expense:
 095                    await ExpenseAsync(ctx, forOneC);
 96                    break;
 97            }
 098        }
 099        catch (TransactionException)
 100        {
 0101            throw;
 102        }
 0103        catch (Exception ex)
 104        {
 0105            _logger.LogError(ex, "TransactionsGrid {Slug}: ошибка 1С", d.Slug);
 0106            await RestoreStatusesAsync(ctx, forOneC, originalStatuses);
 0107            throw new TransactionException(TransactionErrorCode.OneCIntegrationFailed, $"Ошибка обработки в 1С: {ex.Mess
 108        }
 109
 110        var failed = ctx.OneCResponses.Where(r => !r.Success).ToList();
 0111        if (failed.Count > 0)
 112        {
 0113            await RestoreStatusesAsync(ctx, forOneC, originalStatuses);
 114            var messages = string.Join("; ", failed.Select(r => r.Message).Distinct().Take(3).Select(m => WebUtility.Htm
 0115            throw new TransactionException(TransactionErrorCode.OneCIntegrationFailed,
 0116                $"1С: успешно {ctx.OneCResponses.Count - failed.Count} из {ctx.OneCResponses.Count} документов, с ошибка
 117        }
 118
 0119        AppendSummary(ctx, forOneC);
 0120    }
 121
 122    // ---------- Перемещение: склады на документ или на заказ ----------
 123
 124    private async Task TransferAsync(TransactionCommitContext ctx, List<Order> forOneC)
 125    {
 0126        var manager = _transfer ?? throw new TransactionException(TransactionErrorCode.OneCUnavailable, "Перемещения 1С 
 0127        var d = ctx.Definition;
 0128        var doc = ctx.Request.Document;
 0129        var senderByOrder = OneCStockCheckStep.SenderByOrder(ctx);
 0130        var recipientByOrder = OneCStockCheckStep.RecipientByOrder(ctx);
 0131        var perOrderSender = d.WarehouseMode == WarehouseMode.PerOrderBoth;
 0132        var perOrderRecipient = d.WarehouseMode is WarehouseMode.PerOrderBoth or WarehouseMode.PerOrderRecipient;
 133
 0134        var globalSender = doc.SenderWarehouse?.Trim();
 0135        var globalRecipient = doc.RecipientWarehouse?.Trim();
 136
 0137        if (d.WarehouseMode == WarehouseMode.GlobalPair && !ctx.User.IsAdmin && _warehouseResolver != null)
 138        {
 139            // Не-администратору склад-отправитель определяется автоматически (как в WarehouseTransfer).
 0140            var resolved = await _warehouseResolver.ResolveSenderWarehouseAsync(forOneC);
 0141            if (resolved.HasErrors || string.IsNullOrWhiteSpace(resolved.SenderWarehouseName))
 142            {
 0143                throw new TransactionException(TransactionErrorCode.OneCSenderWarehouseMissing, string.Join(" ", resolve
 144            }
 145
 0146            globalSender = resolved.SenderWarehouseName;
 147        }
 148
 0149        if (string.IsNullOrWhiteSpace(globalSender) && !perOrderSender)
 150        {
 0151            throw new TransactionException(TransactionErrorCode.OneCSenderWarehouseMissing, "Не указан склад отправителя
 152        }
 153
 0154        if (string.IsNullOrWhiteSpace(globalRecipient) && !perOrderRecipient)
 155        {
 0156            throw new TransactionException(TransactionErrorCode.OneCRecipientWarehouseMissing, "Не указан склад получате
 157        }
 158
 0159        if (!perOrderSender && !perOrderRecipient && string.Equals(globalSender, globalRecipient, StringComparison.Ordin
 160        {
 0161            throw new TransactionException(TransactionErrorCode.OneCWarehousesMustDiffer, "Склад отправителя и получател
 162        }
 163
 0164        if (perOrderRecipient)
 165        {
 166            var withoutRecipient = forOneC.Where(o => !recipientByOrder.ContainsKey(o.Id)).Select(o => o.ShipmentNumber)
 0167            if (withoutRecipient.Count > 0)
 168            {
 0169                throw new TransactionException(TransactionErrorCode.OneCRecipientWarehouseMissing, $"Не удалось определи
 170            }
 171        }
 172
 0173        if (perOrderSender)
 174        {
 175            var withoutSender = forOneC.Where(o => !senderByOrder.ContainsKey(o.Id)).Select(o => o.ShipmentNumber).ToLis
 0176            if (withoutSender.Count > 0)
 177            {
 0178                throw new TransactionException(TransactionErrorCode.OneCSenderWarehouseMissing, $"Не удалось определить 
 179            }
 180        }
 181
 0182        string SenderOf(Order o) => perOrderSender ? senderByOrder[o.Id] : globalSender!;
 0183        string RecipientOf(Order o) => perOrderRecipient ? recipientByOrder[o.Id] : globalRecipient!;
 184
 0185        var groups = forOneC.GroupBy(o => SenderOf(o).ToUpperInvariant() + "" + RecipientOf(o).ToUpperInvariant()).ToLi
 0186        foreach (var group in groups)
 187        {
 0188            var first = group.First();
 0189            var sender = SenderOf(first);
 0190            var recipient = RecipientOf(first);
 0191            if (string.Equals(sender, recipient, StringComparison.OrdinalIgnoreCase))
 192            {
 0193                throw new TransactionException(TransactionErrorCode.OneCWarehousesMustDiffer, $"Склад отправителя и полу
 194            }
 195
 0196            var groupOrders = group.ToList();
 0197            var result = new List<OneCResponse>();
 0198            var toPost = groupOrders.Where(o => !ctx.StockBlockedOrderIds.Contains(o.Id)).ToList();
 0199            var toDraft = groupOrders.Where(o => ctx.StockBlockedOrderIds.Contains(o.Id)).ToList();
 0200            if (toPost.Count > 0)
 201            {
 0202                result.AddRange(await manager.TransferStockWithCustomWarehouses(toPost, sender, recipient, d.Type, doc.O
 203            }
 204
 0205            if (toDraft.Count > 0)
 206            {
 0207                result.AddRange(await manager.TransferStockWithCustomWarehouses(toDraft, sender, recipient, d.Type, doc.
 208            }
 209
 0210            ctx.OneCGroups.Add((sender, recipient, groupOrders, result));
 0211            ctx.OneCResponses.AddRange(result);
 0212        }
 213
 0214        if (groups.Count == 1)
 215        {
 216            // Одно направление — история и комментарий как у простого перемещения.
 0217            ctx.OneCSender = ctx.OneCGroups[0].Sender;
 0218            ctx.OneCRecipient = ctx.OneCGroups[0].Recipient;
 0219            if (!perOrderSender && !perOrderRecipient)
 220            {
 0221                ctx.OneCGroups.Clear();
 222            }
 223        }
 224        else
 225        {
 226            ctx.OneCSender = string.Join(", ", ctx.OneCGroups.Select(g => g.Sender).Distinct(StringComparer.OrdinalIgnor
 227            ctx.OneCRecipient = string.Join(", ", ctx.OneCGroups.Select(g => g.Recipient).Distinct(StringComparer.Ordina
 228        }
 0229    }
 230
 231    // ---------- Перемещение по маппингу (ShippedToSeller) ----------
 232
 233    private async Task TransferByWarehousesAsync(TransactionCommitContext ctx, List<Order> forOneC)
 234    {
 0235        var manager = _transfer ?? throw new TransactionException(TransactionErrorCode.OneCUnavailable, "Перемещения 1С 
 0236        var mappings = _mappings != null ? await _mappings.GetWarehouseMappings() : null;
 237        var zasylRaw = OneCStockCheckStep.ZasylByOrder(ctx).ToDictionary(kv => kv.Key, kv => (string?)kv.Value);
 0238        var senderMap = ShippedToSellerZasylResolver.BuildSenderMap(forOneC, mappings, zasylRaw);
 0239        var map = senderMap.Count > 0 ? new Dictionary<int, string>(senderMap) : null;
 240
 0241        await PostWithDraftsAsync(ctx, forOneC, (orders, draft) => manager.TransferStockByWarehouses(
 0242            orders, ctx.Definition.Type, map, ctx.Request.Document.OperationDate, ctx.User.Name, ctx.Request.OneCOrganiz
 0243    }
 244
 245    // ---------- Перемещение с «перевёрнутыми» складами (ShippedToCommissioner, ReturnToWarehouse) ----------
 246
 247    private async Task TransferReversedAsync(TransactionCommitContext ctx, List<Order> forOneC)
 248    {
 0249        var manager = _transfer ?? throw new TransactionException(TransactionErrorCode.OneCUnavailable, "Перемещения 1С 
 0250        var map = OneCStockCheckStep.ZasylByOrder(ctx);
 0251        if (ctx.Definition.Type == TransactionType.ShippedToCommissioner && _mappings != null)
 252        {
 0253            var mappings = await _mappings.GetWarehouseMappings();
 0254            foreach (var o in forOneC)
 255            {
 0256                var sender = ShippedToCommissionerWarehouseResolver.ResolveSenderWarehouse(o, mappings);
 0257                if (!string.IsNullOrWhiteSpace(sender))
 258                {
 0259                    map[o.Id] = sender;
 260                }
 261            }
 262        }
 263
 0264        await PostWithDraftsAsync(ctx, forOneC, (orders, draft) => manager.TransferStockByWarehousesWithReversedWarehous
 0265            orders, ctx.Definition.Type, map, ctx.Request.Document.OperationDate, ctx.User.Name, ctx.Request.OneCOrganiz
 0266    }
 267
 268    private static async Task PostWithDraftsAsync(TransactionCommitContext ctx, List<Order> forOneC, Func<List<Order>, b
 269    {
 0270        var toPost = forOneC.Where(o => !ctx.StockBlockedOrderIds.Contains(o.Id)).ToList();
 0271        var toDraft = forOneC.Where(o => ctx.StockBlockedOrderIds.Contains(o.Id)).ToList();
 0272        if (toPost.Count > 0)
 273        {
 0274            ctx.OneCResponses.AddRange(await post(toPost, false));
 275        }
 276
 0277        if (toDraft.Count > 0)
 278        {
 0279            ctx.OneCResponses.AddRange(await post(toDraft, true));
 280        }
 0281    }
 282
 283    // ---------- Приходная накладная ----------
 284
 285    private async Task ReceiptAsync(TransactionCommitContext ctx, List<Order> forOneC)
 286    {
 0287        var manager = _receipt ?? throw new TransactionException(TransactionErrorCode.OneCUnavailable, "Приходные наклад
 0288        var doc = ctx.Request.Document;
 0289        var dto = new ShippedBySupplierTransactionDto
 0290        {
 0291            NDS = doc.Nds ? "true" : "false",
 0292            ContractLink = doc.ContractLink,
 0293            NumberVh = doc.NumberVh,
 0294            DateVh = doc.DateVh ?? DateTime.Now,
 0295            TransactionComment = doc.TransactionComment,
 0296            VatRate = doc.VatRate,
 297            OrderCountryMap = ctx.Edits.Values.Where(e => !string.IsNullOrWhiteSpace(e.Country)).ToDictionary(e => e.Id,
 298            OrderGtdMap = ctx.Edits.Values.Where(e => !string.IsNullOrWhiteSpace(e.Gtd)).ToDictionary(e => e.Id, e => e.
 0299        };
 300
 301        try
 302        {
 0303            ctx.OneCResponses.AddRange(await manager.CreateReceipts(forOneC, dto, ctx.User.Name, orgInnOverride: ctx.Req
 0304        }
 0305        catch (Exception ex)
 306        {
 307            // Как в классическом контроллере: статус не откатываем, заказ отгружен — просто нет документа.
 0308            ctx.OneCResponses.Add(new OneCResponse { Success = false, Message = $"Исключение при создании документа 1С: 
 0309        }
 310
 311        var failed = ctx.OneCResponses.Where(r => !r.Success).ToList();
 0312        if (failed.Count > 0)
 313        {
 314            var lines = failed.Select(r => r.Message).Distinct().Select(m => $"<li class='small'>{WebUtility.HtmlEncode(
 0315            ctx.SummaryBlocks.Add($"<div class='alert alert-warning'><strong>⚠️ 1С: документ не создан ({failed.Count} о
 0316            ctx.Warnings.Add("Приходная накладная в 1С не создана — см. детали в результате.");
 317        }
 318
 319        if (ctx.OneCResponses.Any(r => r.Success))
 320        {
 0321            ctx.SummaryBlocks.Add(OneCTransactionSummaryHelper.BuildAlertSummary(forOneC, ctx.OneCResponses));
 322        }
 323
 0324        AppendCommentDetails(ctx, forOneC, null, null);
 0325    }
 326
 327    // ---------- Расходная накладная ----------
 328
 329    private async Task ExpenseAsync(TransactionCommitContext ctx, List<Order> forOneC)
 330    {
 0331        var manager = _expense ?? throw new TransactionException(TransactionErrorCode.OneCUnavailable, "Расходные наклад
 0332        var doc = ctx.Request.Document;
 0333        await PostWithDraftsAsync(ctx, forOneC, (orders, draft) => manager.SaleStockByWarehouses(
 0334            orders, ctx.Definition.Type, doc.ContractLink, doc.TransactionComment, doc.VatRate, doc.OperationDate, ctx.U
 0335            ctx.Request.OneCOrganizationInn, draft, draft ? DraftReason : null));
 0336    }
 337
 338    // ---------- Общее ----------
 339
 340    private async Task RestoreStatusesAsync(TransactionCommitContext ctx, List<Order> orders, Dictionary<int, int> origi
 341    {
 342        try
 343        {
 0344            var statuses = ctx.Resolve<AppStatusDataServices>();
 0345            foreach (var o in orders)
 346            {
 0347                if (originalStatuses.TryGetValue(o.Id, out var id) && id > 0)
 348                {
 0349                    var st = await statuses.GetAppStatusAsync(id);
 0350                    if (st != null)
 351                    {
 0352                        o.AppStatus = st;
 0353                        o.AppStatusId = st.Id;
 354                    }
 355                }
 0356            }
 357
 0358            await _orders.UpdateOrders(orders);
 0359            ctx.Warnings.Add($"Исходный статус восстановлен для {orders.Count} заказов.");
 0360        }
 0361        catch (Exception ex)
 362        {
 0363            _logger.LogError(ex, "TransactionsGrid: не удалось откатить статусы после ошибки 1С");
 0364        }
 0365    }
 366
 367    private void AppendSummary(TransactionCommitContext ctx, List<Order> forOneC)
 368    {
 0369        var posted = ctx.OneCResponses.Where(r => r.Success && !r.IsDraft).ToList();
 0370        var drafts = ctx.OneCResponses.Where(r => r.IsDraft).ToList();
 0371        var html = ctx.OneCGroups.Count > 0
 0372            ? OneCTransactionSummaryHelper.BuildAlertSummary(forOneC, ctx.OneCResponses, ctx.OneCSender, ctx.OneCRecipie
 0373            : OneCTransactionSummaryHelper.WrapSection("Проведённые документы 1С", OneCTransactionSummaryHelper.BuildAle
 0374              + (drafts.Count > 0 ? OneCTransactionSummaryHelper.WrapSection("Черновики 1С по дефицитным позициям", OneC
 375
 0376        if (ctx.StockCheck != null)
 377        {
 0378            html += OneCStockGate.PartialPostingNotice(ctx.StockCheck, forOneC.Count - forOneC.Count(o => ctx.StockBlock
 0379                  + OneCStockGate.SkippedNotice(ctx.StockCheck);
 380        }
 381
 0382        ctx.SummaryBlocks.Add(html);
 0383        AppendCommentDetails(ctx, forOneC, ctx.OneCSender, ctx.OneCRecipient);
 0384    }
 385
 386    private static void AppendCommentDetails(TransactionCommitContext ctx, List<Order> forOneC, string? sender, string? 
 387    {
 0388        if (ctx.OneCResponses.Count == 0)
 389        {
 0390            return;
 391        }
 392
 0393        var flow = ctx.CommentFlow;
 0394        if (flow.ScheduleAiComment && flow.Builder != null)
 395        {
 0396            flow.Builder.WithOneCResults(ctx.OneCResponses, sender, recipient);
 0397            return;
 398        }
 399
 0400        ctx.TechnicalComment += OneCTransactionSummaryHelper.BuildCommentDetails(forOneC, ctx.OneCResponses, sender, rec
 0401    }
 402
 403    private static string GroupsHtml(TransactionCommitContext ctx)
 404    {
 0405        var lines = ctx.OneCGroups.Select(g =>
 0406        {
 0407            var route = WebUtility.HtmlEncode($"{g.Sender} → {g.Recipient}");
 0408            var numbers = string.Join(", ", g.Orders.Select(o => WebUtility.HtmlEncode(o.ShipmentNumber ?? string.Empty)
 0409            var docs = g.Result.Where(r => r.Success).Select(r =>
 0410            {
 0411                var label = WebUtility.HtmlEncode(string.IsNullOrWhiteSpace(r.DocumentNumber) ? "Документ 1С" : $"№{r.Do
 0412                var link = !string.IsNullOrWhiteSpace(r.TransactionId) && Guid.TryParse(r.TransactionId, out _)
 0413                    ? $"<a href='/Studio2/OneCDocument/Details?refKey={WebUtility.HtmlEncode(r.TransactionId)}' target='
 0414                    : label;
 0415                var status = r.IsDraft ? "<span class='badge bg-warning-subtle text-warning'>черновик</span>"
 0416                    : r.IsPosted ? "<span class='badge bg-success-subtle text-success'>проведён</span>"
 0417                    : "<span class='badge bg-warning-subtle text-warning'>не проведён</span>";
 0418                var reason = r.IsDraft ? " — недостаточно остатка, проведите после оприходования"
 0419                    : !string.IsNullOrWhiteSpace(r.PostingError) ? $" — {WebUtility.HtmlEncode(Services.ApiServices._1CA
 0420                return $"<li>{status} {link}{reason}</li>";
 0421            }).ToList();
 0422            return $"<li class='mb-2'><strong>{route}</strong>: {g.Orders.Count} заказ(ов) ({numbers})" +
 0423                   (docs.Count > 0 ? $"<ul class='mb-0 mt-1'>{string.Join(string.Empty, docs)}</ul>" : string.Empty) + "
 0424        });
 0425        return $"<div class='alert alert-info mt-2'><strong>Что создано в 1С по направлениям</strong><ul class='mb-0'>{s
 426    }
 427}