059895d1c3
- Чтение СПРАВКИ из Excel (ClosedXML), поддержка нескольких файлов - Группировка по ТН ВЭД: схлопывание строк с суммированием кол-ва/веса/суммы - Автоназначение кодов деклараций по справочнику ТН ВЭД (87 пар) - Цветовая маркировка: зелёный/жёлтый/красный по уровню уверенности - Самообучение: ручной выбор кода сохраняется в tnved_codes.json - Формирование Лист3 с разворачиванием строк по рег. номерам (ключевая функция) - Экспорт Лист2 + Лист3 в Excel
108 lines
3.9 KiB
C#
108 lines
3.9 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using ClosedXML.Excel;
|
|
using DeclarationAutomatization.Models;
|
|
|
|
namespace DeclarationAutomatization.Services;
|
|
|
|
public class ExcelImportService
|
|
{
|
|
// Читает лист СПРАВКА из файла Excel.
|
|
// startingNumber — первый п/п для строк этого файла.
|
|
// Возвращает список SpravkaItem с назначенными п/п.
|
|
public List<SpravkaItem> ReadSpravka(string filePath, int startingNumber)
|
|
{
|
|
var result = new List<SpravkaItem>();
|
|
|
|
using var workbook = new XLWorkbook(filePath);
|
|
|
|
// Ищем лист "СПРАВКА" (регистронезависимо)
|
|
IXLWorksheet? sheet = null;
|
|
foreach (var ws in workbook.Worksheets)
|
|
{
|
|
if (ws.Name.Equals("СПРАВКА", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
sheet = ws;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (sheet == null)
|
|
throw new InvalidOperationException($"Лист 'СПРАВКА' не найден в файле: {filePath}");
|
|
|
|
// Ищем строку заголовка: первая строка, где ячейка B содержит "Наименование"
|
|
int headerRow = FindHeaderRow(sheet);
|
|
if (headerRow < 0)
|
|
throw new InvalidOperationException("Не найдена строка заголовка в листе СПРАВКА (ожидалась ячейка с 'Наименование')");
|
|
|
|
int sequentialNumber = startingNumber;
|
|
|
|
for (int row = headerRow + 1; row <= sheet.LastRowUsed()?.RowNumber(); row++)
|
|
{
|
|
var cellB = sheet.Cell(row, 2).GetString().Trim();
|
|
if (string.IsNullOrWhiteSpace(cellB))
|
|
continue; // пропускаем пустые строки
|
|
|
|
var item = new SpravkaItem
|
|
{
|
|
SequentialNumber = sequentialNumber++,
|
|
Description = cellB,
|
|
ProductCode = sheet.Cell(row, 5).GetString().Trim(),
|
|
TnVed = sheet.Cell(row, 8).GetString().Trim(),
|
|
CountryId = sheet.Cell(row, 11).GetString().Trim(),
|
|
Country = sheet.Cell(row, 12).GetString().Trim(),
|
|
Unit = sheet.Cell(row, 13).GetString().Trim(),
|
|
Quantity = ParseDecimal(sheet.Cell(row, 14)),
|
|
AmountWithVat = ParseDecimal(sheet.Cell(row, 15)),
|
|
GrossWeight = ParseDecimal(sheet.Cell(row, 16)),
|
|
NetWeight = ParseDecimal(sheet.Cell(row, 17)),
|
|
RegNumber = sheet.Cell(row, 18).GetString().Trim(),
|
|
RegDate = FormatDate(sheet.Cell(row, 19)),
|
|
};
|
|
|
|
result.Add(item);
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
private static int FindHeaderRow(IXLWorksheet sheet)
|
|
{
|
|
int lastRow = Math.Min(sheet.LastRowUsed()?.RowNumber() ?? 200, 200);
|
|
for (int r = 1; r <= lastRow; r++)
|
|
{
|
|
var val = sheet.Cell(r, 2).GetString();
|
|
if (val.Contains("Наименование", StringComparison.OrdinalIgnoreCase))
|
|
return r;
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
private static decimal ParseDecimal(IXLCell cell)
|
|
{
|
|
try
|
|
{
|
|
if (cell.IsEmpty()) return 0;
|
|
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;
|
|
}
|
|
}
|
|
|
|
private static string FormatDate(IXLCell cell)
|
|
{
|
|
if (cell.IsEmpty()) return "";
|
|
try
|
|
{
|
|
if (cell.DataType == XLDataType.DateTime)
|
|
return cell.GetDateTime().ToString("dd.MM.yyyy");
|
|
}
|
|
catch { }
|
|
return cell.GetString().Trim();
|
|
}
|
|
}
|