| | | 1 | | using OzonDomains.Models; |
| | | 2 | | using OzonDomains.Models.BitrixModels; |
| | | 3 | | using OzonOrdersWeb.Areas.Studio2.ViewModels.Bitrix; |
| | | 4 | | using Services.BitrixStockServices; |
| | | 5 | | |
| | | 6 | | namespace OzonOrdersWeb.WebServices.BitrixStock; |
| | | 7 | | |
| | | 8 | | /// <summary>Склад Bitrix в строке грида «Остатки»: цвет/жирность — по связкам с Ozon (как раньше в Razor).</summary> |
| | | 9 | | public sealed class BitrixStockGridStoreDto |
| | | 10 | | { |
| | | 11 | | public string Title { get; set; } = string.Empty; |
| | | 12 | | public double Amount { get; set; } |
| | | 13 | | public string Color { get; set; } = BitrixStockGridRowBuilder.NeutralColor; |
| | | 14 | | public bool Bold { get; set; } |
| | | 15 | | } |
| | | 16 | | |
| | | 17 | | /// <summary>Склад Ozon в строке: остаток, связанный склад Bitrix, доступность наката/обнуления.</summary> |
| | | 18 | | public sealed class BitrixStockGridOzonStoreDto |
| | | 19 | | { |
| | | 20 | | public string Title { get; set; } = string.Empty; |
| | | 21 | | public double Amount { get; set; } |
| | | 22 | | public string? ClientName { get; set; } |
| | | 23 | | public string OfferId { get; set; } = string.Empty; |
| | | 24 | | public string? ProductId { get; set; } |
| | | 25 | | public long StoreId { get; set; } |
| | | 26 | | public string Color { get; set; } = BitrixStockGridRowBuilder.NeutralColor; |
| | | 27 | | public string AmountColor { get; set; } = BitrixStockGridRowBuilder.NeutralColor; |
| | | 28 | | public bool Bold { get; set; } |
| | | 29 | | /// <summary>Подсказка кнопки наката: остатки Ozon/связанного склада, расхождение, доступные связи.</summary> |
| | | 30 | | public string Hint { get; set; } = string.Empty; |
| | | 31 | | /// <summary>Накат доступен: есть связанный склад Bitrix и товар активен.</summary> |
| | | 32 | | public bool CanRefresh { get; set; } |
| | | 33 | | /// <summary>Обнуление неактивного товара: Ozon ≠ 0 и расходится со связанным складом.</summary> |
| | | 34 | | public bool CanClear { get; set; } |
| | | 35 | | public string? LinkedStore { get; set; } |
| | | 36 | | public double TargetAmount { get; set; } |
| | | 37 | | public bool AmountsDiffer { get; set; } |
| | | 38 | | } |
| | | 39 | | |
| | | 40 | | public sealed class BitrixStockGridPriceIndexDto |
| | | 41 | | { |
| | | 42 | | public double Value { get; set; } |
| | | 43 | | public decimal MinPrice { get; set; } |
| | | 44 | | public string ColorIndex { get; set; } = string.Empty; |
| | | 45 | | public string? ClientName { get; set; } |
| | | 46 | | } |
| | | 47 | | |
| | | 48 | | /// <summary>Строка грида «Остатки» (/Studio2/BitrixStock).</summary> |
| | | 49 | | public sealed class BitrixStockGridRowDto |
| | | 50 | | { |
| | 5 | 51 | | public int ProductId { get; set; } |
| | 5 | 52 | | public string? Article { get; set; } |
| | 5 | 53 | | public string? MarketPrefix { get; set; } |
| | 11 | 54 | | public string ArticleWithKey { get; set; } = string.Empty; |
| | 5 | 55 | | public string? Supplier { get; set; } |
| | 5 | 56 | | public string? PreviewText { get; set; } |
| | 10 | 57 | | public string Active { get; set; } = "Y"; |
| | 27 | 58 | | public List<BitrixStockGridStoreDto> BitrixStores { get; set; } = new(); |
| | 6 | 59 | | public double BitrixTotal { get; set; } |
| | 16 | 60 | | public List<BitrixStockGridOzonStoreDto> OzonStores { get; set; } = new(); |
| | 5 | 61 | | public double OzonTotal { get; set; } |
| | 7 | 62 | | public List<BitrixStockGridPriceIndexDto> PriceIndexes { get; set; } = new(); |
| | 5 | 63 | | public DateTime TimestampX { get; set; } |
| | 5 | 64 | | public double? Price { get; set; } |
| | 5 | 65 | | public string? PriceAuthor { get; set; } |
| | 5 | 66 | | public DateTime? PriceUpdatedAt { get; set; } |
| | 5 | 67 | | public decimal? PriceHistoryValue { get; set; } |
| | | 68 | | } |
| | | 69 | | |
| | | 70 | | /// <summary> |
| | | 71 | | /// Строки грида «Остатки»: логика, которая раньше жила в Razor каждой строки Index.cshtml — видимые склады |
| | | 72 | | /// Bitrix по фильтру, связки Bitrix → Ozon по WarehouseMapping (цвета пар складов), кнопки наката/обнуления. |
| | | 73 | | /// </summary> |
| | | 74 | | public static class BitrixStockGridRowBuilder |
| | | 75 | | { |
| | | 76 | | public const string NeutralColor = "#9ca3af"; |
| | | 77 | | private const string MappedColor = "#0d6efd"; |
| | | 78 | | private const string DiffColor = "orange"; |
| | | 79 | | private static readonly string[] Palette = { "#2563eb", "#7c3aed", "#0d9488", "#ea580c", "#db2777", "#16a34a", "#089 |
| | | 80 | | |
| | | 81 | | public static List<BitrixStockGridRowDto> Build( |
| | | 82 | | IEnumerable<RemainingStockBitrix> items, |
| | | 83 | | IReadOnlyCollection<WarehouseMapping> mappings, |
| | | 84 | | IReadOnlyCollection<string>? selectedStoreTitles, |
| | | 85 | | int? selectedClientId, |
| | | 86 | | string? selectedClientName) |
| | | 87 | | { |
| | | 88 | | var storeFilter = (selectedStoreTitles ?? Array.Empty<string>()).ToHashSet(StringComparer.OrdinalIgnoreCase); |
| | | 89 | | var relevantMappings = mappings |
| | | 90 | | .Where(m => !selectedClientId.HasValue || m.OzonClientId == selectedClientId.Value) |
| | | 91 | | .Where(m => storeFilter.Count == 0 || storeFilter.Contains(m.BitrixWarehouseName?.Trim() ?? string.Empty)) |
| | | 92 | | .ToList(); |
| | | 93 | | var linkageMappings = BitrixOzonStockLinkage.GetStockLinkageMappings(relevantMappings); |
| | | 94 | | var mappedWarehouseIds = linkageMappings |
| | | 95 | | .Select(m => long.TryParse(m.OzonWarehouseId?.Trim(), out var id) ? id : (long?)null) |
| | | 96 | | .Where(id => id.HasValue) |
| | | 97 | | .Select(id => id!.Value) |
| | | 98 | | .ToHashSet(); |
| | | 99 | | var mappedBitrixStoreNames = linkageMappings |
| | | 100 | | .Select(m => m.BitrixWarehouseName!.Trim()) |
| | | 101 | | .ToHashSet(StringComparer.OrdinalIgnoreCase); |
| | | 102 | | |
| | | 103 | | return items.Select(item => BuildRow(item, storeFilter, selectedClientId, selectedClientName, |
| | | 104 | | linkageMappings, mappedWarehouseIds, mappedBitrixStoreNames)).ToList(); |
| | | 105 | | } |
| | | 106 | | |
| | | 107 | | private static BitrixStockGridRowDto BuildRow( |
| | | 108 | | RemainingStockBitrix item, |
| | | 109 | | HashSet<string> storeFilter, |
| | | 110 | | int? selectedClientId, |
| | | 111 | | string? selectedClientName, |
| | | 112 | | List<WarehouseMapping> linkageMappings, |
| | | 113 | | HashSet<long> mappedWarehouseIds, |
| | | 114 | | HashSet<string> mappedBitrixStoreNames) |
| | | 115 | | { |
| | | 116 | | var linkedOzonStores = (item.OzonStores ?? new List<StockByStore>()) |
| | | 117 | | .Where(BitrixOzonStockLinkage.HasValidOzonProductId) |
| | | 118 | | .Where(s => string.IsNullOrWhiteSpace(selectedClientName) || |
| | | 119 | | string.Equals(s.ClientName, selectedClientName, StringComparison.OrdinalIgnoreCase)) |
| | | 120 | | .Where(s => s.StoreId > 0 && mappedWarehouseIds.Contains(s.StoreId)) |
| | | 121 | | .Where(s => s.LinkStores != null && s.LinkStores.Any(ls => |
| | | 122 | | linkageMappings.Any(m => |
| | | 123 | | string.Equals(m.BitrixWarehouseName?.Trim(), ls, StringComparison.OrdinalIgnoreCase) && |
| | | 124 | | (!selectedClientId.HasValue || m.OzonClientId == selectedClientId.Value) && |
| | | 125 | | long.TryParse(m.OzonWarehouseId?.Trim(), out var wid) && wid == s.StoreId))) |
| | | 126 | | .ToList(); |
| | | 127 | | |
| | | 128 | | var linkedBitrixStoreNames = linkedOzonStores |
| | | 129 | | .SelectMany(s => s.LinkStores!) |
| | | 130 | | .Concat((item.Stores ?? new List<StockByStore>()) |
| | | 131 | | .Where(s => mappedBitrixStoreNames.Contains(s.Title)) |
| | | 132 | | .Select(s => s.Title)) |
| | | 133 | | .Distinct(StringComparer.OrdinalIgnoreCase) |
| | | 134 | | .ToList(); |
| | | 135 | | var colorMap = linkedBitrixStoreNames |
| | | 136 | | .Select((name, index) => (name, color: Palette[index % Palette.Length])) |
| | | 137 | | .ToDictionary(x => x.name, x => x.color, StringComparer.OrdinalIgnoreCase); |
| | | 138 | | |
| | | 139 | | var visibleStores = (item.Stores ?? new List<StockByStore>()) |
| | | 140 | | .Where(s => storeFilter.Count == 0 || storeFilter.Contains(s.Title)) |
| | | 141 | | .ToList(); |
| | | 142 | | |
| | | 143 | | var row = new BitrixStockGridRowDto |
| | | 144 | | { |
| | | 145 | | ProductId = item.ProductId, |
| | | 146 | | Article = item.Article, |
| | | 147 | | MarketPrefix = item.MarketPrefix, |
| | | 148 | | ArticleWithKey = item.ArticleWithKey, |
| | | 149 | | Supplier = item.Supplier, |
| | | 150 | | PreviewText = item.PreviewText, |
| | | 151 | | Active = item.Active, |
| | | 152 | | TimestampX = item.TimestampX, |
| | | 153 | | Price = item.Price, |
| | | 154 | | PriceAuthor = item.PriceHistory?.Author, |
| | | 155 | | PriceUpdatedAt = item.PriceHistory?.CreateDateTime, |
| | | 156 | | PriceHistoryValue = item.PriceHistory?.PriceValue |
| | | 157 | | }; |
| | | 158 | | |
| | | 159 | | foreach (var ws in visibleStores) |
| | | 160 | | { |
| | | 161 | | var isLinked = linkedOzonStores.Any(s => s.LinkStores?.Contains(ws.Title) == true); |
| | | 162 | | var isMapped = mappedBitrixStoreNames.Contains(ws.Title); |
| | | 163 | | row.BitrixStores.Add(new BitrixStockGridStoreDto |
| | | 164 | | { |
| | | 165 | | Title = ws.Title, |
| | | 166 | | Amount = ws.Amount, |
| | | 167 | | Color = isLinked && colorMap.TryGetValue(ws.Title, out var c) ? c : isMapped ? MappedColor : NeutralColo |
| | | 168 | | Bold = isLinked || isMapped |
| | | 169 | | }); |
| | | 170 | | } |
| | | 171 | | row.BitrixTotal = row.BitrixStores.Sum(s => s.Amount); |
| | | 172 | | |
| | | 173 | | var isInactive = string.Equals(item.Active, "N", StringComparison.OrdinalIgnoreCase); |
| | | 174 | | foreach (var ws in linkedOzonStores) |
| | | 175 | | { |
| | | 176 | | var hasMapping = ws.LinkStores != null && ws.LinkStores.Any(); |
| | | 177 | | var linkedStore = item.Stores?.FirstOrDefault(st => ws.LinkStores?.Contains(st.Title) == true); |
| | | 178 | | var primaryName = linkedStore?.Title ?? ws.LinkStores?.FirstOrDefault(); |
| | | 179 | | var isLinkedOzon = hasMapping && !string.IsNullOrWhiteSpace(primaryName) && colorMap.ContainsKey(primaryName |
| | | 180 | | var color = isLinkedOzon ? colorMap[primaryName!] : NeutralColor; |
| | | 181 | | var differ = linkedStore != null && Math.Abs(linkedStore.Amount - ws.Amount) > 0.0001; |
| | | 182 | | var ozonRounded = (int)Math.Round(ws.Amount); |
| | | 183 | | var linkedRounded = linkedStore != null ? (int)Math.Round(linkedStore.Amount) : (int?)null; |
| | | 184 | | |
| | | 185 | | var hint = new List<string> { $"Озон: {ws.Amount}" }; |
| | | 186 | | if (linkedStore != null) |
| | | 187 | | { |
| | | 188 | | hint.Add($"Связано с '{linkedStore.Title}': {linkedStore.Amount}"); |
| | | 189 | | if (differ) hint.Add("⚠️ Количества отличаются!"); |
| | | 190 | | } |
| | | 191 | | else |
| | | 192 | | { |
| | | 193 | | hint.Add("Сопоставление есть, строка Bitrix на товаре не найдена"); |
| | | 194 | | } |
| | | 195 | | |
| | | 196 | | if (ws.LinkStores?.Any() == true) hint.Add($"Доступные связи: {string.Join(", ", ws.LinkStores)}"); |
| | | 197 | | |
| | | 198 | | row.OzonStores.Add(new BitrixStockGridOzonStoreDto |
| | | 199 | | { |
| | | 200 | | Title = ws.Title, |
| | | 201 | | Amount = ws.Amount, |
| | | 202 | | ClientName = ws.ClientName, |
| | | 203 | | OfferId = ws.OfferId, |
| | | 204 | | ProductId = ws.ProductId, |
| | | 205 | | StoreId = ws.StoreId, |
| | | 206 | | Color = color, |
| | | 207 | | AmountColor = Math.Abs(ws.Amount) < 0.0001 ? NeutralColor : differ ? DiffColor : color, |
| | | 208 | | Bold = hasMapping && isLinkedOzon, |
| | | 209 | | Hint = string.Join(" | ", hint), |
| | | 210 | | CanRefresh = linkedStore != null && !isInactive, |
| | | 211 | | CanClear = isInactive && ws.Amount != 0 && (linkedStore == null || linkedRounded != ozonRounded), |
| | | 212 | | LinkedStore = linkedStore?.Title, |
| | | 213 | | TargetAmount = linkedStore?.Amount ?? ws.Amount, |
| | | 214 | | AmountsDiffer = differ |
| | | 215 | | }); |
| | | 216 | | } |
| | | 217 | | row.OzonTotal = row.OzonStores.Sum(s => s.Amount); |
| | | 218 | | |
| | | 219 | | foreach (var pi in item.OzonPriceIndexes ?? new List<OzonPriceIndexes>()) |
| | | 220 | | { |
| | | 221 | | if (pi.OzonIndex == null || pi.OzonIndex.PriceIndexValue == 0) continue; |
| | | 222 | | row.PriceIndexes.Add(new BitrixStockGridPriceIndexDto |
| | | 223 | | { |
| | | 224 | | Value = pi.OzonIndex.PriceIndexValue, |
| | | 225 | | MinPrice = pi.OzonIndex.MinPrice, |
| | | 226 | | ColorIndex = pi.ColorIndex ?? string.Empty, |
| | | 227 | | ClientName = pi.ClientName |
| | | 228 | | }); |
| | | 229 | | } |
| | | 230 | | |
| | | 231 | | return row; |
| | | 232 | | } |
| | | 233 | | } |