feat: добавить Sheet1ImportService и Sheet1Group для чтения Лист1

Начало перехода с СПРАВКИ на Лист1 как источник данных:
- Sheet1Group — модель одной группы (строка ИТОГО + все рег. номера группы)
- Sheet1ImportService — читает Лист1, находит ИТОГО-строки, динамически
  собирает все рег. номера по regex-паттерну из всех колонок группы

WIP: TransformService, Sheet3ExpandService и ViewModel ещё не переключены
This commit is contained in:
Dianaka123
2026-04-06 00:21:25 +03:00
parent 059895d1c3
commit addf55e3b2
2 changed files with 185 additions and 0 deletions
@@ -0,0 +1,19 @@
using System.Collections.Generic;
namespace DeclarationAutomatization.Models;
// Одна группа из Листа1: строка ИТОГО + все рег. номера из строк группы
public class Sheet1Group
{
public int SequentialNumber { get; set; }
public string Description { get; set; } = "";
public string TnVed { get; set; } = "";
public string CountryId { get; set; } = "";
public decimal Quantity { get; set; }
public decimal AmountWithVat { get; set; }
public decimal GrossWeight { get; set; }
public decimal NetWeight { get; set; }
// Все уникальные рег. номера из всех строк группы (включая строку ИТОГО)
public List<string> RegNumbers { get; set; } = new();
}
@@ -0,0 +1,166 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
using ClosedXML.Excel;
using DeclarationAutomatization.Models;
namespace DeclarationAutomatization.Services;
public class Sheet1ImportService
{
// Паттерн регистрационного номера: 8цифр/6цифр/буква?7+цифр
private static readonly Regex RegPattern =
new(@"\d{8}/\d{6}/[A-ZА-Я]?\d+", RegexOptions.Compiled);
public List<Sheet1Group> ReadSheet1(string filePath)
{
using var workbook = new XLWorkbook(filePath);
IXLWorksheet? sheet = null;
foreach (var ws in workbook.Worksheets)
{
if (ws.Name.Equals("Лист1", StringComparison.OrdinalIgnoreCase))
{ sheet = ws; break; }
}
if (sheet == null)
throw new InvalidOperationException($"Лист 'Лист1' не найден в файле: {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))
{
var group = BuildGroup(row, groupRows, sequentialNumber++);
if (group != null)
groups.Add(group);
groupRows = new List<IXLRow>();
}
else
{
groupRows.Add(row);
}
}
return groups;
}
private static bool IsItogoRow(IXLRow row)
{
// ИТОГО-строка: ячейка C содержит "ИТОГО" (или B)
foreach (var cell in row.CellsUsed())
{
var val = cell.GetString();
if (val.Contains("ИТОГО", StringComparison.OrdinalIgnoreCase))
return true;
}
return false;
}
private Sheet1Group? BuildGroup(IXLRow itogoRow, List<IXLRow> detailRows, int sequentialNumber)
{
// Берём ТН ВЭД из строки ИТОГО — ищем первую ячейку с 10-значным кодом
string tnVed = FindTnVed(itogoRow);
if (string.IsNullOrWhiteSpace(tnVed))
return null;
// Описание — столбец B строки ИТОГО
string description = itogoRow.Cell(2).GetString().Trim();
if (string.IsNullOrWhiteSpace(description))
description = itogoRow.Cell(3).GetString().Trim(); // иногда в C
// Страна — первый двухбуквенный код в строке
string countryId = FindCountryId(itogoRow);
// Числа: Количество(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);
return new Sheet1Group
{
SequentialNumber = sequentialNumber,
Description = description,
TnVed = tnVed,
CountryId = countryId,
Quantity = qty,
AmountWithVat = amount,
GrossWeight = gross,
NetWeight = net,
RegNumbers = regNumbers,
};
}
private static string FindTnVed(IXLRow row)
{
foreach (var cell in row.CellsUsed())
{
var val = cell.GetString().Trim();
// ТН ВЭД = 10 цифр подряд
if (Regex.IsMatch(val, @"^\d{10}$"))
return val;
}
return "";
}
private static string FindCountryId(IXLRow row)
{
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;
try
{
var val = cell.GetString().Trim().Replace(',', '.');
return decimal.TryParse(val, System.Globalization.NumberStyles.Any,
System.Globalization.CultureInfo.InvariantCulture, out var d) ? d : 0;
}
catch { return 0; }
}
}