| | | 1 | | using Microsoft.EntityFrameworkCore; |
| | | 2 | | using OzonDomains.Models; |
| | | 3 | | using OzonOrdersWeb.Areas.Studio2.Controllers.Base; |
| | | 4 | | using OzonRepositories.Context; |
| | | 5 | | using Services.ApiServices._1CApi; |
| | | 6 | | using Services.ApiServices._1CApi.Stock; |
| | | 7 | | |
| | | 8 | | namespace OzonOrdersWeb.Areas.Studio2.Helpers; |
| | | 9 | | |
| | | 10 | | /// <summary> |
| | | 11 | | /// Собирает строки для проверки остатков 1С. Резолв склада-отправителя остаётся там, где он живёт |
| | | 12 | | /// для каждой транзакции, — сервис проверки получает готовые тройки (заказ, склад, количество). |
| | | 13 | | /// </summary> |
| | | 14 | | public static class TransactionStockCheckLineBuilder |
| | | 15 | | { |
| | | 16 | | /// <summary> |
| | | 17 | | /// Перезагружает заказы со связями, нужными для резолва склада (засыл, склад отгрузки) и |
| | | 18 | | /// производителя. Заказы, пришедшие из формы, этих связей не содержат — без перезагрузки склад |
| | | 19 | | /// не определяется и проверка молча ничего не проверяет. |
| | | 20 | | /// </summary> |
| | | 21 | | public static Task<List<Order>> LoadForStockCheckAsync(ApplicationDbContext context, IEnumerable<int> orderIds) |
| | | 22 | | { |
| | 0 | 23 | | var ids = orderIds.Distinct().ToList(); |
| | 0 | 24 | | return context.Orders |
| | 0 | 25 | | .AsNoTracking() |
| | 0 | 26 | | .Include(o => o.OzonClient) |
| | 0 | 27 | | .Include(o => o.ShipmentWarehouse) |
| | 0 | 28 | | .Include(o => o.Delivery) |
| | 0 | 29 | | .Include(o => o.OrderExtensions) |
| | 0 | 30 | | .Include(o => o.EtProducer) |
| | 0 | 31 | | .Include(o => o.Manufacturer) |
| | 0 | 32 | | .Where(o => ids.Contains(o.Id)) |
| | 0 | 33 | | .ToListAsync(); |
| | | 34 | | } |
| | | 35 | | |
| | | 36 | | /// <summary> |
| | | 37 | | /// Количества из формы в ШТУКАХ ЗАКАЗА (без кратности упаковки): пользователь мог изменить их |
| | | 38 | | /// перед отправкой. Кратность применяется позже, по ProductKey перезагруженного заказа. |
| | | 39 | | /// </summary> |
| | | 40 | | public static Dictionary<int, decimal> QuantitiesFromForm(IEnumerable<Order>? formOrders) |
| | | 41 | | { |
| | 1 | 42 | | var result = new Dictionary<int, decimal>(); |
| | 1 | 43 | | if (formOrders == null) |
| | 0 | 44 | | return result; |
| | | 45 | | |
| | 10 | 46 | | foreach (var order in formOrders) |
| | | 47 | | { |
| | 4 | 48 | | var qty = order.Quantity ?? 0; |
| | 4 | 49 | | if (qty > 0) |
| | 2 | 50 | | result[order.Id] = qty; |
| | | 51 | | } |
| | | 52 | | |
| | 1 | 53 | | return result; |
| | | 54 | | } |
| | | 55 | | |
| | | 56 | | /// <summary>Склад выбран пользователем в форме: WarehouseTransfer, ReturnToSupplier.</summary> |
| | | 57 | | public static IReadOnlyList<StockCheckLine> FromExplicitWarehouse( |
| | | 58 | | IEnumerable<Order>? orders, |
| | | 59 | | string? warehouseSender, |
| | | 60 | | IReadOnlyDictionary<int, decimal>? formQuantities = null, |
| | | 61 | | IReadOnlyDictionary<int, decimal>? finalQtyOverrides = null) => |
| | 16 | 62 | | Build(orders, _ => warehouseSender, formQuantities, finalQtyOverrides); |
| | | 63 | | |
| | | 64 | | /// <summary> |
| | | 65 | | /// Склад выбран пользователем отдельно для каждого заказа. Используется для маршрутов |
| | | 66 | | /// «Возврат поставщику»: остаток проверяется именно на складе-отправителе этой строки. |
| | | 67 | | /// </summary> |
| | | 68 | | public static IReadOnlyList<StockCheckLine> FromExplicitWarehouses( |
| | | 69 | | IEnumerable<Order>? orders, |
| | | 70 | | IReadOnlyDictionary<int, string>? warehouseSenderByOrder, |
| | | 71 | | string? defaultWarehouseSender = null, |
| | | 72 | | IReadOnlyDictionary<int, decimal>? formQuantities = null, |
| | | 73 | | IReadOnlyDictionary<int, decimal>? finalQtyOverrides = null) => |
| | 2 | 74 | | Build(orders, |
| | 4 | 75 | | order => warehouseSenderByOrder != null && warehouseSenderByOrder.TryGetValue(order.Id, out var warehouse) |
| | 4 | 76 | | ? warehouse |
| | 4 | 77 | | : defaultWarehouseSender, |
| | 2 | 78 | | formQuantities, |
| | 2 | 79 | | finalQtyOverrides); |
| | | 80 | | |
| | | 81 | | /// <summary> |
| | | 82 | | /// Склад берётся из засыла / маппинга складов: ShippedToCommissioner, ShippedToSeller, транзит. |
| | | 83 | | /// Использует тот же резолвер, что и создание документа. |
| | | 84 | | /// </summary> |
| | | 85 | | public static IReadOnlyList<StockCheckLine> FromWarehouseMappings( |
| | | 86 | | IEnumerable<Order>? orders, |
| | | 87 | | List<WarehouseMapping>? warehouseMappings, |
| | | 88 | | Dictionary<int, string>? orderSupplierMap, |
| | | 89 | | IReadOnlyDictionary<int, decimal>? formQuantities = null, |
| | | 90 | | IReadOnlyDictionary<int, decimal>? finalQtyOverrides = null) => |
| | 3 | 91 | | Build(orders, |
| | 3 | 92 | | order => OneCTransferManager.ResolveWarehouseFor1C(order, orderSupplierMap, warehouseMappings), |
| | 3 | 93 | | formQuantities, |
| | 3 | 94 | | finalQtyOverrides); |
| | | 95 | | |
| | | 96 | | /// <summary>Склад из засыла заказа или склада отгрузки: ShippedToClient (расходная накладная).</summary> |
| | | 97 | | public static IReadOnlyList<StockCheckLine> FromEffectiveWarehouse( |
| | | 98 | | IEnumerable<Order>? orders, |
| | | 99 | | IReadOnlyDictionary<int, decimal>? formQuantities = null, |
| | | 100 | | IReadOnlyDictionary<int, decimal>? finalQtyOverrides = null) => |
| | 2 | 101 | | Build(orders, TransactionWarehouseHelper.GetEffectiveWarehouseName, formQuantities, finalQtyOverrides); |
| | | 102 | | |
| | | 103 | | private static IReadOnlyList<StockCheckLine> Build( |
| | | 104 | | IEnumerable<Order>? orders, |
| | | 105 | | Func<Order, string?> warehouseResolver, |
| | | 106 | | IReadOnlyDictionary<int, decimal>? formQuantities, |
| | | 107 | | IReadOnlyDictionary<int, decimal>? finalQtyOverrides) |
| | | 108 | | { |
| | 15 | 109 | | if (orders == null) |
| | 1 | 110 | | return Array.Empty<StockCheckLine>(); |
| | | 111 | | |
| | 14 | 112 | | var lines = new List<StockCheckLine>(); |
| | 66 | 113 | | foreach (var order in orders) |
| | | 114 | | { |
| | 19 | 115 | | if (order == null) |
| | | 116 | | continue; |
| | | 117 | | |
| | 19 | 118 | | var quantity = ResolveQuantity(order, formQuantities, finalQtyOverrides); |
| | 19 | 119 | | if (quantity <= 0) |
| | | 120 | | continue; |
| | | 121 | | |
| | 17 | 122 | | lines.Add(new StockCheckLine( |
| | 17 | 123 | | order.Id, |
| | 17 | 124 | | order.Article, |
| | 17 | 125 | | order.ProductName, |
| | 17 | 126 | | order.EtProducer?.Name ?? order.Manufacturer?.Name, |
| | 17 | 127 | | warehouseResolver(order), |
| | 17 | 128 | | quantity)); |
| | | 129 | | } |
| | | 130 | | |
| | 14 | 131 | | return lines; |
| | | 132 | | } |
| | | 133 | | |
| | | 134 | | /// <summary> |
| | | 135 | | /// Карта «заказ → склад-отправитель» для ViewData["StockCheckSenderByOrderId"]: предпросмотр на |
| | | 136 | | /// странице должен проверять тот же склад, что и серверный гейт, а в разметке строки стоит |
| | | 137 | | /// «эффективный» склад заказа, а не результат маппинга. |
| | | 138 | | /// </summary> |
| | | 139 | | public static Dictionary<int, string> BuildStockCheckSenderMap( |
| | | 140 | | IEnumerable<Order>? orders, |
| | | 141 | | List<WarehouseMapping>? warehouseMappings) |
| | | 142 | | { |
| | 1 | 143 | | var map = new Dictionary<int, string>(); |
| | 1 | 144 | | if (orders == null) |
| | 0 | 145 | | return map; |
| | | 146 | | |
| | 6 | 147 | | foreach (var order in orders) |
| | | 148 | | { |
| | 2 | 149 | | var sender = OneCTransferManager.ResolveWarehouseFor1C(order, null, warehouseMappings); |
| | 2 | 150 | | if (!string.IsNullOrWhiteSpace(sender)) |
| | 1 | 151 | | map[order.Id] = sender; |
| | | 152 | | } |
| | | 153 | | |
| | 1 | 154 | | return map; |
| | | 155 | | } |
| | | 156 | | |
| | | 157 | | /// <summary> |
| | | 158 | | /// Зеркало расчёта количества в OneCTransferManager.TransferByClientAsync: |
| | | 159 | | /// stockQtyOverrides перекрывает всё, иначе TrueQuantity (то есть штуки заказа × кратность). |
| | | 160 | | /// </summary> |
| | | 161 | | private static decimal ResolveQuantity( |
| | | 162 | | Order order, |
| | | 163 | | IReadOnlyDictionary<int, decimal>? formQuantities, |
| | | 164 | | IReadOnlyDictionary<int, decimal>? finalQtyOverrides) |
| | | 165 | | { |
| | | 166 | | // Готовое количество для 1С (например, остаток WS у транзитного перемещения) — как есть. |
| | 19 | 167 | | if (finalQtyOverrides != null && finalQtyOverrides.TryGetValue(order.Id, out var finalQty) && finalQty > 0) |
| | 2 | 168 | | return finalQty; |
| | | 169 | | |
| | | 170 | | // Штуки заказа: из формы, если пользователь их правил, иначе из БД. Кратность упаковки |
| | | 171 | | // берём из ProductKey перезагруженного заказа — в форме это скрытое поле, на него полагаться |
| | | 172 | | // нельзя, а ошибка тут занижает потребность в 10 раз (артикул вида 9046708011=TYT[10]). |
| | 17 | 173 | | var baseQty = formQuantities != null && formQuantities.TryGetValue(order.Id, out var formQty) |
| | 17 | 174 | | ? formQty |
| | 17 | 175 | | : order.Quantity ?? 0; |
| | | 176 | | |
| | 17 | 177 | | if (baseQty <= 0) |
| | 2 | 178 | | return 0; |
| | | 179 | | |
| | 15 | 180 | | return baseQty == decimal.Truncate(baseQty) |
| | 15 | 181 | | ? Order.ApplyProductKeyMultiplier((int)baseQty, order.ProductKey) |
| | 15 | 182 | | : baseQty; |
| | | 183 | | } |
| | | 184 | | } |