| | | 1 | | using OzonDomains; |
| | | 2 | | using OzonDomains.Models; |
| | | 3 | | using OzonOrdersWeb.Areas.Studio2.Helpers; |
| | | 4 | | using OzonOrdersWeb.WebServices.Transactions.Grid.Definitions; |
| | | 5 | | using OzonOrdersWeb.WebServices.Transactions.Grid.Preview; |
| | | 6 | | using OzonRepositories.Context; |
| | | 7 | | using Services.ApiServices._1CApi.Stock; |
| | | 8 | | using Services.DataServices; |
| | | 9 | | |
| | | 10 | | namespace OzonOrdersWeb.WebServices.Transactions.Grid.Commit.Steps; |
| | | 11 | | |
| | | 12 | | /// <summary> |
| | | 13 | | /// Проверка остатков 1С до смены статусов — теми же построителями строк, что классические страницы |
| | | 14 | | /// (склад по режиму определения). Дефицит: черновики (по умолчанию) либо пропуск 1С с предупреждением. |
| | | 15 | | /// Приходная накладная остатков не требует. |
| | | 16 | | /// </summary> |
| | | 17 | | public sealed class OneCStockCheckStep : ITransactionCommitStep |
| | | 18 | | { |
| | | 19 | | private readonly ApplicationDbContext _context; |
| | | 20 | | private readonly IOneCStockAvailabilityService? _stock; |
| | | 21 | | private readonly WarehouseMappingDataServices? _mappings; |
| | | 22 | | |
| | | 23 | | private readonly ShipToSellerOneCPlanner? _shipToSeller; |
| | | 24 | | private readonly IOneCStockAlertReporter? _stockAlerts; |
| | | 25 | | |
| | 0 | 26 | | public OneCStockCheckStep( |
| | 0 | 27 | | ApplicationDbContext context, |
| | 0 | 28 | | IOneCStockAvailabilityService? stock = null, |
| | 0 | 29 | | WarehouseMappingDataServices? mappings = null, |
| | 0 | 30 | | ShipToSellerOneCPlanner? shipToSeller = null, |
| | 0 | 31 | | IOneCStockAlertReporter? stockAlerts = null) |
| | | 32 | | { |
| | 0 | 33 | | _context = context; |
| | 0 | 34 | | _stock = stock; |
| | 0 | 35 | | _mappings = mappings; |
| | 0 | 36 | | _shipToSeller = shipToSeller; |
| | 0 | 37 | | _stockAlerts = stockAlerts; |
| | 0 | 38 | | } |
| | | 39 | | |
| | 0 | 40 | | public string Key => TransactionCommitStepKeys.OneCStockCheck; |
| | | 41 | | |
| | | 42 | | public async Task ExecuteAsync(TransactionCommitContext ctx) |
| | | 43 | | { |
| | 0 | 44 | | var d = ctx.Definition; |
| | 0 | 45 | | ctx.StockCheck = StockCheckResult.Empty; |
| | 0 | 46 | | if (!ctx.ProcessIn1C || _stock == null || d.OneCDocument is OneCDocumentKind.None or OneCDocumentKind.Receipt) |
| | | 47 | | { |
| | 0 | 48 | | return; |
| | | 49 | | } |
| | | 50 | | |
| | 0 | 51 | | var ids = ctx.Orders.Where(o => !ctx.StatusOnlyOrderIds.Contains(o.Id)).Select(o => o.Id).ToList(); |
| | 0 | 52 | | if (ids.Count == 0) |
| | | 53 | | { |
| | 0 | 54 | | return; |
| | | 55 | | } |
| | | 56 | | |
| | 0 | 57 | | await ctx.StageAsync("stock-check", "Проверяем остатки в 1С"); |
| | 0 | 58 | | var doc = ctx.Request.Document; |
| | | 59 | | |
| | 0 | 60 | | if (_shipToSeller != null && d.Type == TransactionType.ShippedToSeller && d.WarehouseMode == WarehouseMode.Mappi |
| | | 61 | | { |
| | 0 | 62 | | await CheckShipToSellerAsync(ctx, ids, _shipToSeller); |
| | 0 | 63 | | return; |
| | | 64 | | } |
| | | 65 | | |
| | 0 | 66 | | var stockOrders = await TransactionStockCheckLineBuilder.LoadForStockCheckAsync(_context, ids); |
| | 0 | 67 | | var quantities = QuantitiesFromEdits(ctx); |
| | | 68 | | |
| | | 69 | | IReadOnlyList<StockCheckLine> lines; |
| | 0 | 70 | | switch (d.WarehouseMode) |
| | | 71 | | { |
| | | 72 | | case WarehouseMode.GlobalPair: |
| | | 73 | | case WarehouseMode.SenderOnly: |
| | 0 | 74 | | lines = TransactionStockCheckLineBuilder.FromExplicitWarehouse(stockOrders, doc.SenderWarehouse, quantit |
| | 0 | 75 | | break; |
| | | 76 | | case WarehouseMode.PerOrderBoth: |
| | 0 | 77 | | lines = TransactionStockCheckLineBuilder.FromExplicitWarehouses(stockOrders, SenderByOrder(ctx), doc.Sen |
| | 0 | 78 | | break; |
| | 0 | 79 | | case WarehouseMode.Mapping when d.OneCDocument == OneCDocumentKind.Expense: |
| | 0 | 80 | | lines = TransactionStockCheckLineBuilder.FromEffectiveWarehouse(stockOrders, quantities); |
| | 0 | 81 | | break; |
| | | 82 | | case WarehouseMode.Mapping: |
| | 0 | 83 | | var mappings = _mappings != null ? await _mappings.GetWarehouseMappings() : null; |
| | 0 | 84 | | lines = TransactionStockCheckLineBuilder.FromWarehouseMappings(stockOrders, mappings, ZasylByOrder(ctx), |
| | 0 | 85 | | break; |
| | | 86 | | default: |
| | 0 | 87 | | lines = TransactionStockCheckLineBuilder.FromEffectiveWarehouse(stockOrders, quantities); |
| | | 88 | | break; |
| | | 89 | | } |
| | | 90 | | |
| | 0 | 91 | | ctx.StockCheck = await OneCStockGate.RunAsync(_stock, lines); |
| | 0 | 92 | | await (_stockAlerts?.ReportAsync(ctx.StockCheck, "Проведение через TransactionsGrid", ctx.CancellationToken) ?? |
| | 0 | 93 | | ApplyStockCheckResult(ctx); |
| | 0 | 94 | | } |
| | | 95 | | |
| | | 96 | | /// <summary> |
| | | 97 | | /// «Отгружен реализатору»: как классика — склад основных строк по засылу и связям складов, плюс транзитные |
| | | 98 | | /// строки (qty = 0) с остатком WS. Нехватка по транзиту тоже уводит заказ в черновик 1С. |
| | | 99 | | /// </summary> |
| | | 100 | | private async Task CheckShipToSellerAsync(TransactionCommitContext ctx, List<int> ids, ShipToSellerOneCPlanner plann |
| | | 101 | | { |
| | 0 | 102 | | var zeroIds = ids.Where(id => ctx.Edits.TryGetValue(id, out var e) && ApplyEditsStep.IsZeroQuantity(e)).ToList() |
| | 0 | 103 | | var mainIds = ids.Except(zeroIds).ToList(); |
| | 0 | 104 | | var manufacturers = ctx.Edits.Values |
| | 0 | 105 | | .Where(e => zeroIds.Contains(e.Id) && !string.IsNullOrWhiteSpace(e.Manufacturer)) |
| | 0 | 106 | | .ToDictionary(e => e.Id, e => e.Manufacturer!.Trim()); |
| | 0 | 107 | | var zasyl = ZasylByOrder(ctx).ToDictionary(kv => kv.Key, kv => (string?)kv.Value); |
| | | 108 | | |
| | 0 | 109 | | var plan = await planner.BuildAsync(new ShipToSellerPlanRequest(mainIds, QuantitiesFromEdits(ctx), zasyl, zeroId |
| | 0 | 110 | | ctx.StockCheck = await planner.CheckStockAsync(plan); |
| | 0 | 111 | | await (_stockAlerts?.ReportAsync(ctx.StockCheck, "Отгружен реализатору через TransactionsGrid", ctx.Cancellation |
| | 0 | 112 | | ApplyStockCheckResult(ctx); |
| | 0 | 113 | | } |
| | | 114 | | |
| | | 115 | | private static void ApplyStockCheckResult(TransactionCommitContext ctx) |
| | | 116 | | { |
| | 0 | 117 | | var check = ctx.StockCheck!; |
| | 0 | 118 | | if (!check.HasBlocking) |
| | | 119 | | { |
| | 0 | 120 | | return; |
| | | 121 | | } |
| | | 122 | | |
| | 0 | 123 | | foreach (var id in OneCStockGate.GetBlockingOrderIds(check)) |
| | | 124 | | { |
| | 0 | 125 | | ctx.StockBlockedOrderIds.Add(id); |
| | | 126 | | } |
| | | 127 | | |
| | 0 | 128 | | if (ctx.Request.Document.CreateDraftsOnShortage) |
| | | 129 | | { |
| | | 130 | | // Дефицитные позиции уйдут отдельным черновиком (OneCDocumentStep). |
| | 0 | 131 | | return; |
| | | 132 | | } |
| | | 133 | | |
| | | 134 | | // Классическое безопасное поведение: 1С не вызывается, статусы и документ Studio2 создаются. |
| | 0 | 135 | | ctx.ProcessIn1C = false; |
| | 0 | 136 | | ctx.SummaryBlocks.Add(OneCStockShortageHtmlBuilder.Build(check)); |
| | 0 | 137 | | ctx.Warnings.Add("Недостаточно остатка в 1С — документ 1С не создан, статусы изменены."); |
| | 0 | 138 | | } |
| | | 139 | | |
| | | 140 | | internal static Dictionary<int, decimal> QuantitiesFromEdits(TransactionCommitContext ctx) |
| | | 141 | | { |
| | 0 | 142 | | var result = new Dictionary<int, decimal>(); |
| | 0 | 143 | | foreach (var o in ctx.Orders) |
| | | 144 | | { |
| | 0 | 145 | | var edit = ctx.Edits[o.Id]; |
| | 0 | 146 | | var qty = edit.Changed.Contains("quantity", StringComparer.OrdinalIgnoreCase) && edit.Quantity.HasValue ? ed |
| | 0 | 147 | | if (qty > 0) |
| | | 148 | | { |
| | 0 | 149 | | result[o.Id] = qty; |
| | | 150 | | } |
| | | 151 | | } |
| | | 152 | | |
| | 0 | 153 | | return result; |
| | | 154 | | } |
| | | 155 | | |
| | | 156 | | internal static Dictionary<int, string> SenderByOrder(TransactionCommitContext ctx) |
| | 0 | 157 | | => ctx.Edits.Values.Where(e => !string.IsNullOrWhiteSpace(e.SenderWarehouse)).ToDictionary(e => e.Id, e => e.Sen |
| | | 158 | | |
| | | 159 | | internal static Dictionary<int, string> RecipientByOrder(TransactionCommitContext ctx) |
| | 0 | 160 | | => ctx.Edits.Values.Where(e => !string.IsNullOrWhiteSpace(e.RecipientWarehouse)).ToDictionary(e => e.Id, e => e. |
| | | 161 | | |
| | | 162 | | /// <summary>Засыл: изменённый в строке или текущий из OrderExtension.</summary> |
| | | 163 | | internal static Dictionary<int, string> ZasylByOrder(TransactionCommitContext ctx) |
| | | 164 | | { |
| | 0 | 165 | | var map = new Dictionary<int, string>(); |
| | 0 | 166 | | foreach (var o in ctx.Orders) |
| | | 167 | | { |
| | 0 | 168 | | var edit = ctx.Edits[o.Id]; |
| | 0 | 169 | | var value = edit.Changed.Contains("zasyl", StringComparer.OrdinalIgnoreCase) ? edit.Zasyl : Areas.Studio2.Co |
| | 0 | 170 | | if (!string.IsNullOrWhiteSpace(value)) |
| | | 171 | | { |
| | 0 | 172 | | map[o.Id] = value.Trim(); |
| | | 173 | | } |
| | | 174 | | } |
| | | 175 | | |
| | 0 | 176 | | return map; |
| | | 177 | | } |
| | | 178 | | } |