| | | 1 | | using OzonDomains.Models; |
| | | 2 | | using OzonOrdersWeb.Areas.Studio2.ViewModels.OrderHistory; |
| | | 3 | | using OzonOrdersWeb.TransactionServices; |
| | | 4 | | using Services.OneCServices; |
| | | 5 | | using Services.SupplierStatusServices; |
| | | 6 | | |
| | | 7 | | namespace OzonOrdersWeb.WebServices.OrderHistory; |
| | | 8 | | |
| | | 9 | | /// <summary>Смена статуса у API-поставщика в строке истории (цвета статусов — данные справочника).</summary> |
| | | 10 | | public sealed record OrderHistorySupplierStatusCell( |
| | | 11 | | string ProviderName, string? LogoUrl, string? FromName, string? FromColor, string ToName, string? ToColor, |
| | | 12 | | string? Stage, string? Source, string? Comment); |
| | | 13 | | |
| | | 14 | | /// <summary>Размещение позиции в корзине API-поставщика в строке истории.</summary> |
| | | 15 | | public sealed record OrderHistoryBasketCell( |
| | | 16 | | string Tone, string Icon, string Text, string? LogoUrl, string? SupplierOrderNumber, string? ApiItemId, |
| | | 17 | | string? Product, string? Article, string? Quantity, string? ShipmentNumber, string? ClientName, |
| | | 18 | | string? WarehouseName, string? Buyer, string? DeliveryAddress, string? Error, string? CartUrl); |
| | | 19 | | |
| | | 20 | | /// <summary>Системная проверка заказа у поставщика (повторное чтение API после проведения) в строке истории.</summary> |
| | | 21 | | public sealed record OrderHistoryVerificationCell( |
| | | 22 | | string Tone, string Icon, string Title, string ProviderName, string? LogoUrl, string? Stage, string Summary, |
| | | 23 | | string? SupplierOrderNumber, string? SupplierStatus, IReadOnlyList<string> Checks, DateTime? VerifiedAt); |
| | | 24 | | |
| | | 25 | | /// <summary>Событие создания или выдачи PDF-этикетки в общей ленте заказа.</summary> |
| | 5 | 26 | | public sealed record OrderHistoryLabelPrintCell(string FileName, string Source); |
| | | 27 | | |
| | | 28 | | /// <summary> |
| | | 29 | | /// Строка грида «История изменений»: одно изменённое поле. Строки одной операции (OperationId, минута, автор) |
| | | 30 | | /// имеют общий <see cref="OpKey"/> — по нему грид объединяет ячейки операции (rowSpan). |
| | | 31 | | /// </summary> |
| | | 32 | | public sealed record OrderHistoryChangeRow( |
| | | 33 | | int Id, |
| | | 34 | | DateTime ChangedAt, |
| | | 35 | | string OpKey, |
| | | 36 | | int OpIndex, |
| | | 37 | | DateTime OpTime, |
| | | 38 | | string User, |
| | | 39 | | string? UserName, |
| | | 40 | | string? UserAvatar, |
| | | 41 | | string UserKind, |
| | | 42 | | string Field, |
| | | 43 | | string Kind, |
| | | 44 | | string? Old, |
| | | 45 | | string? New, |
| | | 46 | | OrderHistorySupplierStatusCell? SupplierStatus, |
| | | 47 | | OrderHistoryBasketCell? Basket, |
| | | 48 | | OrderHistoryVerificationCell? Verification, |
| | | 49 | | long? SnapshotId, |
| | | 50 | | int OneCCount, |
| | | 51 | | bool OneCOk, |
| | | 52 | | long PointTicks, |
| | | 53 | | string SearchText, |
| | | 54 | | OrderHistoryLabelPrintCell? LabelPrint = null); |
| | | 55 | | |
| | | 56 | | /// <summary>Операция истории: проведения 1С, показанные по кнопке в строке (разметка — партиал заголовка 1С).</summary> |
| | | 57 | | public sealed record OrderHistoryOperation(int Index, string OpKey, IReadOnlyList<OrderHistoryOneCPostingViewModel> OneC |
| | | 58 | | |
| | | 59 | | public sealed record OrderHistoryChangeGrid(IReadOnlyList<OrderHistoryChangeRow> Rows, IReadOnlyList<OrderHistoryOperati |
| | | 60 | | |
| | | 61 | | /// <summary> |
| | | 62 | | /// Строки вкладок истории заказа для гридов AG Grid. Раньше эти вычисления жили в GetOrderHistory.cshtml — |
| | | 63 | | /// теперь по ним работают фильтры и сортировка грида. |
| | | 64 | | /// </summary> |
| | | 65 | | public static class OrderHistoryGridRows |
| | | 66 | | { |
| | | 67 | | public const string UserKindUser = "user"; |
| | | 68 | | public const string UserKindSystem = "system"; |
| | | 69 | | public const string UserKindRollback = "rollback"; |
| | | 70 | | |
| | | 71 | | public static OrderHistoryChangeGrid BuildChanges(OrderHistoryViewModel model) |
| | | 72 | | { |
| | | 73 | | var rows = new List<OrderHistoryChangeRow>(); |
| | | 74 | | var operations = new List<OrderHistoryOperation>(); |
| | | 75 | | var all = model.Histories.Values.SelectMany(x => x).ToList(); |
| | | 76 | | |
| | | 77 | | var groups = all |
| | | 78 | | .GroupBy(h => new |
| | | 79 | | { |
| | | 80 | | OperationId = h.OperationId?.ToString(), |
| | | 81 | | RoundedTime = new DateTime(h.ChangedAt.Ticks - h.ChangedAt.Ticks % TimeSpan.TicksPerMinute, h.ChangedAt. |
| | | 82 | | h.ChangedBy |
| | | 83 | | }) |
| | | 84 | | .OrderByDescending(g => g.Max(h => h.ChangedAt)) |
| | | 85 | | .ToList(); |
| | | 86 | | |
| | | 87 | | foreach (var group in groups) |
| | | 88 | | { |
| | | 89 | | var changes = group.OrderBy(h => h.ColumnDisplayName ?? string.Empty).ToList(); |
| | | 90 | | var opTime = group.Max(h => h.ChangedAt); |
| | | 91 | | var opIndex = operations.Count; |
| | | 92 | | var opKey = $"{group.Key.OperationId}|{group.Key.RoundedTime.Ticks}|{group.Key.ChangedBy}"; |
| | | 93 | | var changedBy = group.Key.ChangedBy ?? string.Empty; |
| | | 94 | | var isRollback = changedBy.StartsWith("rollback:", StringComparison.OrdinalIgnoreCase); |
| | | 95 | | var displayUser = isRollback ? changedBy["rollback:".Length..] : changedBy; |
| | | 96 | | var isSystem = !isRollback && (string.IsNullOrWhiteSpace(changedBy) |
| | | 97 | | || changedBy.Contains("system", StringComparison.OrdinalIgnoreCase) |
| | | 98 | | || changedBy.Contains("auto", StringComparison.OrdinalIgnoreCase) |
| | | 99 | | || changedBy.Contains("robot", StringComparison.OrdinalIgnoreCase)); |
| | | 100 | | model.UserProfiles.TryGetValue(displayUser, out var profile); |
| | | 101 | | var userName = profile?.DisplayName is { } name && !string.Equals(name, profile.LoginOrEmail, StringComparis |
| | | 102 | | var login = profile?.LoginOrEmail ?? displayUser; |
| | | 103 | | |
| | | 104 | | var postings = OneCPostingsForGroup(model, changes, group.Key.ChangedBy, opTime); |
| | | 105 | | operations.Add(new OrderHistoryOperation(opIndex, opKey, postings)); |
| | | 106 | | long? snapshotId = group.Key.OperationId is { } opId && model.CalculationSnapshotIdsByOperationId.TryGetValu |
| | | 107 | | |
| | | 108 | | foreach (var change in changes) |
| | | 109 | | { |
| | | 110 | | var (kind, field, oldValue, newValue, status, basket, verification, filterText) = DescribeChange(model, |
| | | 111 | | rows.Add(new OrderHistoryChangeRow( |
| | | 112 | | change.Id, |
| | | 113 | | change.ChangedAt, |
| | | 114 | | opKey, |
| | | 115 | | opIndex, |
| | | 116 | | opTime, |
| | | 117 | | string.IsNullOrWhiteSpace(login) ? "Неизвестно" : login, |
| | | 118 | | userName, |
| | | 119 | | profile?.AvatarUrl, |
| | | 120 | | isRollback ? UserKindRollback : isSystem ? UserKindSystem : UserKindUser, |
| | | 121 | | field, |
| | | 122 | | kind, |
| | | 123 | | oldValue, |
| | | 124 | | newValue, |
| | | 125 | | status, |
| | | 126 | | basket, |
| | | 127 | | verification, |
| | | 128 | | snapshotId, |
| | | 129 | | postings.Count, |
| | | 130 | | postings.All(p => p.Success), |
| | | 131 | | opTime.Ticks, |
| | | 132 | | filterText)); |
| | | 133 | | } |
| | | 134 | | } |
| | | 135 | | |
| | | 136 | | foreach (var label in model.LabelPrints.OrderByDescending(x => x.OccurredAtUtc).ThenByDescending(x => x.Id)) |
| | | 137 | | { |
| | | 138 | | var opIndex = operations.Count; |
| | | 139 | | var opKey = $"label|{label.Id}"; |
| | | 140 | | var source = label.UsesFileDate |
| | | 141 | | ? "Дата файла" |
| | | 142 | | : label.Source == LabelPrintSource.PartnerApi ? "Partner API" : "Studio2"; |
| | | 143 | | var actor = string.IsNullOrWhiteSpace(label.ActorName) ? "Неизвестно" : label.ActorName; |
| | | 144 | | var labelCell = new OrderHistoryLabelPrintCell(label.FileName, source); |
| | | 145 | | operations.Add(new OrderHistoryOperation(opIndex, opKey, [])); |
| | | 146 | | rows.Add(new OrderHistoryChangeRow( |
| | | 147 | | -(opIndex + 1), |
| | | 148 | | label.OccurredAtUtc, |
| | | 149 | | opKey, |
| | | 150 | | opIndex, |
| | | 151 | | label.OccurredAtUtc, |
| | | 152 | | actor, |
| | | 153 | | null, |
| | | 154 | | null, |
| | | 155 | | UserKindUser, |
| | | 156 | | "Печать этикетки", |
| | | 157 | | "labelPrint", |
| | | 158 | | null, |
| | | 159 | | source, |
| | | 160 | | null, |
| | | 161 | | null, |
| | | 162 | | null, |
| | | 163 | | null, |
| | | 164 | | 0, |
| | | 165 | | true, |
| | | 166 | | label.OccurredAtUtc.Ticks, |
| | | 167 | | string.Join(' ', "Печать этикетки", label.FileName, source, actor), |
| | | 168 | | labelCell)); |
| | | 169 | | } |
| | | 170 | | |
| | | 171 | | return new OrderHistoryChangeGrid(rows.OrderByDescending(x => x.OpTime).ThenBy(x => x.OpIndex).ToList(), operati |
| | | 172 | | } |
| | | 173 | | |
| | | 174 | | private static (string Kind, string Field, string? Old, string? New, OrderHistorySupplierStatusCell? Status, |
| | | 175 | | OrderHistoryBasketCell? Basket, OrderHistoryVerificationCell? Verification, string SearchText) DescribeChange(Or |
| | | 176 | | { |
| | | 177 | | if (change.ColumnName == OrderSupplierStateService.HistoryColumnName) |
| | | 178 | | { |
| | | 179 | | model.SupplierStatusTimelineByHistoryId.TryGetValue(change.Id, out var timeline); |
| | | 180 | | var providerKey = timeline?.ProviderKey; |
| | | 181 | | var cell = new OrderHistorySupplierStatusCell( |
| | | 182 | | providerKey is null ? change.ColumnDisplayName ?? "Статус поставщика" : SupplierProviderName(providerKey |
| | | 183 | | providerKey is null ? null : SupplierProviderLogoUrl(providerKey), |
| | | 184 | | string.IsNullOrWhiteSpace(timeline?.FromStatusName ?? change.OldValue) ? null : timeline?.FromStatusName |
| | | 185 | | timeline?.FromStatusColor, |
| | | 186 | | timeline?.ToStatusName ?? change.NewValue ?? "Статус не указан", |
| | | 187 | | timeline?.ToStatusColor, |
| | | 188 | | timeline is null ? null : $"{SupplierStageName(timeline.FromStage)} → {SupplierStageName(timeline.ToStag |
| | | 189 | | timeline is null ? null : SupplierSourceName(timeline.Source), |
| | | 190 | | timeline?.Comment); |
| | | 191 | | var field = change.ColumnDisplayName ?? "Статус поставщика"; |
| | | 192 | | return ("supplierStatus", field, cell.FromName, cell.ToName, cell, null, null, |
| | | 193 | | string.Join(' ', field, cell.ProviderName, cell.FromName, cell.ToName, cell.Comment)); |
| | | 194 | | } |
| | | 195 | | |
| | | 196 | | if (change.ColumnName == ApiSupplierBasketHistoryEntry.HistoryColumnName) |
| | | 197 | | { |
| | | 198 | | var field = change.ColumnDisplayName ?? "Корзина API-поставщика"; |
| | | 199 | | var entry = ApiSupplierBasketHistoryEntry.TryParse(change.NewValue); |
| | | 200 | | if (entry is null) return ("basket", field, null, null, null, null, null, field); |
| | | 201 | | var (tone, icon, text) = DescribeBasketStatus(entry); |
| | | 202 | | var quantity = entry.Price.HasValue || entry.Quantity.HasValue |
| | | 203 | | ? (entry.Quantity?.ToString("0.##") ?? "1") + (entry.Price.HasValue ? $" × {entry.Price.Value:0.##} {ent |
| | | 204 | | : null; |
| | | 205 | | var product = string.IsNullOrWhiteSpace(entry.ProductName) && string.IsNullOrWhiteSpace(entry.Article) |
| | | 206 | | ? null |
| | | 207 | | : string.IsNullOrWhiteSpace(entry.ProductName) ? "Без названия" : entry.ProductName; |
| | | 208 | | var cell = new OrderHistoryBasketCell(tone, icon, text, NullIfEmpty(entry.LogoUrl), NullIfEmpty(entry.Suppli |
| | | 209 | | NullIfEmpty(entry.ApiItemId), product, NullIfEmpty(entry.Article), quantity, NullIfEmpty(entry.ShipmentN |
| | | 210 | | NullIfEmpty(entry.ClientName), NullIfEmpty(entry.WarehouseName), NullIfEmpty(entry.BuyerKunnr), |
| | | 211 | | NullIfEmpty(entry.DeliveryAddressName), NullIfEmpty(entry.ErrorMessage), NullIfEmpty(entry.SupplierCartU |
| | | 212 | | return ("basket", field, null, text, null, cell, null, entry.BuildFilterText()); |
| | | 213 | | } |
| | | 214 | | |
| | | 215 | | if (change.ColumnName == SupplierVerificationHistoryEntry.HistoryColumnName) |
| | | 216 | | { |
| | | 217 | | var field = change.ColumnDisplayName ?? "Проверка заказа у поставщика"; |
| | | 218 | | var entry = SupplierVerificationHistoryEntry.TryParse(change.NewValue); |
| | | 219 | | if (entry is null) return ("verification", field, null, null, null, null, null, field); |
| | | 220 | | var (tone, icon) = DescribeVerificationStatus(entry.Status); |
| | | 221 | | var title = SupplierVerificationStatus.Title(entry.Status); |
| | | 222 | | var cell = new OrderHistoryVerificationCell(tone, icon, title, entry.SupplierDisplayName, |
| | | 223 | | SupplierProviderLogoUrl(entry.ProviderKey), SupplierStageName(entry.Stage), entry.Summary, |
| | | 224 | | NullIfEmpty(entry.SupplierOrderNumber), NullIfEmpty(entry.SupplierStatusName), |
| | | 225 | | entry.Checks ?? Array.Empty<string>(), entry.VerifiedAt); |
| | | 226 | | return ("verification", field, null, title, null, null, cell, entry.BuildFilterText()); |
| | | 227 | | } |
| | | 228 | | |
| | | 229 | | var display = change.ColumnDisplayName ?? "Не указано"; |
| | | 230 | | var oldValue = string.IsNullOrEmpty(change.OldValue) ? null : change.FormattedOldValue; |
| | | 231 | | var newValue = string.IsNullOrEmpty(change.NewValue) ? null : change.FormattedNewValue; |
| | | 232 | | return ("change", display, oldValue, newValue, null, null, null, string.Join(' ', display, change.OldValue, chan |
| | | 233 | | } |
| | | 234 | | |
| | | 235 | | /// <summary>Проведения 1С операции: по привязке к строкам истории, ключу пакета или времени (±10 мин, с поправкой н |
| | | 236 | | private static List<OrderHistoryOneCPostingViewModel> OneCPostingsForGroup( |
| | | 237 | | OrderHistoryViewModel model, IReadOnlyCollection<OzonDomains.Models.OrderHistory> changes, string? changedBy, Da |
| | | 238 | | { |
| | | 239 | | if (model.OneCPostings.Count == 0) return []; |
| | | 240 | | var historyIds = changes.Select(c => c.Id).ToHashSet(); |
| | | 241 | | var batchKey = OneCPostingSummaryBuilder.BuildHistoryBatchKey(model.OrderId, opTime, changedBy); |
| | | 242 | | return model.OneCPostings |
| | | 243 | | .Where(p => p.LinkedHistoryIds.Any(historyIds.Contains) |
| | | 244 | | || (!string.IsNullOrEmpty(p.HistoryBatchKey) && p.HistoryBatchKey == batchKey) |
| | | 245 | | || MinTimeDiffMinutes(p.PostedAt, opTime) <= 10) |
| | | 246 | | .DistinctBy(p => p.Id) |
| | | 247 | | .OrderByDescending(p => p.PostedAt) |
| | | 248 | | .ToList(); |
| | | 249 | | } |
| | | 250 | | |
| | | 251 | | public static double MinTimeDiffMinutes(DateTime a, DateTime b) |
| | | 252 | | { |
| | | 253 | | var direct = Math.Abs((a - b).TotalMinutes); |
| | | 254 | | var plus3 = Math.Abs((a.AddHours(3) - b).TotalMinutes); |
| | | 255 | | var minus3 = Math.Abs((a.AddHours(-3) - b).TotalMinutes); |
| | | 256 | | return Math.Min(direct, Math.Min(plus3, minus3)); |
| | | 257 | | } |
| | | 258 | | |
| | | 259 | | private static string? NullIfEmpty(string? value) => string.IsNullOrWhiteSpace(value) ? null : value; |
| | | 260 | | |
| | | 261 | | private static string BasketSupplierName(ApiSupplierBasketHistoryEntry entry) => entry.ProviderKey switch |
| | | 262 | | { |
| | | 263 | | "MlAuto" => "ML-Auto BY", |
| | | 264 | | "MlAutoRu" => "ML-Auto RU", |
| | | 265 | | _ => entry.SupplierDisplayName |
| | | 266 | | }; |
| | | 267 | | |
| | | 268 | | public static (string Tone, string Icon, string Text) DescribeBasketStatus(ApiSupplierBasketHistoryEntry entry) => e |
| | | 269 | | { |
| | | 270 | | { CartStatus: "Ordered" } => ("success", "fa-circle-check", $"Заказ размещён у поставщика {BasketSupplierName(en |
| | | 271 | | { CartStatus: "Failed" } or { Success: false } => ("danger", "fa-circle-xmark", $"Ошибка добавления в корзину {B |
| | | 272 | | { CartStatus: "Pending" } => ("info", "fa-hourglass-half", $"В корзине {BasketSupplierName(entry)} — ждёт оформл |
| | | 273 | | _ => ("success", "fa-cart-shopping", $"Добавлено в корзину {BasketSupplierName(entry)}") |
| | | 274 | | }; |
| | | 275 | | |
| | | 276 | | public static (string Tone, string Icon) DescribeVerificationStatus(string status) => status switch |
| | | 277 | | { |
| | | 278 | | SupplierVerificationStatus.Confirmed => ("success", "fa-circle-check"), |
| | | 279 | | SupplierVerificationStatus.NotVerifiable => ("muted", "fa-circle-question"), |
| | | 280 | | SupplierVerificationStatus.ApiError => ("warning", "fa-plug-circle-exclamation"), |
| | | 281 | | _ => ("danger", "fa-circle-xmark") |
| | | 282 | | }; |
| | | 283 | | |
| | | 284 | | public static string SupplierProviderName(string providerKey) => providerKey switch |
| | | 285 | | { |
| | | 286 | | "Armtek" => "Армтек RU", |
| | | 287 | | "ArmtekBy" => "Армтек РБ", |
| | | 288 | | "Avd" => "АВД", |
| | | 289 | | "MlAuto" => "ML-Auto РБ", |
| | | 290 | | "MlAutoRu" => "ML-Auto RU", |
| | | 291 | | "MikadoMskHod20" => "Микадо МскХод20", |
| | | 292 | | "ZZapMoscow" => "ZZap Москва", |
| | | 293 | | "Japarts" => "Japarts", |
| | | 294 | | _ => providerKey |
| | | 295 | | }; |
| | | 296 | | |
| | | 297 | | public static string? SupplierProviderLogoUrl(string providerKey) => providerKey switch |
| | | 298 | | { |
| | | 299 | | "Armtek" or "ArmtekBy" => "/images/armtek-logo.svg", |
| | | 300 | | "Avd" => "/images/avd-logo.svg", |
| | | 301 | | "MlAuto" => "https://www.ml-auto.by/media/files/settings/inside-placeholder-logo-mobile.png", |
| | | 302 | | "MlAutoRu" => "https://ml-auto.ru/media/files/settings/inside-placeholder-logo-mobile.png", |
| | | 303 | | "MikadoMskHod20" => "https://mikado-parts.ru/img/MikadoLogo.svg", |
| | | 304 | | "ZZapMoscow" => "/images/zzap-logo.svg", |
| | | 305 | | "Japarts" => "/images/japarts-logo.jpg", |
| | | 306 | | _ => null |
| | | 307 | | }; |
| | | 308 | | |
| | | 309 | | public static string SupplierStageName(string? stage) => stage switch |
| | | 310 | | { |
| | | 311 | | "Cart" => "Корзина", |
| | | 312 | | "Ordered" => "Заказ оформлен", |
| | | 313 | | "Closed" => "Завершён", |
| | | 314 | | _ => "—" |
| | | 315 | | }; |
| | | 316 | | |
| | | 317 | | public static string SupplierSourceName(string source) => source switch |
| | | 318 | | { |
| | | 319 | | "Checkout" => "Действие пользователя", |
| | | 320 | | "Job" => "Синхронизация с поставщиком", |
| | | 321 | | "Manual" => "Изменено вручную", |
| | | 322 | | _ => source |
| | | 323 | | }; |
| | | 324 | | |
| | | 325 | | /// <summary>Статус позиции акта возврата: тон бейджа темы, иконка, подпись.</summary> |
| | | 326 | | public static (string Tone, string Icon, string Text) DescribeHandoverStatus(string? status) => status?.ToLowerInvar |
| | | 327 | | { |
| | | 328 | | "accepted" => ("success", "fa-circle-check", "Принято"), |
| | | 329 | | "rejected" => ("danger", "fa-circle-xmark", "Отказ"), |
| | | 330 | | "pending" => ("info", "fa-hourglass-half", "Ожидает"), |
| | | 331 | | _ => ("muted", "fa-question-circle", status ?? "—") |
| | | 332 | | }; |
| | | 333 | | |
| | | 334 | | /// <summary>Подробности позиции возврата (подпись → значение) — то, что раньше шло строкой под позицией.</summary> |
| | | 335 | | public static List<KeyValuePair<string, string>> HandoverDetails(OrderHistoryHandoverItemViewModel h) |
| | | 336 | | { |
| | | 337 | | var list = new List<KeyValuePair<string, string>>(); |
| | | 338 | | void Add(string label, string? value) |
| | | 339 | | { |
| | | 340 | | if (!string.IsNullOrWhiteSpace(value)) list.Add(new(label, value)); |
| | | 341 | | } |
| | | 342 | | |
| | | 343 | | Add("Комментарий", h.Comment); |
| | | 344 | | Add("Статус Ozon", h.OzonStatusName); |
| | | 345 | | Add("ID возврата", h.ReturnId?.ToString()); |
| | | 346 | | Add("Коробка", h.BoxBarcode); |
| | | 347 | | Add("Причина", h.Reason is null ? null : h.Reason + (h.ReasonIsDefect ? " (причина брака)" : string.Empty)); |
| | | 348 | | Add("Комментарий клиента", h.ClientComment); |
| | | 349 | | Add("Причина отказа", h.RejectionReason); |
| | | 350 | | Add("Комментарий отказа", h.RejectionComment); |
| | | 351 | | Add("Трек", h.RuPostTracking); |
| | | 352 | | Add("Номер возврата", h.ReturnNumber); |
| | | 353 | | Add("Номер заказа Ozon", h.OrderNumber); |
| | | 354 | | Add("Способ возврата", h.ClientReturnMethod); |
| | | 355 | | Add("Склад Ozon", h.WarehouseId?.ToString()); |
| | | 356 | | Add("Фото", h.PhotosError); |
| | | 357 | | return list; |
| | | 358 | | } |
| | | 359 | | } |