| | | 1 | | using OzonDomains; |
| | | 2 | | using OzonDomains.Models; |
| | | 3 | | |
| | | 4 | | namespace OzonOrdersWeb.WebServices.Orders.Grid; |
| | | 5 | | |
| | | 6 | | /// <summary> |
| | | 7 | | /// Чистое преобразование <see cref="Order"/> → <see cref="OrderGridRowDto"/>. |
| | | 8 | | /// Правила тона строки и группировки по отправлению повторяют <c>_IndexTableBody.cshtml</c>. |
| | | 9 | | /// Гейтинг: поля закрытых колонок остаются null (см. <see cref="OrdersGridColumnAccess"/>). |
| | | 10 | | /// </summary> |
| | | 11 | | public static class OrderGridRowMapper |
| | | 12 | | { |
| | | 13 | | public static List<OrderGridRowDto> MapBlock( |
| | | 14 | | IReadOnlyList<Order> orders, |
| | | 15 | | IReadOnlySet<string> allowedColumns, |
| | | 16 | | IReadOnlyDictionary<string, bool>? labelExists, |
| | | 17 | | bool groupByShipment, |
| | | 18 | | string? groupBy = null) |
| | | 19 | | { |
| | 2 | 20 | | var groupSizes = new Dictionary<long, int>(); |
| | 2 | 21 | | if (groupByShipment) |
| | | 22 | | { |
| | 10 | 23 | | foreach (var o in orders) |
| | | 24 | | { |
| | 4 | 25 | | if (o.ShipmentId.HasValue) |
| | | 26 | | { |
| | 3 | 27 | | groupSizes[o.ShipmentId.Value] = groupSizes.GetValueOrDefault(o.ShipmentId.Value) + 1; |
| | | 28 | | } |
| | | 29 | | } |
| | | 30 | | } |
| | | 31 | | |
| | 2 | 32 | | var result = new List<OrderGridRowDto>(orders.Count); |
| | 2 | 33 | | long? previousShipmentId = null; |
| | 2 | 34 | | var groupKeyMode = groupBy != null && OrdersGridGrouping.Supports(groupBy) && !OrdersGridGrouping.IsShipment(gro |
| | 2 | 35 | | string? previousGroupKey = null; |
| | 2 | 36 | | var first = true; |
| | 16 | 37 | | foreach (var order in orders) |
| | | 38 | | { |
| | 6 | 39 | | string? position = null; |
| | 6 | 40 | | var groupSize = 0; |
| | 6 | 41 | | if (groupByShipment && order.ShipmentId.HasValue) |
| | | 42 | | { |
| | 3 | 43 | | groupSize = groupSizes.GetValueOrDefault(order.ShipmentId.Value, 1); |
| | 3 | 44 | | if (groupSize > 1) |
| | | 45 | | { |
| | 2 | 46 | | position = order.ShipmentId != previousShipmentId ? "first" : "continuation"; |
| | | 47 | | } |
| | 3 | 48 | | previousShipmentId = order.ShipmentId; |
| | | 49 | | } |
| | | 50 | | else |
| | | 51 | | { |
| | 3 | 52 | | previousShipmentId = null; |
| | | 53 | | } |
| | | 54 | | |
| | 6 | 55 | | var row = Map(order, allowedColumns, labelExists); |
| | 6 | 56 | | row.ShipmentGroupPosition = position; |
| | 6 | 57 | | row.ShipmentGroupSize = groupSize; |
| | 6 | 58 | | if (groupKeyMode) |
| | | 59 | | { |
| | 0 | 60 | | var key = OrdersGridGrouping.KeyOf(order, groupBy!) ?? string.Empty; |
| | 0 | 61 | | row.GroupKey = key; |
| | 0 | 62 | | row.GroupPosition = first || !string.Equals(key, previousGroupKey, StringComparison.Ordinal) ? "first" : |
| | 0 | 63 | | previousGroupKey = key; |
| | | 64 | | } |
| | 6 | 65 | | else if (groupByShipment) |
| | | 66 | | { |
| | 4 | 67 | | row.GroupKey = order.ShipmentId?.ToString(); |
| | 4 | 68 | | row.GroupPosition = position == "continuation" ? "continuation" : "first"; |
| | | 69 | | } |
| | | 70 | | |
| | 6 | 71 | | first = false; |
| | 6 | 72 | | result.Add(row); |
| | | 73 | | } |
| | | 74 | | |
| | 2 | 75 | | return result; |
| | | 76 | | } |
| | | 77 | | |
| | | 78 | | public static OrderGridRowDto Map( |
| | | 79 | | Order o, |
| | | 80 | | IReadOnlySet<string> allowed, |
| | | 81 | | IReadOnlyDictionary<string, bool>? labelExists) |
| | | 82 | | { |
| | 11 | 83 | | var row = new OrderGridRowDto |
| | 11 | 84 | | { |
| | 11 | 85 | | Id = o.Id, |
| | 11 | 86 | | ShipmentId = o.ShipmentId, |
| | 11 | 87 | | CartItemId = o.CartItemId, |
| | 11 | 88 | | IsArchived = o.OrderArchive != null, |
| | 11 | 89 | | IsReturned = o.IsReturned, |
| | 11 | 90 | | IsReturnable = o.IsReturnable == true, |
| | 11 | 91 | | IsCancelled = IsCancelled(o), |
| | 11 | 92 | | MarketStatus = o.Status, |
| | 11 | 93 | | FromFile = o.FromFile == true, |
| | 11 | 94 | | HasOzonPromotion = o.OzonPromotionSnapshot?.HasAnyPromotion == true, |
| | 11 | 95 | | HasTransactionDocument = !string.IsNullOrEmpty(Extension(o, "Документ транзакции")), |
| | 11 | 96 | | ClientKind = o.OzonClient?.ClientType switch |
| | 11 | 97 | | { |
| | 9 | 98 | | ClientType.OZON => "ozon", |
| | 0 | 99 | | ClientType.YANDEX => "yandex", |
| | 2 | 100 | | _ => "other" |
| | 11 | 101 | | } |
| | 11 | 102 | | }; |
| | 11 | 103 | | row.RowTone = ResolveRowTone(o, row.IsCancelled); |
| | 11 | 104 | | if (o.Problems is { Count: > 0 }) |
| | 2 | 105 | | row.Problems = o.Problems.Select(x => new OrderGridProblemDto(x.Severity, x.Kind, x.Text)).ToList(); |
| | | 106 | | |
| | 11 | 107 | | if (!string.IsNullOrEmpty(o.ShipmentNumber) && labelExists != null |
| | 11 | 108 | | && labelExists.TryGetValue(o.ShipmentNumber, out var exists)) |
| | | 109 | | { |
| | 1 | 110 | | row.LabelExists = exists; |
| | | 111 | | } |
| | | 112 | | |
| | 11 | 113 | | if (allowed.Contains("ShipmentNumber")) |
| | | 114 | | { |
| | 9 | 115 | | row.ShipmentNumber = o.ShipmentNumber; |
| | 9 | 116 | | row.Scanit = o.Scanit; |
| | | 117 | | } |
| | | 118 | | |
| | 11 | 119 | | if (allowed.Contains("Delivery")) |
| | | 120 | | { |
| | 8 | 121 | | row.DeliveryProvider = o.Delivery?.Provider?.Name; |
| | | 122 | | } |
| | | 123 | | |
| | 11 | 124 | | if (allowed.Contains("DeliveryType")) |
| | | 125 | | { |
| | 8 | 126 | | row.DeliveryName = o.Delivery?.Name; |
| | | 127 | | } |
| | | 128 | | |
| | 11 | 129 | | if (allowed.Contains("TrackingNumber")) |
| | | 130 | | { |
| | 8 | 131 | | row.TrackingNumber = o.TrackingNumber; |
| | | 132 | | } |
| | | 133 | | |
| | 11 | 134 | | if (allowed.Contains("OzonClient")) |
| | | 135 | | { |
| | 9 | 136 | | row.OzonClient = o.OzonClient?.Name; |
| | 9 | 137 | | row.OzonClientType = o.OzonClient?.ClientType.ToString(); |
| | | 138 | | } |
| | | 139 | | |
| | 11 | 140 | | if (allowed.Contains("DeliveryCity")) |
| | | 141 | | { |
| | 8 | 142 | | row.DeliveryCity = o.DeliveryCity; |
| | | 143 | | } |
| | | 144 | | |
| | 11 | 145 | | if (allowed.Contains("ProcessingDate")) |
| | | 146 | | { |
| | 8 | 147 | | row.ProcessingDate = o.ProcessingDate; |
| | 8 | 148 | | row.ProcessingDateText = OrderDateDisplay.FormatTableDateFull(o.ProcessingDate); |
| | 8 | 149 | | row.ProcessingDateFull = o.FullFormattedProcessingDate; |
| | | 150 | | } |
| | | 151 | | |
| | 11 | 152 | | if (allowed.Contains("ShippingDate")) |
| | | 153 | | { |
| | 8 | 154 | | row.ShippingDate = o.ShippingDate; |
| | 8 | 155 | | row.ShippingDateText = OrderDateDisplay.FormatTableDateFull(o.ShippingDate); |
| | 8 | 156 | | row.ShippingDateFull = o.FullFormattedShippingDate; |
| | | 157 | | } |
| | | 158 | | |
| | 11 | 159 | | if (allowed.Contains("DeliveryPeriod")) |
| | | 160 | | { |
| | 8 | 161 | | row.DeliveryPeriodText = o.FormattedDeliveryDay; |
| | 8 | 162 | | row.DeliveryPeriodHoursText = o.FormattedDeliveryHours; |
| | 8 | 163 | | row.TimeLeftText = o.FormattedTimeLeftDay; |
| | 8 | 164 | | var timeLeft = o.TimeLeft; |
| | 8 | 165 | | row.TimeLeftDays = timeLeft?.Days; |
| | 8 | 166 | | row.DeliveryOverdue = timeLeft.HasValue && timeLeft.Value < TimeSpan.Zero; |
| | | 167 | | } |
| | | 168 | | |
| | 11 | 169 | | if (allowed.Contains("ProductKey")) |
| | | 170 | | { |
| | 8 | 171 | | row.ProductKey = o.ProductKey; |
| | 8 | 172 | | row.ImageUrl = o.ProductInfo?.ImageUrl; |
| | 8 | 173 | | row.Barcode = o.ProductInfo?.Barcode; |
| | | 174 | | } |
| | | 175 | | |
| | 11 | 176 | | if (allowed.Contains("Manufacturer")) |
| | | 177 | | { |
| | 8 | 178 | | row.Manufacturer = o.EtProducer?.Name; |
| | 8 | 179 | | row.EtProducerId = o.EtProducer?.Id; |
| | | 180 | | } |
| | | 181 | | |
| | 11 | 182 | | if (allowed.Contains("Article")) |
| | | 183 | | { |
| | 8 | 184 | | row.Article = o.Article; |
| | 8 | 185 | | row.ReplacementArticle = o.ProductInfo?.ReplacementProduct?.Article; |
| | | 186 | | } |
| | | 187 | | |
| | 11 | 188 | | if (allowed.Contains("ProductName")) |
| | | 189 | | { |
| | 8 | 190 | | row.ProductName = o.ProductName; |
| | | 191 | | } |
| | | 192 | | |
| | 11 | 193 | | if (allowed.Contains("ClientStatus")) |
| | | 194 | | { |
| | 8 | 195 | | row.ClientStatus = o.ClientStatus?.Name; |
| | 8 | 196 | | row.ClientStatusColor = o.ClientStatus?.StatusColor?.ColorCode; |
| | 8 | 197 | | row.CancellationReason = BuildCancellationReason(o); |
| | | 198 | | } |
| | | 199 | | |
| | 11 | 200 | | if (allowed.Contains("AppStatus")) |
| | | 201 | | { |
| | 8 | 202 | | row.AppStatus = o.AppStatus?.Name; |
| | 8 | 203 | | row.AppStatusColor = o.AppStatus?.StatusColor?.ColorCode; |
| | 8 | 204 | | row.AppStatusIsFinal = o.AppStatus?.IsFinal == true; |
| | 8 | 205 | | row.CartItemStatus = o.CartItemStatus?.Name; |
| | 8 | 206 | | row.CartItemStatusColor = o.CartItemStatusColor?.ColorCode; |
| | 8 | 207 | | row.CartItemStatusId = o.CartItemStatus?.Id; |
| | 8 | 208 | | row.SupplierStatus = o.SupplierStatusName; |
| | 8 | 209 | | row.SupplierStatusColor = o.SupplierStatusColor; |
| | 8 | 210 | | row.SupplierStatusProviderKey = o.SupplierStateProviderKey; |
| | 8 | 211 | | row.SupplierStatusTooltip = BuildSupplierTooltip(o); |
| | 8 | 212 | | row.SupplierExpectedDeliveryText = o.SupplierExpectedDeliveryDate.HasValue ? o.FormattedSupplierExpectedDeli |
| | 8 | 213 | | row.SupplierExpectedDeliveryDaysLeft = o.FormattedSupplierExpectedDeliveryDaysLeft; |
| | 8 | 214 | | row.ReturnStatusText = string.IsNullOrWhiteSpace(o.ReturnStatusDisplayName) ? o.ReturnStatusSysName : o.Retu |
| | | 215 | | } |
| | | 216 | | |
| | 11 | 217 | | if (allowed.Contains("ShipmentWarehouse")) |
| | | 218 | | { |
| | 8 | 219 | | row.ShipmentWarehouse = o.ShipmentWarehouse?.Name; |
| | 8 | 220 | | row.ZasylWarehouse = Extension(o, "Засыл"); |
| | 8 | 221 | | row.ReturnWarehouseName = o.ReturnWarehouseName; |
| | | 222 | | } |
| | | 223 | | |
| | 11 | 224 | | if (allowed.Contains("LastStatusChangeDate")) |
| | | 225 | | { |
| | 8 | 226 | | row.LastStatusChangeDate = o.LastStatusChangeDate; |
| | 8 | 227 | | row.LastStatusChangeDateText = o.FormattedLastStatusChangeDate; |
| | 8 | 228 | | row.LastStatusChangeDateFull = o.FullFormattedLastStatusChangeDate; |
| | | 229 | | } |
| | | 230 | | |
| | 11 | 231 | | if (allowed.Contains("Supplier")) |
| | | 232 | | { |
| | 8 | 233 | | row.Supplier = o.Supplier?.Name; |
| | 8 | 234 | | row.SupplierDirection = o.SupplierStateDirection; |
| | | 235 | | } |
| | | 236 | | |
| | 11 | 237 | | if (allowed.Contains("SupplierOrderNumber")) |
| | | 238 | | { |
| | 8 | 239 | | row.OrderNumberToSupplier = o.OrderNumberToSupplier; |
| | | 240 | | } |
| | | 241 | | |
| | 11 | 242 | | if (allowed.Contains("Category")) |
| | | 243 | | { |
| | 8 | 244 | | row.Category = o.ProductInfo?.CommercialCategory; |
| | | 245 | | } |
| | | 246 | | |
| | 11 | 247 | | if (allowed.Contains("VolumeWeight") && o.ProductInfo != null) |
| | | 248 | | { |
| | 0 | 249 | | row.VolumetricWeight = o.ProductInfo.VolumetricWeight; |
| | 0 | 250 | | row.Weight = o.ProductInfo.Weight; |
| | | 251 | | } |
| | | 252 | | |
| | 11 | 253 | | if (allowed.Contains("SitePrice")) |
| | | 254 | | { |
| | 8 | 255 | | row.SitePrice = o.ProductInfo?.CurrentPriceWithDiscount; |
| | | 256 | | } |
| | | 257 | | |
| | 11 | 258 | | if (allowed.Contains("Quantity")) |
| | | 259 | | { |
| | 8 | 260 | | row.Quantity = o.Quantity; |
| | | 261 | | } |
| | | 262 | | |
| | 11 | 263 | | if (allowed.Contains("Price")) |
| | | 264 | | { |
| | 8 | 265 | | row.Price = o.Price; |
| | 8 | 266 | | row.Currency = o.Сurrency?.Name; |
| | 8 | 267 | | row.PriceIndexColor = o.OrderPrice?.PriceIndexes?.ColorIndex; |
| | | 268 | | } |
| | | 269 | | |
| | 11 | 270 | | if (allowed.Contains("ShipmentTotal")) |
| | | 271 | | { |
| | 8 | 272 | | row.ShipmentAmount = o.ShipmentAmount; |
| | | 273 | | } |
| | | 274 | | |
| | 11 | 275 | | if (allowed.Contains("PurchasePrice")) |
| | | 276 | | { |
| | 8 | 277 | | row.PurchasePrice = o.PurchasePrice; |
| | | 278 | | } |
| | | 279 | | |
| | 11 | 280 | | if (allowed.Contains("Cost")) |
| | | 281 | | { |
| | 8 | 282 | | row.CostPrice = o.CostPrice; |
| | 8 | 283 | | var expenses = (o.ExtraExpenses ?? []) |
| | 0 | 284 | | .Where(e => e.ExpenseTypeId > 0 && e.Amount > 0) |
| | 8 | 285 | | .ToList(); |
| | 8 | 286 | | if (expenses.Count > 0) |
| | | 287 | | { |
| | 0 | 288 | | row.ExtraExpenses = expenses.Sum(e => e.Amount); |
| | 0 | 289 | | row.ExtraExpensesItems = expenses |
| | 0 | 290 | | .Select(e => new OrderGridExtraExpenseDto |
| | 0 | 291 | | { |
| | 0 | 292 | | Type = e.ExpenseType?.Name ?? string.Empty, |
| | 0 | 293 | | Amount = e.Amount, |
| | 0 | 294 | | Comment = e.Comment |
| | 0 | 295 | | }) |
| | 0 | 296 | | .ToList(); |
| | | 297 | | } |
| | | 298 | | } |
| | | 299 | | |
| | 11 | 300 | | if (allowed.Contains("OzonCommission")) |
| | | 301 | | { |
| | 8 | 302 | | row.OzonCommissionMin = o.MinOzonCommission; |
| | 8 | 303 | | row.OzonCommissionMax = o.MaxOzonCommission; |
| | | 304 | | } |
| | | 305 | | |
| | 11 | 306 | | if (allowed.Contains("Profit")) |
| | | 307 | | { |
| | 8 | 308 | | row.ProfitMin = o.MinProfit; |
| | 8 | 309 | | row.ProfitMax = o.MaxProfit; |
| | 8 | 310 | | row.TargetProfit135 = o.TargetShipmentProfit135; |
| | | 311 | | } |
| | | 312 | | |
| | 11 | 313 | | if (allowed.Contains("MarkupPercent")) |
| | | 314 | | { |
| | 8 | 315 | | row.DiscountMin = o.MinDiscount; |
| | 8 | 316 | | row.DiscountMax = o.MaxDiscount; |
| | | 317 | | } |
| | | 318 | | |
| | 11 | 319 | | return row; |
| | | 320 | | } |
| | | 321 | | |
| | | 322 | | /// <summary>Правило «отменён» из _IndexTableBody: красная строка.</summary> |
| | | 323 | | public static bool IsCancelled(Order o) |
| | | 324 | | { |
| | 18 | 325 | | var client = o.ClientStatus?.Name; |
| | 18 | 326 | | return (o.AppStatus?.Name == "Отменен" && (client == "Отменён" || o.ClientStatus == null)) |
| | 18 | 327 | | || client is "Отменен" or "Отменён"; |
| | | 328 | | } |
| | | 329 | | |
| | | 330 | | public static string? ResolveRowTone(Order o, bool isCancelled) |
| | | 331 | | { |
| | 22 | 332 | | if (isCancelled) return "danger"; |
| | 17 | 333 | | if (o.IsReturnable == true) return "returnable"; |
| | 15 | 334 | | return o.AppStatus?.Name switch |
| | 15 | 335 | | { |
| | 2 | 336 | | "Заказан поставщику" => "success", |
| | 1 | 337 | | "Отгружен поставщиком" => "info", |
| | 1 | 338 | | "Отгружен реализатору" => "warning", |
| | 1 | 339 | | "Заказан реализатору" => "secondary", |
| | 10 | 340 | | _ => null |
| | 15 | 341 | | }; |
| | | 342 | | } |
| | | 343 | | |
| | | 344 | | private static string? Extension(Order o, string key) |
| | 19 | 345 | | => o.OrderExtensions?.FirstOrDefault(e => e.Key == key)?.Value; |
| | | 346 | | |
| | | 347 | | private static string? BuildCancellationReason(Order o) |
| | | 348 | | { |
| | 8 | 349 | | var c = o.LatestOzonPostingCancellation; |
| | 16 | 350 | | if (c == null) return null; |
| | 0 | 351 | | if (string.IsNullOrWhiteSpace(c.ReasonTitle) && string.IsNullOrWhiteSpace(c.ReasonMessage)) return null; |
| | | 352 | | |
| | 0 | 353 | | var text = $"Причина отмены: {c.ReasonTitle ?? c.ReasonMessage}"; |
| | 0 | 354 | | if (c.ReasonId.HasValue) text += $" (ID: {c.ReasonId})"; |
| | 0 | 355 | | if (!string.IsNullOrWhiteSpace(c.ReasonTypeId)) text += $"; инициатор: {c.ReasonTypeId}"; |
| | 0 | 356 | | if (!string.IsNullOrWhiteSpace(c.ReasonMessage) && c.ReasonMessage != c.ReasonTitle) text += $"; {c.ReasonMessag |
| | 0 | 357 | | return text; |
| | | 358 | | } |
| | | 359 | | |
| | | 360 | | private static string? BuildSupplierTooltip(Order o) |
| | | 361 | | { |
| | 16 | 362 | | if (string.IsNullOrWhiteSpace(o.SupplierStatusName)) return null; |
| | 0 | 363 | | var supplierName = o.SupplierStateSupplierName ?? o.SupplierStateProviderKey; |
| | 0 | 364 | | var tooltip = $"{supplierName}: {o.SupplierStatusName}"; |
| | 0 | 365 | | if (!string.IsNullOrWhiteSpace(o.SupplierStateExternalOrderNumber)) |
| | | 366 | | { |
| | 0 | 367 | | tooltip += $" · заказ {o.SupplierStateExternalOrderNumber}"; |
| | | 368 | | } |
| | 0 | 369 | | return tooltip; |
| | | 370 | | } |
| | | 371 | | } |