diff --git a/DeclarationAutomatization/App.xaml.cs b/DeclarationAutomatization/App.xaml.cs index 2abc30c..f41cbb3 100644 --- a/DeclarationAutomatization/App.xaml.cs +++ b/DeclarationAutomatization/App.xaml.cs @@ -22,7 +22,9 @@ public partial class App : Application private static void ConfigureServices(IServiceCollection services) { + services.AddSingleton(); services.AddSingleton(); + services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); diff --git a/DeclarationAutomatization/Models/DeclarationItem.cs b/DeclarationAutomatization/Models/DeclarationItem.cs index 50e4874..2369abc 100644 --- a/DeclarationAutomatization/Models/DeclarationItem.cs +++ b/DeclarationAutomatization/Models/DeclarationItem.cs @@ -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 RegEntries { get; set; } = new(); [ObservableProperty] private string _declarationCode = ""; diff --git a/DeclarationAutomatization/Models/Sheet1Group.cs b/DeclarationAutomatization/Models/Sheet1Group.cs index 7c99fc4..d5209ea 100644 --- a/DeclarationAutomatization/Models/Sheet1Group.cs +++ b/DeclarationAutomatization/Models/Sheet1Group.cs @@ -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 RegNumbers { get; set; } = new(); } diff --git a/DeclarationAutomatization/Models/Sheet3Row.cs b/DeclarationAutomatization/Models/Sheet3Row.cs index b347ef6..4f45754 100644 --- a/DeclarationAutomatization/Models/Sheet3Row.cs +++ b/DeclarationAutomatization/Models/Sheet3Row.cs @@ -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; } = ""; } diff --git a/DeclarationAutomatization/Models/SpravkaFileEntry.cs b/DeclarationAutomatization/Models/SpravkaFileEntry.cs deleted file mode 100644 index 70b251b..0000000 --- a/DeclarationAutomatization/Models/SpravkaFileEntry.cs +++ /dev/null @@ -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); -} diff --git a/DeclarationAutomatization/Services/CodeLookupService.cs b/DeclarationAutomatization/Services/CodeLookupService.cs index 0ec2d4c..8e77049 100644 --- a/DeclarationAutomatization/Services/CodeLookupService.cs +++ b/DeclarationAutomatization/Services/CodeLookupService.cs @@ -15,9 +15,15 @@ public class CodeLookupService _entries = _persistence.Load(); } - public void Reload() => _entries = _persistence.Load(); + // Загружает коды из внешнего xlsx-справочника (заменяет текущие записи) + public void LoadFromEntries(List entries) + { + _entries = entries; + } - // Назначает декларационные коды всем позициям в списке. + public bool HasEntries => _entries.Count > 0; + + // Назначает декларационные коды всем позициям в списке public void AssignCodes(IEnumerable items) { foreach (var item in items) @@ -46,18 +52,8 @@ public class CodeLookupService // Несколько кодов — неоднозначность item.CandidateCodes = new List(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; } // Фиксирует ручной выбор кода декларантом diff --git a/DeclarationAutomatization/Services/CodesImportService.cs b/DeclarationAutomatization/Services/CodesImportService.cs new file mode 100644 index 0000000..7eb40b7 --- /dev/null +++ b/DeclarationAutomatization/Services/CodesImportService.cs @@ -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 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(); + 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 { code }, + }); + } + + return result; + } +} diff --git a/DeclarationAutomatization/Services/ExcelExportService.cs b/DeclarationAutomatization/Services/ExcelExportService.cs index 9323474..d433e35 100644 --- a/DeclarationAutomatization/Services/ExcelExportService.cs +++ b/DeclarationAutomatization/Services/ExcelExportService.cs @@ -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 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; + } } diff --git a/DeclarationAutomatization/Services/Sheet1ImportService.cs b/DeclarationAutomatization/Services/Sheet1ImportService.cs index 9d7cbc5..d7ec8d1 100644 --- a/DeclarationAutomatization/Services/Sheet1ImportService.cs +++ b/DeclarationAutomatization/Services/Sheet1ImportService.cs @@ -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 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(); - var groupRows = new List(); // строки текущей группы (детальные) 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(); - } - 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 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(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 CollectRegNumbers(IEnumerable rows) - { - var seen = new HashSet(StringComparer.OrdinalIgnoreCase); - var result = new List(); - - 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; diff --git a/DeclarationAutomatization/Services/Sheet3ExpandService.cs b/DeclarationAutomatization/Services/Sheet3ExpandService.cs index a53622e..41df77b 100644 --- a/DeclarationAutomatization/Services/Sheet3ExpandService.cs +++ b/DeclarationAutomatization/Services/Sheet3ExpandService.cs @@ -1,56 +1,41 @@ using System.Collections.Generic; -using System.Linq; using DeclarationAutomatization.Models; namespace DeclarationAutomatization.Services; public class Sheet3ExpandService { - // Строит строки Листа3, разворачивая позиции по регистрационным номерам. - // Группировка по ТН ВЭД: все рег. номера с таким же ТН ВЭД из СПРАВКИ. - public List Expand( - IEnumerable declarationItems, - IEnumerable allSpravkaItems) + // Разворачивает позиции Листа2 по рег. номерам. + // Один товар с N рег. номерами → N строк в Листе3 (с тем же п/п). + public List Expand(IEnumerable 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(); - 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; diff --git a/DeclarationAutomatization/Services/TransformService.cs b/DeclarationAutomatization/Services/TransformService.cs index d345785..a314052 100644 --- a/DeclarationAutomatization/Services/TransformService.cs +++ b/DeclarationAutomatization/Services/TransformService.cs @@ -6,35 +6,36 @@ namespace DeclarationAutomatization.Services; public class TransformService { - // Группирует строки СПРАВКИ по ТН ВЭД → одна позиция Листа2. - // Количество, сумма, вес — суммируются. - // Рег. номер / дата — берётся первый непустой в группе. - // п/п — нумерация по порядку с 1. - public List BuildDeclarationItems(IEnumerable allItems) + // Строит позиции Листа2 из ИТОГО-строк Листа1. + // Рег. номера берёт из СПРАВКИ по ТН ВЭД. + public List BuildDeclarationItems( + IEnumerable groups, + IEnumerable 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(), + }).ToList(); } } diff --git a/DeclarationAutomatization/ViewModels/MainViewModel.cs b/DeclarationAutomatization/ViewModels/MainViewModel.cs index 71afc66..d358faf 100644 --- a/DeclarationAutomatization/ViewModels/MainViewModel.cs +++ b/DeclarationAutomatization/ViewModels/MainViewModel.cs @@ -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 SpravkaFiles { get; } = new(); + // Все позиции после обработки (полный список для экспорта) + private readonly List _allItems = new(); - // Все строки из всех загруженных СПРАВОК - private List _allSpravkaItems = new(); + // Отфильтрованный список для отображения в таблице + public ObservableCollection FilteredItems { get; } = new(); - // Позиции для Листа2 после обработки - public ObservableCollection 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()); + 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 _) => 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(); + 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)); } } diff --git a/DeclarationAutomatization/Views/MainWindow.xaml b/DeclarationAutomatization/Views/MainWindow.xaml index 32f3ff6..66dee4c 100644 --- a/DeclarationAutomatization/Views/MainWindow.xaml +++ b/DeclarationAutomatization/Views/MainWindow.xaml @@ -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"> - - + + @@ -68,59 +68,172 @@ - + - - + + + + + + - - - - - - - - + + + - - + + - - + + + + + + + + + + + + + +