| | | 1 | | using System.Globalization; |
| | | 2 | | using System.Net; |
| | | 3 | | using System.Text; |
| | | 4 | | using Services.ApiServices._1CApi.Stock; |
| | | 5 | | |
| | | 6 | | namespace OzonOrdersWeb.Areas.Studio2.Helpers; |
| | | 7 | | |
| | | 8 | | /// <summary> |
| | | 9 | | /// Панель «не хватает остатка в 1С» для TempData["ErorrResult"]. |
| | | 10 | | /// Разметка и классы те же, что у предпросмотра на странице транзакции |
| | | 11 | | /// (_OneCStockCheckScript.cshtml), стили — .tx-stock-gap* в TransactionsTable.css. |
| | | 12 | | /// </summary> |
| | | 13 | | public static class OneCStockShortageHtmlBuilder |
| | | 14 | | { |
| | | 15 | | private const int MaxRows = 30; |
| | | 16 | | |
| | | 17 | | /// <param name="statusesChanged"> |
| | | 18 | | /// true — транзакция уже проведена по статусам и пропущено только создание документа 1С |
| | | 19 | | /// (серверный сценарий); false — предупреждение показывается до отправки формы. |
| | | 20 | | /// </param> |
| | | 21 | | public static string Build(StockCheckResult? result, bool statusesChanged = true, bool createdAsDraft = false) |
| | | 22 | | { |
| | 16 | 23 | | var shortages = result?.Blocking; |
| | 16 | 24 | | if (shortages == null || shortages.Count == 0) |
| | 2 | 25 | | return string.Empty; |
| | | 26 | | |
| | 14 | 27 | | var ordered = shortages |
| | 79 | 28 | | .OrderBy(s => s.WarehouseName, StringComparer.OrdinalIgnoreCase) |
| | 79 | 29 | | .ThenBy(s => s.Article, StringComparer.OrdinalIgnoreCase) |
| | 14 | 30 | | .ToList(); |
| | | 31 | | |
| | 14 | 32 | | var sb = new StringBuilder(); |
| | 14 | 33 | | sb.Append("<div class='tx-stock-gap'>"); |
| | | 34 | | |
| | 14 | 35 | | var headline = createdAsDraft |
| | 14 | 36 | | ? $"По {ordered.Count} {PositionsWord(ordered.Count)} создан черновик 1С — проведение требует оприходования" |
| | 14 | 37 | | : statusesChanged |
| | 14 | 38 | | ? $"Статусы заказов изменены, но документ в 1С не создан — не хватает остатка по {ordered.Count} {PositionsW |
| | 14 | 39 | | : $"Не хватает остатка в 1С по {ordered.Count} {PositionsWord(ordered.Count)} — документ не будет создан"; |
| | | 40 | | |
| | 14 | 41 | | sb.Append("<div class='tx-stock-gap-head'>"); |
| | 14 | 42 | | sb.Append("<i class='fas fa-triangle-exclamation'></i>"); |
| | 14 | 43 | | sb.Append($"<span>{headline}</span>"); |
| | 14 | 44 | | sb.Append("</div>"); |
| | | 45 | | |
| | 14 | 46 | | if (createdAsDraft) |
| | 1 | 47 | | sb.Append("<details class='tx-stock-gap-details'><summary>Позиции с нехваткой</summary>"); |
| | | 48 | | |
| | 14 | 49 | | sb.Append("<div class='tx-stock-gap-table-wrap'><table class='tx-stock-gap-table'>"); |
| | 14 | 50 | | sb.Append("<thead><tr>"); |
| | 14 | 51 | | sb.Append("<th>Артикул</th><th>Наименование</th><th>Склад</th>"); |
| | 14 | 52 | | sb.Append("<th class='tx-stock-gap-num'>Нужно</th><th class='tx-stock-gap-num'>Остаток</th>"); |
| | 14 | 53 | | sb.Append("</tr></thead><tbody>"); |
| | | 54 | | |
| | 176 | 55 | | foreach (var row in ordered.Take(MaxRows)) |
| | | 56 | | { |
| | 74 | 57 | | sb.Append("<tr>"); |
| | 74 | 58 | | sb.Append($"<td class='tx-stock-gap-article'>{H(row.Article)}</td>"); |
| | 74 | 59 | | sb.Append($"<td class='tx-stock-gap-name'>{H(row.ProductName)}</td>"); |
| | 74 | 60 | | sb.Append($"<td><span class='tx-stock-gap-wh'>{H(row.WarehouseName)}</span></td>"); |
| | 74 | 61 | | sb.Append($"<td class='tx-stock-gap-num'>{Format(row.Required)}</td>"); |
| | 74 | 62 | | sb.Append($"<td class='tx-stock-gap-num tx-stock-gap-zero'>{Format(row.Available)}</td>"); |
| | 74 | 63 | | sb.Append("</tr>"); |
| | | 64 | | } |
| | | 65 | | |
| | 14 | 66 | | sb.Append("</tbody></table></div>"); |
| | | 67 | | |
| | 14 | 68 | | var hidden = ordered.Count - MaxRows; |
| | 14 | 69 | | if (hidden > 0) |
| | 1 | 70 | | sb.Append($"<div class='tx-stock-gap-hint'>…и ещё {hidden} {PositionsWord(hidden)}</div>"); |
| | | 71 | | |
| | 14 | 72 | | sb.Append("<div class='tx-stock-gap-hint'>"); |
| | 14 | 73 | | sb.Append(createdAsDraft |
| | 14 | 74 | | ? "Черновик содержит позиции с нехваткой. Проведите приход в 1С, затем откройте и проведите документ вручную |
| | 14 | 75 | | : statusesChanged |
| | 14 | 76 | | ? "Проведите приход в 1С и создайте документ вручную либо повторите транзакцию после оприходования." |
| | 14 | 77 | | : "Уменьшите количество, смените склад отгрузки или проведите приход в 1С."); |
| | 14 | 78 | | sb.Append("</div>"); |
| | | 79 | | |
| | 14 | 80 | | if (createdAsDraft) |
| | 1 | 81 | | sb.Append("</details>"); |
| | | 82 | | |
| | 14 | 83 | | sb.Append("</div>"); |
| | 14 | 84 | | return sb.ToString(); |
| | | 85 | | } |
| | | 86 | | |
| | | 87 | | private static string PositionsWord(int count) |
| | | 88 | | { |
| | 15 | 89 | | var lastTwo = count % 100; |
| | 15 | 90 | | if (lastTwo is >= 11 and <= 14) |
| | 1 | 91 | | return "позициям"; |
| | | 92 | | |
| | 14 | 93 | | return count % 10 == 1 ? "позиции" : "позициям"; |
| | | 94 | | } |
| | | 95 | | |
| | 222 | 96 | | private static string H(string? value) => WebUtility.HtmlEncode(value ?? string.Empty); |
| | | 97 | | |
| | | 98 | | // Инвариантная культура: в ru-RU дробный разделитель — запятая, а число попадает в HTML-таблицу |
| | | 99 | | // рядом с числами из 1С, где разделитель точка. |
| | | 100 | | private static string Format(decimal value) => |
| | 148 | 101 | | value == decimal.Truncate(value) |
| | 148 | 102 | | ? ((long)value).ToString(CultureInfo.InvariantCulture) |
| | 148 | 103 | | : value.ToString("0.###", CultureInfo.InvariantCulture); |
| | | 104 | | } |