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
+2
View File
@@ -22,7 +22,9 @@ public partial class App : Application
private static void ConfigureServices(IServiceCollection services)
{
services.AddSingleton<Sheet1ImportService>();
services.AddSingleton<ExcelImportService>();
services.AddSingleton<CodesImportService>();
services.AddSingleton<TransformService>();
services.AddSingleton<RulesPersistenceService>();
services.AddSingleton<CodeLookupService>();
@@ -3,9 +3,11 @@ using CommunityToolkit.Mvvm.ComponentModel;
namespace DeclarationAutomatization.Models;
public record RegEntry(string Number, string Date);
public partial class DeclarationItem : ObservableObject
{
public int SequentialNumber { get; init; }
public int SequentialNumber { get; set; }
public string Description { get; init; } = "";
public string TnVed { get; init; } = "";
public string CountryId { get; init; } = "";
@@ -13,8 +15,9 @@ public partial class DeclarationItem : ObservableObject
public decimal AmountWithVat { get; init; }
public decimal GrossWeight { get; init; }
public decimal NetWeight { get; init; }
public string RegNumber { get; init; } = "";
public string RegDate { get; init; } = "";
// Все рег. номера из СПРАВКИ для данного ТН ВЭД
public List<RegEntry> RegEntries { get; set; } = new();
[ObservableProperty]
private string _declarationCode = "";
@@ -1,8 +1,6 @@
using System.Collections.Generic;
namespace DeclarationAutomatization.Models;
// Одна группа из Листа1: строка ИТОГО + все рег. номера из строк группы
// Одна группа из Листа1: строка ИТОГО (рег. номера берём из СПРАВКИ)
public class Sheet1Group
{
public int SequentialNumber { get; set; }
@@ -13,7 +11,4 @@ public class Sheet1Group
public decimal AmountWithVat { get; set; }
public decimal GrossWeight { get; set; }
public decimal NetWeight { get; set; }
// Все уникальные рег. номера из всех строк группы (включая строку ИТОГО)
public List<string> RegNumbers { get; set; } = new();
}
@@ -3,7 +3,7 @@ namespace DeclarationAutomatization.Models;
public class Sheet3Row
{
public int SequentialNumber { get; init; }
public string ClassifierCode { get; init; } = "09035";
public string TnVed { get; init; } = "";
public string RegNumber { get; init; } = "";
public string RegDate { get; init; } = "";
}
@@ -1,15 +0,0 @@
using CommunityToolkit.Mvvm.ComponentModel;
namespace DeclarationAutomatization.Models;
// Запись о загруженном файле СПРАВКИ с настройкой начального п/п
public partial class SpravkaFileEntry : ObservableObject
{
[ObservableProperty]
private string _filePath = "";
[ObservableProperty]
private int _startingNumber = 1;
public string FileName => System.IO.Path.GetFileName(FilePath);
}
@@ -15,9 +15,15 @@ public class CodeLookupService
_entries = _persistence.Load();
}
public void Reload() => _entries = _persistence.Load();
// Загружает коды из внешнего xlsx-справочника (заменяет текущие записи)
public void LoadFromEntries(List<CodeLookupEntry> entries)
{
_entries = entries;
}
// Назначает декларационные коды всем позициям в списке.
public bool HasEntries => _entries.Count > 0;
// Назначает декларационные коды всем позициям в списке
public void AssignCodes(IEnumerable<DeclarationItem> items)
{
foreach (var item in items)
@@ -46,18 +52,8 @@ public class CodeLookupService
// Несколько кодов — неоднозначность
item.CandidateCodes = new List<string>(entry.Codes);
// Эвристика: если у позиции есть рег. номер — предпочесть 9000
if (!string.IsNullOrEmpty(item.RegNumber) && entry.Codes.Contains("9000"))
{
item.DeclarationCode = "9000";
item.Confidence = ConfidenceLevel.Auto;
}
else
{
item.DeclarationCode = entry.Codes[0]; // первый = наиболее вероятный
item.Confidence = ConfidenceLevel.Review;
}
item.DeclarationCode = entry.Codes[0];
item.Confidence = ConfidenceLevel.Review;
}
// Фиксирует ручной выбор кода декларантом
@@ -0,0 +1,48 @@
using System;
using System.Collections.Generic;
using ClosedXML.Excel;
using DeclarationAutomatization.Models;
namespace DeclarationAutomatization.Services;
// Читает справочник ТН ВЭД → код декларации из внешнего xlsx-файла.
// Ожидаемый формат: лист "новые", колонка B = ТН ВЭД, колонка C = код.
public class CodesImportService
{
private const string SheetName = "новые";
public List<CodeLookupEntry> ReadEntries(string filePath)
{
using var workbook = new XLWorkbook(filePath);
IXLWorksheet? sheet = null;
foreach (var ws in workbook.Worksheets)
{
if (ws.Name.Equals(SheetName, StringComparison.OrdinalIgnoreCase))
{ sheet = ws; break; }
}
if (sheet == null)
throw new InvalidOperationException($"Лист '{SheetName}' не найден в файле: {filePath}");
var result = new List<CodeLookupEntry>();
int lastRow = sheet.LastRowUsed()?.RowNumber() ?? 1;
for (int r = 1; r <= lastRow; r++)
{
var tnVed = sheet.Cell(r, 2).GetString().Trim();
var code = sheet.Cell(r, 3).GetString().Trim();
if (tnVed.Length != 10 || string.IsNullOrWhiteSpace(code))
continue;
result.Add(new CodeLookupEntry
{
TnVed = tnVed,
Codes = new List<string> { code },
});
}
return result;
}
}
@@ -1,5 +1,4 @@
using System.Collections.Generic;
using System.Drawing;
using ClosedXML.Excel;
using DeclarationAutomatization.Models;
@@ -24,7 +23,6 @@ public class ExcelExportService
{
var ws = workbook.Worksheets.Add("Лист2");
// Заголовки
var headers = new[]
{
"п/п",
@@ -37,7 +35,7 @@ public class ExcelExportService
"Масса нетто, КГ",
"Код декларации",
"Регистрационный номер",
"Дата регистрационного номера"
"Дата регистрационного номера",
};
for (int c = 0; c < headers.Length; c++)
@@ -49,7 +47,7 @@ public class ExcelExportService
}
int row = 2;
decimal totalAmount = 0, totalGross = 0, totalNet = 0;
decimal totalAmount = 0, totalGross = 0, totalNet = 0, totalQty = 0;
foreach (var item in items)
{
@@ -62,36 +60,37 @@ public class ExcelExportService
ws.Cell(row, 7).Value = item.GrossWeight;
ws.Cell(row, 8).Value = item.NetWeight;
ws.Cell(row, 9).Value = item.DeclarationCode;
ws.Cell(row, 10).Value = item.RegNumber;
ws.Cell(row, 11).Value = item.RegDate;
// Цветовая маркировка строк по уровню уверенности
var rowStyle = ws.Row(row).Style;
var fillColor = item.Confidence switch
if (item.RegEntries.Count == 1)
{
ConfidenceLevel.Auto => XLColor.FromArgb(198, 239, 206), // светло-зелёный
ConfidenceLevel.Review => XLColor.FromArgb(255, 235, 156), // светло-жёлтый
ConfidenceLevel.Missing => XLColor.FromArgb(255, 199, 206), // светло-красный
_ => XLColor.White
};
ws.Row(row).Style.Fill.BackgroundColor = fillColor;
ws.Cell(row, 10).Value = item.RegEntries[0].Number;
ws.Cell(row, 11).Value = item.RegEntries[0].Date;
}
else if (item.RegEntries.Count > 1)
{
// Несколько партий — жёлтая строка целиком, подробности в Лист3
ws.Row(row).Style.Fill.BackgroundColor = XLColor.Yellow;
}
totalAmount += item.AmountWithVat;
totalGross += item.GrossWeight;
totalNet += item.NetWeight;
totalGross += item.GrossWeight;
totalNet += item.NetWeight;
totalQty += item.Quantity;
row++;
}
// Итоговая строка
ws.Cell(row, 5).Value = "ИТОГО:";
ws.Cell(row, 5).Style.Font.Bold = true;
ws.Cell(row, 2).Value = "ИТОГО:";
ws.Cell(row, 5).Value = totalQty;
ws.Cell(row, 6).Value = totalAmount;
ws.Cell(row, 7).Value = totalGross;
ws.Cell(row, 8).Value = totalNet;
ws.Row(row).Style.Font.Bold = true;
ApplyBorders(ws, 1, row, headers.Length);
ws.Columns().AdjustToContents();
ws.Column(2).Width = 50; // описание — широкая колонка
ws.Column(2).Width = 50;
}
private static void WriteSheet3(XLWorkbook workbook, IEnumerable<Sheet3Row> rows)
@@ -101,9 +100,9 @@ public class ExcelExportService
var headers = new[]
{
"п/п",
"Наименование (классификатор)",
"ТН ВЭД",
"Регистрационный номер",
"Дата регистрационного номера"
"Дата регистрационного номера",
};
for (int c = 0; c < headers.Length; c++)
@@ -118,12 +117,21 @@ public class ExcelExportService
foreach (var r in rows)
{
ws.Cell(row, 1).Value = r.SequentialNumber;
ws.Cell(row, 2).Value = r.ClassifierCode;
ws.Cell(row, 2).Value = r.TnVed;
ws.Cell(row, 3).Value = r.RegNumber;
ws.Cell(row, 4).Value = r.RegDate;
row++;
}
ApplyBorders(ws, 1, row - 1, headers.Length);
ws.Columns().AdjustToContents();
}
private static void ApplyBorders(IXLWorksheet ws, int fromRow, int toRow, int colCount)
{
var range = ws.Range(fromRow, 1, toRow, colCount);
range.Style.Border.InsideBorder = XLBorderStyleValues.Thin;
range.Style.Border.OutsideBorder = XLBorderStyleValues.Thin;
}
}
@@ -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;
@@ -1,56 +1,41 @@
using System.Collections.Generic;
using System.Linq;
using DeclarationAutomatization.Models;
namespace DeclarationAutomatization.Services;
public class Sheet3ExpandService
{
// Строит строки Листа3, разворачивая позиции по регистрационным номерам.
// Группировка по ТН ВЭД: все рег. номера с таким же ТН ВЭД из СПРАВКИ.
public List<Sheet3Row> Expand(
IEnumerable<DeclarationItem> declarationItems,
IEnumerable<SpravkaItem> allSpravkaItems)
// Разворачивает позиции Листа2 по рег. номерам.
// Один товар с N рег. номерами → N строк в Листе3 (с тем же п/п).
public List<Sheet3Row> Expand(IEnumerable<DeclarationItem> items)
{
// Индекс: ТнВэд → уникальные (РегНомер, Дата)
var regByTnVed = allSpravkaItems
.Where(s => !string.IsNullOrWhiteSpace(s.RegNumber))
.GroupBy(s => s.TnVed)
.ToDictionary(
g => g.Key,
g => g
.Select(s => (s.RegNumber, s.RegDate))
.Distinct()
.ToList()
);
var result = new List<Sheet3Row>();
foreach (var item in declarationItems)
foreach (var item in items)
{
if (regByTnVed.TryGetValue(item.TnVed, out var regPairs) && regPairs.Count > 0)
{
foreach (var (regNum, regDate) in regPairs)
{
result.Add(new Sheet3Row
{
SequentialNumber = item.SequentialNumber,
ClassifierCode = "09035",
RegNumber = regNum,
RegDate = regDate,
});
}
}
else
if (item.RegEntries.Count == 0)
{
result.Add(new Sheet3Row
{
SequentialNumber = item.SequentialNumber,
ClassifierCode = "09035",
TnVed = item.TnVed,
RegNumber = "",
RegDate = "",
});
}
else
{
foreach (var entry in item.RegEntries)
{
result.Add(new Sheet3Row
{
SequentialNumber = item.SequentialNumber,
TnVed = item.TnVed,
RegNumber = entry.Number,
RegDate = entry.Date,
});
}
}
}
return result;
@@ -6,35 +6,36 @@ namespace DeclarationAutomatization.Services;
public class TransformService
{
// Группирует строки СПРАВКИ по ТН ВЭД → одна позиция Листа2.
// Количество, сумма, вес — суммируются.
// Рег. номер / дата — берётся первый непустой в группе.
// п/п — нумерация по порядку с 1.
public List<DeclarationItem> BuildDeclarationItems(IEnumerable<SpravkaItem> allItems)
// Строит позиции Листа2 из ИТОГО-строк Листа1.
// Рег. номера берёт из СПРАВКИ по ТН ВЭД.
public List<DeclarationItem> BuildDeclarationItems(
IEnumerable<Sheet1Group> groups,
IEnumerable<SpravkaItem> spravkaItems)
{
int sequentialNumber = 1;
// Индекс: (ТН ВЭД, Страна) → уникальные (рег. номер, дата) из СПРАВКИ
var regByKey = spravkaItems
.Where(s => !string.IsNullOrWhiteSpace(s.RegNumber))
.GroupBy(s => (s.TnVed, s.CountryId))
.ToDictionary(
g => g.Key,
g => g.Select(s => new RegEntry(s.RegNumber, s.RegDate))
.Distinct()
.ToList()
);
return allItems
.GroupBy(s => s.TnVed)
.Select(g =>
{
var first = g.First();
var regEntry = g.FirstOrDefault(s => !string.IsNullOrWhiteSpace(s.RegNumber));
return new DeclarationItem
{
SequentialNumber = sequentialNumber++,
Description = first.Description,
TnVed = first.TnVed,
CountryId = first.CountryId,
Quantity = g.Sum(s => s.Quantity),
AmountWithVat = g.Sum(s => s.AmountWithVat),
GrossWeight = g.Sum(s => s.GrossWeight),
NetWeight = g.Sum(s => s.NetWeight),
RegNumber = regEntry?.RegNumber ?? "",
RegDate = regEntry?.RegDate ?? "",
};
})
.ToList();
return groups.Select(group => new DeclarationItem
{
SequentialNumber = group.SequentialNumber,
Description = group.Description,
TnVed = group.TnVed,
CountryId = group.CountryId,
Quantity = group.Quantity,
AmountWithVat = group.AmountWithVat,
GrossWeight = group.GrossWeight,
NetWeight = group.NetWeight,
RegEntries = regByKey.TryGetValue((group.TnVed, group.CountryId), out var regs)
? regs
: new List<RegEntry>(),
}).ToList();
}
}
@@ -14,136 +14,187 @@ namespace DeclarationAutomatization.ViewModels;
public partial class MainViewModel : ObservableObject
{
private readonly ExcelImportService _importService;
private readonly TransformService _transformService;
private readonly CodeLookupService _codeLookupService;
private readonly Sheet1ImportService _sheet1ImportService;
private readonly ExcelImportService _spravkaImportService;
private readonly CodesImportService _codesImportService;
private readonly TransformService _transformService;
private readonly CodeLookupService _codeLookupService;
private readonly Sheet3ExpandService _expandService;
private readonly ExcelExportService _exportService;
private readonly ExcelExportService _exportService;
// Загруженные файлы СПРАВОК
public ObservableCollection<SpravkaFileEntry> SpravkaFiles { get; } = new();
// Все позиции после обработки (полный список для экспорта)
private readonly List<DeclarationItem> _allItems = new();
// Все строки из всех загруженных СПРАВОК
private List<SpravkaItem> _allSpravkaItems = new();
// Отфильтрованный список для отображения в таблице
public ObservableCollection<DeclarationItem> FilteredItems { get; } = new();
// Позиции для Листа2 после обработки
public ObservableCollection<DeclarationItem> DeclarationItems { get; } = new();
[ObservableProperty] private int _startingNumber = 1;
[ObservableProperty] private string _declarationFileName = "";
[ObservableProperty] private string _codesFileName = "";
[ObservableProperty] private string _statusMessage = "";
[ObservableProperty] private string _errorMessage = "";
[ObservableProperty] private bool _isProcessing;
[ObservableProperty] private bool _hasResults;
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(AutoCount))]
[NotifyPropertyChangedFor(nameof(ReviewCount))]
[NotifyPropertyChangedFor(nameof(MissingCount))]
private int _totalItems;
[NotifyPropertyChangedFor(nameof(IsFilterAll))]
[NotifyPropertyChangedFor(nameof(IsFilterAuto))]
[NotifyPropertyChangedFor(nameof(IsFilterReview))]
[NotifyPropertyChangedFor(nameof(IsFilterMissing))]
[NotifyPropertyChangedFor(nameof(IsFilterNoNetWeight))]
private string _activeFilter = "All";
public int AutoCount => DeclarationItems.Count(i => i.Confidence == ConfidenceLevel.Auto);
public int ReviewCount => DeclarationItems.Count(i => i.Confidence == ConfidenceLevel.Review);
public int MissingCount => DeclarationItems.Count(i => i.Confidence == ConfidenceLevel.Missing);
public bool IsFilterAll => ActiveFilter == "All";
public bool IsFilterAuto => ActiveFilter == "Auto";
public bool IsFilterReview => ActiveFilter == "Review";
public bool IsFilterMissing => ActiveFilter == "Missing";
public bool IsFilterNoNetWeight => ActiveFilter == "NoNetWeight";
[ObservableProperty] private string _statusMessage = "Загрузите файл СПРАВКИ для начала работы";
[ObservableProperty] private bool _isProcessing;
[ObservableProperty] private bool _hasResults;
public int AutoCount => _allItems.Count(i => i.Confidence == ConfidenceLevel.Auto);
public int ReviewCount => _allItems.Count(i => i.Confidence == ConfidenceLevel.Review);
public int MissingCount => _allItems.Count(i => i.Confidence == ConfidenceLevel.Missing);
public int MissingNetWeightCount => _allItems.Count(i => i.NetWeight == 0);
private string _filePath = "";
public MainViewModel(
ExcelImportService importService,
TransformService transformService,
CodeLookupService codeLookupService,
Sheet1ImportService sheet1ImportService,
ExcelImportService spravkaImportService,
CodesImportService codesImportService,
TransformService transformService,
CodeLookupService codeLookupService,
Sheet3ExpandService expandService,
ExcelExportService exportService)
ExcelExportService exportService)
{
_importService = importService;
_transformService = transformService;
_codeLookupService = codeLookupService;
_expandService = expandService;
_exportService = exportService;
_sheet1ImportService = sheet1ImportService;
_spravkaImportService = spravkaImportService;
_codesImportService = codesImportService;
_transformService = transformService;
_codeLookupService = codeLookupService;
_expandService = expandService;
_exportService = exportService;
}
[RelayCommand]
private void AddSpravkaFile()
private async Task OpenDeclarationFileAsync()
{
var dialog = new Microsoft.Win32.OpenFileDialog
{
Title = "Выберите файл СПРАВКИ",
Filter = "Excel файлы (*.xlsx)|*.xlsx|Все файлы (*.*)|*.*",
Multiselect = true
Title = "Выберите файл декларации",
Filter = "Excel файлы (*.xlsx)|*.xlsx",
};
if (dialog.ShowDialog() != true) return;
foreach (var path in dialog.FileNames)
_filePath = dialog.FileName;
DeclarationFileName = Path.GetFileName(_filePath);
await ProcessAsync();
}
[RelayCommand]
private void ClearDeclarationFile()
{
_filePath = "";
DeclarationFileName = "";
_allItems.Clear();
FilteredItems.Clear();
HasResults = false;
ActiveFilter = "All";
NotifyStats();
StatusMessage = "Загрузите файл декларации для начала работы";
}
[RelayCommand]
private void ClearCodesFile()
{
_codeLookupService.LoadFromEntries(new System.Collections.Generic.List<CodeLookupEntry>());
CodesFileName = "";
StatusMessage = "Справочник кодов очищен";
if (_allItems.Count > 0)
{
// Определяем начальный п/п: продолжаем с последнего
int nextStart = SpravkaFiles.Count == 0 ? 1
: SpravkaFiles.Max(f => f.StartingNumber) + EstimateRowCount(f => f.FilePath == path ? 0 : 1);
// Простая эвристика: берём последний стартовый номер + 1000
int startingNumber = SpravkaFiles.Count == 0 ? 1
: SpravkaFiles.Last().StartingNumber + 1000;
SpravkaFiles.Add(new SpravkaFileEntry
{
FilePath = path,
StartingNumber = startingNumber
});
_codeLookupService.AssignCodes(_allItems);
RefreshFilter();
NotifyStats();
}
}
private int EstimateRowCount(Func<SpravkaFileEntry, int> _) => 1000;
[RelayCommand]
private void RemoveSpravkaFile(SpravkaFileEntry? entry)
private void LoadCodesFile()
{
if (entry != null)
SpravkaFiles.Remove(entry);
var dialog = new Microsoft.Win32.OpenFileDialog
{
Title = "Выберите справочник кодов",
Filter = "Excel файлы (*.xlsx)|*.xlsx",
};
if (dialog.ShowDialog() != true) return;
try
{
var entries = _codesImportService.ReadEntries(dialog.FileName);
_codeLookupService.LoadFromEntries(entries);
CodesFileName = Path.GetFileName(dialog.FileName);
StatusMessage = $"Справочник загружен: {entries.Count} кодов";
// Переназначить коды если декларация уже загружена
if (_allItems.Count > 0)
{
_codeLookupService.AssignCodes(_allItems);
RefreshFilter();
NotifyStats();
}
}
catch (Exception ex)
{
StatusMessage = $"Ошибка загрузки справочника: {ex.Message}";
}
}
[RelayCommand]
private void SetFilter(string filter)
{
ActiveFilter = filter;
RefreshFilter();
}
private async Task ProcessAsync()
{
if (SpravkaFiles.Count == 0)
{
StatusMessage = "Добавьте хотя бы один файл СПРАВКИ";
return;
}
if (string.IsNullOrEmpty(_filePath)) return;
IsProcessing = true;
HasResults = false;
ErrorMessage = "";
StatusMessage = "Обработка...";
try
{
await Task.Run(() =>
{
_allSpravkaItems = new List<SpravkaItem>();
var groups = _sheet1ImportService.ReadSheet1(_filePath);
var spravkaItems = _spravkaImportService.ReadSpravka(_filePath, 1);
var items = _transformService.BuildDeclarationItems(groups, spravkaItems);
foreach (var entry in SpravkaFiles)
{
var items = _importService.ReadSpravka(entry.FilePath, entry.StartingNumber);
_allSpravkaItems.AddRange(items);
}
var declItems = _transformService.BuildDeclarationItems(_allSpravkaItems);
_codeLookupService.AssignCodes(declItems);
_codeLookupService.AssignCodes(items);
Application.Current.Dispatcher.Invoke(() =>
{
DeclarationItems.Clear();
foreach (var item in declItems)
DeclarationItems.Add(item);
TotalItems = DeclarationItems.Count;
OnPropertyChanged(nameof(AutoCount));
OnPropertyChanged(nameof(ReviewCount));
OnPropertyChanged(nameof(MissingCount));
HasResults = DeclarationItems.Count > 0;
StatusMessage = $"Обработано {DeclarationItems.Count} позиций: " +
$"✅ {AutoCount} авто, ⚠️ {ReviewCount} проверить, 🔴 {MissingCount} не найдено";
_allItems.Clear();
_allItems.AddRange(items.OrderBy(i => i.TnVed));
RenumberItems();
RefreshFilter();
NotifyStats();
HasResults = _allItems.Count > 0;
StatusMessage = $"Загружено {_allItems.Count} позиций: " +
$"✅ {AutoCount} ⚠️ {ReviewCount} 🔴 {MissingCount}";
});
});
}
catch (Exception ex)
{
StatusMessage = $"Ошибка: {ex.Message}";
_filePath = "";
DeclarationFileName = "";
ErrorMessage = FriendlyError(ex);
StatusMessage = "";
}
finally
{
@@ -151,20 +202,12 @@ public partial class MainViewModel : ObservableObject
}
}
[RelayCommand]
private void ApproveAllAuto()
{
// Кнопка «Принять все авто» — уже зелёные, действий не требуется.
// Можно добавить визуальное подтверждение.
StatusMessage = $"Подтверждено {AutoCount} авто-позиций. Проверьте жёлтые и красные.";
}
[RelayCommand]
private async Task ExportAsync()
{
if (!HasResults)
{
StatusMessage = "Нет данных для экспорта. Сначала выполните обработку.";
StatusMessage = "Нет данных для экспорта. Сначала загрузите файл.";
return;
}
@@ -172,9 +215,8 @@ public partial class MainViewModel : ObservableObject
{
Title = "Сохранить результат",
Filter = "Excel файлы (*.xlsx)|*.xlsx",
FileName = $"Декларация_{DateTime.Now:yyyyMMdd_HHmm}.xlsx"
FileName = $"Декларация_{DateTime.Now:yyyyMMdd_HHmm}.xlsx",
};
if (dialog.ShowDialog() != true) return;
IsProcessing = true;
@@ -182,14 +224,15 @@ public partial class MainViewModel : ObservableObject
try
{
var sheet3Rows = await Task.Run(() =>
_expandService.Expand(DeclarationItems, _allSpravkaItems));
// _allItems уже отсортированы и перенумерованы — просто экспортируем
var sheet3Rows = await Task.Run(() => _expandService.Expand(_allItems));
await Task.Run(() => _exportService.Export(dialog.FileName, _allItems, sheet3Rows));
await Task.Run(() =>
_exportService.Export(dialog.FileName, DeclarationItems, sheet3Rows));
// Сдвигаем стартовый номер для следующей справки
StartingNumber += _allItems.Count;
StatusMessage = $"Файл сохранён: {Path.GetFileName(dialog.FileName)} " +
$"(Лист2: {DeclarationItems.Count} строк, Лист3: {sheet3Rows.Count} строк)";
$"(Лист2: {_allItems.Count} строк, Лист3: {sheet3Rows.Count} строк)";
}
catch (Exception ex)
{
@@ -204,15 +247,54 @@ public partial class MainViewModel : ObservableObject
// Вызывается из UI при ручном изменении кода в таблице
public void OnCodeManuallyChanged(DeclarationItem item, string newCode)
{
if (string.IsNullOrWhiteSpace(item.TnVed) || string.IsNullOrWhiteSpace(newCode))
return;
if (string.IsNullOrWhiteSpace(item.TnVed) || string.IsNullOrWhiteSpace(newCode)) return;
item.DeclarationCode = newCode;
item.Confidence = ConfidenceLevel.Auto;
_codeLookupService.LearnFromManualEdit(item.TnVed, newCode);
NotifyStats();
}
private void RefreshFilter()
{
FilteredItems.Clear();
var items = ActiveFilter switch
{
"Auto" => _allItems.Where(i => i.Confidence == ConfidenceLevel.Auto),
"Review" => _allItems.Where(i => i.Confidence == ConfidenceLevel.Review),
"Missing" => _allItems.Where(i => i.Confidence == ConfidenceLevel.Missing),
"NoNetWeight" => _allItems.Where(i => i.NetWeight == 0),
_ => _allItems.AsEnumerable(),
};
foreach (var item in items)
FilteredItems.Add(item);
}
partial void OnStartingNumberChanged(int value)
{
if (_allItems.Count == 0) return;
RenumberItems();
RefreshFilter();
}
private void RenumberItems()
{
for (int i = 0; i < _allItems.Count; i++)
_allItems[i].SequentialNumber = StartingNumber + i;
}
private static string FriendlyError(Exception ex)
{
if (ex is IOException || ex.InnerException is IOException)
return "Файл открыт в другой программе. Закройте его и попробуйте снова.";
return ex.Message;
}
private void NotifyStats()
{
OnPropertyChanged(nameof(AutoCount));
OnPropertyChanged(nameof(ReviewCount));
OnPropertyChanged(nameof(MissingCount));
OnPropertyChanged(nameof(MissingNetWeightCount));
}
}
+255 -141
View File
@@ -2,20 +2,13 @@
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:DeclarationAutomatization.Views"
xmlns:models="clr-namespace:DeclarationAutomatization.Models"
Title="Автоматизация деклараций" Height="780" Width="1200"
Title="Автоматизация деклараций" Height="800" Width="1200"
WindowStartupLocation="CenterScreen">
<Window.Resources>
<local:ConfidenceToColorConverter x:Key="ConfToColor"/>
<BooleanToVisibilityConverter x:Key="BoolToVis"/>
<Style x:Key="HeaderStyle" TargetType="TextBlock">
<Setter Property="FontSize" Value="13"/>
<Setter Property="FontWeight" Value="SemiBold"/>
<Setter Property="Margin" Value="0,0,0,6"/>
</Style>
<Style x:Key="SectionBorder" TargetType="Border">
<Setter Property="BorderBrush" Value="#DDDDDD"/>
<Setter Property="BorderThickness" Value="1"/>
@@ -55,6 +48,13 @@
<Style x:Key="SecondaryButton" TargetType="Button" BasedOn="{StaticResource PrimaryButton}">
<Setter Property="Background" Value="#6B7280"/>
</Style>
<Style x:Key="FilterButton" TargetType="Button" BasedOn="{StaticResource PrimaryButton}">
<Setter Property="Background" Value="#E5E7EB"/>
<Setter Property="Foreground" Value="#374151"/>
<Setter Property="Margin" Value="0,0,6,0"/>
<Setter Property="Padding" Value="12,5"/>
</Style>
</Window.Resources>
<Grid Margin="16">
@@ -68,59 +68,172 @@
<!-- Заголовок -->
<TextBlock Grid.Row="0" Text="Автоматизация оформления деклараций"
FontSize="18" FontWeight="Bold" Margin="0,0,0,16"
FontSize="18" FontWeight="Bold" Margin="0,0,0,12"
Foreground="#1F2937"/>
<!-- Шаг 1: Загрузка файлов -->
<!-- Загрузка файлов -->
<Border Grid.Row="1" Style="{StaticResource SectionBorder}">
<StackPanel>
<TextBlock Style="{StaticResource HeaderStyle}" Text="1. Файлы СПРАВКИ"/>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="20"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<DataGrid ItemsSource="{Binding SpravkaFiles}"
AutoGenerateColumns="False" CanUserAddRows="False"
Height="120" Margin="0,0,0,8"
GridLinesVisibility="Horizontal"
HeadersVisibility="Column">
<DataGrid.Columns>
<DataGridTextColumn Header="Файл" Binding="{Binding FileName}"
IsReadOnly="True" Width="*"/>
<DataGridTextColumn Header="Начальный п/п"
Binding="{Binding StartingNumber, UpdateSourceTrigger=PropertyChanged}"
Width="130"/>
<DataGridTemplateColumn Header="" Width="60">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<Button Content="✕" Width="28" Height="24"
Command="{Binding DataContext.RemoveSpravkaFileCommand,
RelativeSource={RelativeSource AncestorType=Window}}"
CommandParameter="{Binding}"
Background="#EF4444" Foreground="White"
BorderThickness="0" Cursor="Hand"/>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
</DataGrid.Columns>
</DataGrid>
<!-- Файл декларации -->
<StackPanel Grid.Column="0">
<TextBlock Text="Файл декларации (Лист1 + СПРАВКА)" FontWeight="SemiBold" Margin="0,0,0,6"/>
<StackPanel Orientation="Horizontal">
<Button Content="📂 Открыть файл"
Command="{Binding OpenDeclarationFileCommand}"
Style="{StaticResource PrimaryButton}" Margin="0,0,8,0"/>
<TextBlock Text="{Binding DeclarationFileName}" VerticalAlignment="Center"
Foreground="#6B7280" FontStyle="Italic" Margin="0,0,6,0"/>
<Button Content="✕" Width="24" Height="24"
Command="{Binding ClearDeclarationFileCommand}"
Background="#EF4444" Foreground="White" BorderThickness="0" Cursor="Hand"
Visibility="{Binding HasResults, Converter={StaticResource BoolToVis}}"
ToolTip="Очистить и загрузить другой файл"/>
</StackPanel>
<TextBlock Text="{Binding ErrorMessage}" Foreground="#DC2626" FontSize="12"
Margin="0,6,0,0" TextWrapping="Wrap">
<TextBlock.Style>
<Style TargetType="TextBlock">
<Style.Triggers>
<DataTrigger Binding="{Binding ErrorMessage}" Value="">
<Setter Property="Visibility" Value="Collapsed"/>
</DataTrigger>
</Style.Triggers>
</Style>
</TextBlock.Style>
</TextBlock>
</StackPanel>
<Button Content="+ Добавить файл СПРАВКИ"
Command="{Binding AddSpravkaFileCommand}"
Style="{StaticResource SecondaryButton}"
HorizontalAlignment="Left"/>
</StackPanel>
<!-- Справочник кодов -->
<StackPanel Grid.Column="2">
<TextBlock Text="Справочник кодов (опционально)" FontWeight="SemiBold" Margin="0,0,0,6"/>
<StackPanel Orientation="Horizontal">
<Button Content="📂 Загрузить справочник"
Command="{Binding LoadCodesFileCommand}"
Style="{StaticResource SecondaryButton}" Margin="0,0,8,0"/>
<TextBlock Text="{Binding CodesFileName, FallbackValue='не загружен'}"
VerticalAlignment="Center" Foreground="#6B7280" FontStyle="Italic" Margin="0,0,6,0"/>
<Button Content="✕" Width="24" Height="24"
Command="{Binding ClearCodesFileCommand}"
Background="#EF4444" Foreground="White" BorderThickness="0" Cursor="Hand"
ToolTip="Очистить справочник кодов">
<Button.Style>
<Style TargetType="Button">
<Style.Triggers>
<DataTrigger Binding="{Binding CodesFileName}" Value="">
<Setter Property="Visibility" Value="Collapsed"/>
</DataTrigger>
</Style.Triggers>
</Style>
</Button.Style>
</Button>
</StackPanel>
</StackPanel>
</Grid>
</Border>
<!-- Шаг 2: Кнопки действий -->
<Border Grid.Row="2" Style="{StaticResource SectionBorder}">
<!-- Статистика + фильтры -->
<Border Grid.Row="2" Style="{StaticResource SectionBorder}"
Visibility="{Binding HasResults, Converter={StaticResource BoolToVis}}">
<StackPanel Orientation="Horizontal">
<TextBlock Style="{StaticResource HeaderStyle}" Text="2. Действия:"
VerticalAlignment="Center" Margin="0,0,16,0"/>
<Button Content="▶ Обработать"
Command="{Binding ProcessCommand}"
Style="{StaticResource PrimaryButton}"/>
<Button Content="✅ Принять все авто"
Command="{Binding ApproveAllAutoCommand}"
Style="{StaticResource SecondaryButton}"
IsEnabled="{Binding HasResults}"/>
<TextBlock Text="Показать:" VerticalAlignment="Center" Margin="0,0,10,0" Foreground="#374151"/>
<Button Content="Все" Command="{Binding SetFilterCommand}" CommandParameter="All">
<Button.Style>
<Style TargetType="Button" BasedOn="{StaticResource FilterButton}">
<Style.Triggers>
<DataTrigger Binding="{Binding IsFilterAll}" Value="True">
<Setter Property="Background" Value="#0078D4"/>
<Setter Property="Foreground" Value="White"/>
</DataTrigger>
</Style.Triggers>
</Style>
</Button.Style>
</Button>
<Button Command="{Binding SetFilterCommand}" CommandParameter="Auto">
<Button.Style>
<Style TargetType="Button" BasedOn="{StaticResource FilterButton}">
<Style.Triggers>
<DataTrigger Binding="{Binding IsFilterAuto}" Value="True">
<Setter Property="Background" Value="#16A34A"/>
<Setter Property="Foreground" Value="White"/>
</DataTrigger>
</Style.Triggers>
</Style>
</Button.Style>
<StackPanel Orientation="Horizontal">
<TextBlock Text="✅ Авто: " VerticalAlignment="Center"/>
<TextBlock Text="{Binding AutoCount}" FontWeight="Bold" VerticalAlignment="Center"/>
</StackPanel>
</Button>
<Button Command="{Binding SetFilterCommand}" CommandParameter="Review">
<Button.Style>
<Style TargetType="Button" BasedOn="{StaticResource FilterButton}">
<Style.Triggers>
<DataTrigger Binding="{Binding IsFilterReview}" Value="True">
<Setter Property="Background" Value="#D97706"/>
<Setter Property="Foreground" Value="White"/>
</DataTrigger>
</Style.Triggers>
</Style>
</Button.Style>
<StackPanel Orientation="Horizontal">
<TextBlock Text="⚠️ Проверить: " VerticalAlignment="Center"/>
<TextBlock Text="{Binding ReviewCount}" FontWeight="Bold" VerticalAlignment="Center"/>
</StackPanel>
</Button>
<Button Command="{Binding SetFilterCommand}" CommandParameter="Missing">
<Button.Style>
<Style TargetType="Button" BasedOn="{StaticResource FilterButton}">
<Style.Triggers>
<DataTrigger Binding="{Binding IsFilterMissing}" Value="True">
<Setter Property="Background" Value="#DC2626"/>
<Setter Property="Foreground" Value="White"/>
</DataTrigger>
</Style.Triggers>
</Style>
</Button.Style>
<StackPanel Orientation="Horizontal">
<TextBlock Text="🔴 Нет кода: " VerticalAlignment="Center"/>
<TextBlock Text="{Binding MissingCount}" FontWeight="Bold" VerticalAlignment="Center"/>
</StackPanel>
</Button>
<Button Command="{Binding SetFilterCommand}" CommandParameter="NoNetWeight">
<Button.Style>
<Style TargetType="Button" BasedOn="{StaticResource FilterButton}">
<Style.Triggers>
<DataTrigger Binding="{Binding MissingNetWeightCount}" Value="0">
<Setter Property="Visibility" Value="Collapsed"/>
</DataTrigger>
<DataTrigger Binding="{Binding IsFilterNoNetWeight}" Value="True">
<Setter Property="Background" Value="#D97706"/>
<Setter Property="Foreground" Value="White"/>
</DataTrigger>
</Style.Triggers>
</Style>
</Button.Style>
<StackPanel Orientation="Horizontal">
<TextBlock Text="⚠️ Без нетто: " VerticalAlignment="Center"/>
<TextBlock Text="{Binding MissingNetWeightCount}" FontWeight="Bold" VerticalAlignment="Center"/>
</StackPanel>
</Button>
<Separator Width="1" Background="#DDDDDD" Margin="10,0"/>
<TextBlock Text="Начальный п/п:" VerticalAlignment="Center" Margin="0,0,6,0" Foreground="#374151"/>
<TextBox Text="{Binding StartingNumber, UpdateSourceTrigger=PropertyChanged}"
Width="60" Padding="4,4" VerticalAlignment="Center" Margin="0,0,8,0"
BorderBrush="#D1D5DB" BorderThickness="1"/>
<Button Content="💾 Экспорт"
Command="{Binding ExportCommand}"
Style="{StaticResource PrimaryButton}"
@@ -130,96 +243,98 @@
<!-- Таблица позиций -->
<Border Grid.Row="3" Style="{StaticResource SectionBorder}">
<DockPanel>
<!-- Статистика -->
<StackPanel DockPanel.Dock="Top" Orientation="Horizontal" Margin="0,0,0,8">
<TextBlock Style="{StaticResource HeaderStyle}" Text="3. Позиции декларации"/>
<Border Background="#C6EFCE" CornerRadius="3" Padding="8,2" Margin="12,0,4,0">
<TextBlock>
<Run Text="✅ Авто: "/>
<Run Text="{Binding AutoCount, Mode=OneWay}" FontWeight="Bold"/>
</TextBlock>
</Border>
<Border Background="#FFEB9C" CornerRadius="3" Padding="8,2" Margin="4,0">
<TextBlock>
<Run Text="⚠️ Проверить: "/>
<Run Text="{Binding ReviewCount, Mode=OneWay}" FontWeight="Bold"/>
</TextBlock>
</Border>
<Border Background="#FFC7CE" CornerRadius="3" Padding="8,2" Margin="4,0">
<TextBlock>
<Run Text="🔴 Не найдено: "/>
<Run Text="{Binding MissingCount, Mode=OneWay}" FontWeight="Bold"/>
</TextBlock>
</Border>
</StackPanel>
<DataGrid x:Name="ItemsGrid"
ItemsSource="{Binding FilteredItems}"
AutoGenerateColumns="False"
CanUserAddRows="False"
SelectionMode="Single"
GridLinesVisibility="Horizontal"
RowBackground="White"
AlternatingRowBackground="#F9FAFB">
<DataGrid x:Name="ItemsGrid"
ItemsSource="{Binding DeclarationItems}"
AutoGenerateColumns="False"
CanUserAddRows="False"
SelectionMode="Single"
GridLinesVisibility="Horizontal"
RowBackground="White"
AlternatingRowBackground="#F9FAFB">
<DataGrid.RowStyle>
<Style TargetType="DataGridRow">
<Setter Property="Background"
Value="{Binding Confidence, Converter={StaticResource ConfToColor}}"/>
</Style>
</DataGrid.RowStyle>
<DataGrid.RowStyle>
<Style TargetType="DataGridRow">
<Setter Property="Background"
Value="{Binding Confidence, Converter={StaticResource ConfToColor}}"/>
</Style>
</DataGrid.RowStyle>
<DataGrid.Columns>
<DataGridTextColumn Header="п/п"
Binding="{Binding SequentialNumber}"
IsReadOnly="True" Width="55"/>
<DataGridTextColumn Header="ТН ВЭД"
Binding="{Binding TnVed}"
IsReadOnly="True" Width="115"/>
<DataGridTextColumn Header="Наименование"
Binding="{Binding Description}"
IsReadOnly="True" Width="*"/>
<DataGridTextColumn Header="Страна"
Binding="{Binding CountryId}"
IsReadOnly="True" Width="60"/>
<DataGridTextColumn Header="Кол-во"
Binding="{Binding Quantity}"
IsReadOnly="True" Width="75"/>
<DataGridTextColumn Header="Сумма"
Binding="{Binding AmountWithVat, StringFormat=N2}"
IsReadOnly="True" Width="100"/>
<DataGridTextColumn Header="Брутто, кг"
Binding="{Binding GrossWeight, StringFormat=N3}"
IsReadOnly="True" Width="90"/>
<DataGridTemplateColumn Header="Нетто, кг" Width="90" IsReadOnly="True">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<Grid>
<Grid.Style>
<Style TargetType="Grid">
<Style.Triggers>
<DataTrigger Binding="{Binding NetWeight}" Value="0">
<Setter Property="Background" Value="#FEE2E2"/>
</DataTrigger>
</Style.Triggers>
</Style>
</Grid.Style>
<TextBlock Text="{Binding NetWeight, StringFormat=N3}"
Padding="4,2" VerticalAlignment="Center">
<TextBlock.Style>
<Style TargetType="TextBlock">
<Style.Triggers>
<DataTrigger Binding="{Binding NetWeight}" Value="0">
<Setter Property="Foreground" Value="#DC2626"/>
<Setter Property="FontWeight" Value="SemiBold"/>
</DataTrigger>
</Style.Triggers>
</Style>
</TextBlock.Style>
</TextBlock>
</Grid>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<DataGrid.Columns>
<DataGridTextColumn Header="п/п"
Binding="{Binding SequentialNumber}"
IsReadOnly="True" Width="55"/>
<DataGridTextColumn Header="ТН ВЭД"
Binding="{Binding TnVed}"
IsReadOnly="True" Width="120"/>
<DataGridTextColumn Header="Наименование"
Binding="{Binding Description}"
IsReadOnly="True" Width="*"/>
<DataGridTextColumn Header="Страна"
Binding="{Binding CountryId}"
IsReadOnly="True" Width="60"/>
<DataGridTextColumn Header="Кол-во"
Binding="{Binding Quantity}"
IsReadOnly="True" Width="80"/>
<DataGridTextColumn Header="Сумма"
Binding="{Binding AmountWithVat, StringFormat=N2}"
IsReadOnly="True" Width="100"/>
<DataGridTextColumn Header="Брутто, кг"
Binding="{Binding GrossWeight, StringFormat=N3}"
IsReadOnly="True" Width="90"/>
<DataGridTextColumn Header="Нетто, кг"
Binding="{Binding NetWeight, StringFormat=N3}"
IsReadOnly="True" Width="90"/>
<!-- Редактируемая колонка кода -->
<DataGridTemplateColumn Header="Код декларации" Width="130">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<TextBlock Text="{Binding DeclarationCode}"
Padding="4,2" VerticalAlignment="Center"/>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
<DataGridTemplateColumn.CellEditingTemplate>
<DataTemplate>
<ComboBox IsEditable="True"
Text="{Binding DeclarationCode, UpdateSourceTrigger=PropertyChanged}"
ItemsSource="{Binding CandidateCodes}"
VerticalAlignment="Center"/>
</DataTemplate>
</DataGridTemplateColumn.CellEditingTemplate>
</DataGridTemplateColumn>
<!-- Редактируемая колонка кода — ComboBox при наличии кандидатов -->
<DataGridTemplateColumn Header="Код декларации" Width="130">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<TextBlock Text="{Binding DeclarationCode}"
Padding="4,2" VerticalAlignment="Center"/>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
<DataGridTemplateColumn.CellEditingTemplate>
<DataTemplate>
<ComboBox IsEditable="True"
Text="{Binding DeclarationCode, UpdateSourceTrigger=PropertyChanged}"
ItemsSource="{Binding CandidateCodes}"
VerticalAlignment="Center"/>
</DataTemplate>
</DataGridTemplateColumn.CellEditingTemplate>
</DataGridTemplateColumn>
<DataGridTextColumn Header="Рег. номер"
Binding="{Binding RegNumber}"
IsReadOnly="True" Width="180"/>
</DataGrid.Columns>
</DataGrid>
</DockPanel>
<DataGridTextColumn Header="Рег. номеров"
Binding="{Binding RegEntries.Count}"
IsReadOnly="True" Width="90"/>
</DataGrid.Columns>
</DataGrid>
</Border>
<!-- Строка статуса -->
@@ -227,8 +342,7 @@
<Grid>
<TextBlock Text="{Binding StatusMessage}" VerticalAlignment="Center"
FontSize="12" Foreground="#374151"/>
<ProgressBar IsIndeterminate="True" Height="4"
VerticalAlignment="Bottom"
<ProgressBar IsIndeterminate="True" Height="4" VerticalAlignment="Bottom"
Visibility="{Binding IsProcessing, Converter={StaticResource BoolToVis}}"/>
</Grid>
</Border>