feat: добавить Sheet1ImportService и Sheet1Group для чтения Лист1
Начало перехода с СПРАВКИ на Лист1 как источник данных: - Sheet1Group — модель одной группы (строка ИТОГО + все рег. номера группы) - Sheet1ImportService — читает Лист1, находит ИТОГО-строки, динамически собирает все рег. номера по regex-паттерну из всех колонок группы WIP: TransformService, Sheet3ExpandService и ViewModel ещё не переключены
This commit is contained in:
@@ -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; }
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user