feat: единый файл Лист1+СПРАВКА, фильтры, сортировка, живой п/п

- Загрузка одного .xlsx (СПРАВКА + Лист1 в одном файле)
- Рег. номера берутся из СПРАВКА автоматически, группируются по (ТН ВЭД, Страна)
- Отдельный опциональный справочник кодов (CodesImportService)
- Фильтры: Все / Авто / Проверить / Нет кода / Без нетто
- Таблица сразу отсортирована по ТН ВЭД с живым обновлением п/п
- Начальный п/п: поле в UI, автоинкремент после экспорта
- Лист2: рамки, жёлтая строка при >1 рег. номере, итого по кол-ву
- Лист3: ТН ВЭД в колонке B вместо константы 09035
- Красный ErrorMessage под кнопкой загрузки, удалён SpravkaFileEntry
This commit is contained in:
Dianaka123
2026-04-07 11:47:31 +03:00
parent addf55e3b2
commit 697ae44519
13 changed files with 602 additions and 429 deletions
@@ -1,6 +1,5 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
using ClosedXML.Excel;
using DeclarationAutomatization.Models;
@@ -9,9 +8,8 @@ namespace DeclarationAutomatization.Services;
public class Sheet1ImportService
{
// Паттерн регистрационного номера: 8цифр/6цифр/буква?7+цифр
private static readonly Regex RegPattern =
new(@"\d{8}/\d{6}/[A-ZА-Я]?\d+", RegexOptions.Compiled);
private const string SheetName = "Лист1";
private const string ItogoMarker = "ИТОГО";
public List<Sheet1Group> ReadSheet1(string filePath)
{
@@ -20,34 +18,27 @@ public class Sheet1ImportService
IXLWorksheet? sheet = null;
foreach (var ws in workbook.Worksheets)
{
if (ws.Name.Equals("Лист1", StringComparison.OrdinalIgnoreCase))
if (ws.Name.Equals(SheetName, StringComparison.OrdinalIgnoreCase))
{ sheet = ws; break; }
}
if (sheet == null)
throw new InvalidOperationException($"Лист 'Лист1' не найден в файле: {filePath}");
throw new InvalidOperationException($"Лист '{SheetName}' не найден в файле: {filePath}");
var groups = new List<Sheet1Group>();
var groupRows = new List<IXLRow>(); // строки текущей группы (детальные)
int sequentialNumber = 1;
int lastRow = sheet.LastRowUsed()?.RowNumber() ?? 1;
for (int r = 1; r <= lastRow; r++)
{
var row = sheet.Row(r);
if (!IsItogoRow(row)) continue;
if (IsItogoRow(row))
var group = BuildGroup(row, sequentialNumber);
if (group != null)
{
var group = BuildGroup(row, groupRows, sequentialNumber++);
if (group != null)
groups.Add(group);
groupRows = new List<IXLRow>();
}
else
{
groupRows.Add(row);
groups.Add(group);
sequentialNumber++;
}
}
@@ -56,40 +47,29 @@ public class Sheet1ImportService
private static bool IsItogoRow(IXLRow row)
{
// ИТОГО-строка: ячейка C содержит "ИТОГО" (или B)
foreach (var cell in row.CellsUsed())
{
var val = cell.GetString();
if (val.Contains("ИТОГО", StringComparison.OrdinalIgnoreCase))
if (cell.GetString().Contains(ItogoMarker, StringComparison.OrdinalIgnoreCase))
return true;
}
return false;
}
private Sheet1Group? BuildGroup(IXLRow itogoRow, List<IXLRow> detailRows, int sequentialNumber)
private static Sheet1Group? BuildGroup(IXLRow row, int sequentialNumber)
{
// Берём ТН ВЭД из строки ИТОГО — ищем первую ячейку с 10-значным кодом
string tnVed = FindTnVed(itogoRow);
if (string.IsNullOrWhiteSpace(tnVed))
return null;
string tnVed = FindTnVed(row);
if (string.IsNullOrWhiteSpace(tnVed)) return null;
// Описание — столбец B строки ИТОГО
string description = itogoRow.Cell(2).GetString().Trim();
string description = row.Cell(2).GetString().Trim();
if (string.IsNullOrWhiteSpace(description))
description = itogoRow.Cell(3).GetString().Trim(); // иногда в C
description = row.Cell(3).GetString().Trim();
// Страна — первый двухбуквенный код в строке
string countryId = FindCountryId(itogoRow);
string countryId = FindCountryId(row);
// Числа: Количество(I=9), Сумма(J=10), Брутто(K=11), Нетто(L=12)
decimal qty = ParseDecimal(itogoRow.Cell(9));
decimal amount = ParseDecimal(itogoRow.Cell(10));
decimal gross = ParseDecimal(itogoRow.Cell(11));
decimal net = ParseDecimal(itogoRow.Cell(12));
// Собираем все уникальные рег. номера из ВСЕХ строк группы + строки ИТОГО
var allRows = new List<IXLRow>(detailRows) { itogoRow };
var regNumbers = CollectRegNumbers(allRows);
decimal qty = ParseDecimal(row.Cell(9));
decimal amount = ParseDecimal(row.Cell(10));
decimal gross = ParseDecimal(row.Cell(11));
decimal net = ParseDecimal(row.Cell(12));
return new Sheet1Group
{
@@ -101,7 +81,6 @@ public class Sheet1ImportService
AmountWithVat = amount,
GrossWeight = gross,
NetWeight = net,
RegNumbers = regNumbers,
};
}
@@ -110,7 +89,6 @@ public class Sheet1ImportService
foreach (var cell in row.CellsUsed())
{
var val = cell.GetString().Trim();
// ТН ВЭД = 10 цифр подряд
if (Regex.IsMatch(val, @"^\d{10}$"))
return val;
}
@@ -122,36 +100,12 @@ public class Sheet1ImportService
foreach (var cell in row.CellsUsed())
{
var val = cell.GetString().Trim();
// Двухбуквенный код страны (AM, BD, CN, ...)
if (Regex.IsMatch(val, @"^[A-Z]{2}$"))
return val;
}
return "";
}
private List<string> CollectRegNumbers(IEnumerable<IXLRow> rows)
{
var seen = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
var result = new List<string>();
foreach (var row in rows)
{
foreach (var cell in row.CellsUsed())
{
var val = cell.GetString().Trim();
var match = RegPattern.Match(val);
if (match.Success)
{
var regNum = match.Value;
if (seen.Add(regNum))
result.Add(regNum);
}
}
}
return result;
}
private static decimal ParseDecimal(IXLCell cell)
{
if (cell.IsEmpty()) return 0;