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