feat: единый файл Лист1+СПРАВКА, фильтры, сортировка, живой п/п
- Загрузка одного .xlsx (СПРАВКА + Лист1 в одном файле) - Рег. номера берутся из СПРАВКА автоматически, группируются по (ТН ВЭД, Страна) - Отдельный опциональный справочник кодов (CodesImportService) - Фильтры: Все / Авто / Проверить / Нет кода / Без нетто - Таблица сразу отсортирована по ТН ВЭД с живым обновлением п/п - Начальный п/п: поле в UI, автоинкремент после экспорта - Лист2: рамки, жёлтая строка при >1 рег. номере, итого по кол-ву - Лист3: ТН ВЭД в колонке B вместо константы 09035 - Красный ErrorMessage под кнопкой загрузки, удалён SpravkaFileEntry
This commit is contained in:
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user