Files
declaratrions-automatization/DeclarationAutomatization/ViewModels/MainViewModel.cs
T
Dianaka123 059895d1c3 feat: WPF-приложение для автоматизации оформления деклараций
- Чтение СПРАВКИ из Excel (ClosedXML), поддержка нескольких файлов
- Группировка по ТН ВЭД: схлопывание строк с суммированием кол-ва/веса/суммы
- Автоназначение кодов деклараций по справочнику ТН ВЭД (87 пар)
- Цветовая маркировка: зелёный/жёлтый/красный по уровню уверенности
- Самообучение: ручной выбор кода сохраняется в tnved_codes.json
- Формирование Лист3 с разворачиванием строк по рег. номерам (ключевая функция)
- Экспорт Лист2 + Лист3 в Excel
2026-04-05 23:19:58 +03:00

219 lines
7.8 KiB
C#

using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using System.Windows;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using DeclarationAutomatization.Models;
using DeclarationAutomatization.Services;
namespace DeclarationAutomatization.ViewModels;
public partial class MainViewModel : ObservableObject
{
private readonly ExcelImportService _importService;
private readonly TransformService _transformService;
private readonly CodeLookupService _codeLookupService;
private readonly Sheet3ExpandService _expandService;
private readonly ExcelExportService _exportService;
// Загруженные файлы СПРАВОК
public ObservableCollection<SpravkaFileEntry> SpravkaFiles { get; } = new();
// Все строки из всех загруженных СПРАВОК
private List<SpravkaItem> _allSpravkaItems = new();
// Позиции для Листа2 после обработки
public ObservableCollection<DeclarationItem> DeclarationItems { get; } = new();
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(AutoCount))]
[NotifyPropertyChangedFor(nameof(ReviewCount))]
[NotifyPropertyChangedFor(nameof(MissingCount))]
private int _totalItems;
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);
[ObservableProperty] private string _statusMessage = "Загрузите файл СПРАВКИ для начала работы";
[ObservableProperty] private bool _isProcessing;
[ObservableProperty] private bool _hasResults;
public MainViewModel(
ExcelImportService importService,
TransformService transformService,
CodeLookupService codeLookupService,
Sheet3ExpandService expandService,
ExcelExportService exportService)
{
_importService = importService;
_transformService = transformService;
_codeLookupService = codeLookupService;
_expandService = expandService;
_exportService = exportService;
}
[RelayCommand]
private void AddSpravkaFile()
{
var dialog = new Microsoft.Win32.OpenFileDialog
{
Title = "Выберите файл СПРАВКИ",
Filter = "Excel файлы (*.xlsx)|*.xlsx|Все файлы (*.*)|*.*",
Multiselect = true
};
if (dialog.ShowDialog() != true) return;
foreach (var path in dialog.FileNames)
{
// Определяем начальный п/п: продолжаем с последнего
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
});
}
}
private int EstimateRowCount(Func<SpravkaFileEntry, int> _) => 1000;
[RelayCommand]
private void RemoveSpravkaFile(SpravkaFileEntry? entry)
{
if (entry != null)
SpravkaFiles.Remove(entry);
}
[RelayCommand]
private async Task ProcessAsync()
{
if (SpravkaFiles.Count == 0)
{
StatusMessage = "Добавьте хотя бы один файл СПРАВКИ";
return;
}
IsProcessing = true;
HasResults = false;
StatusMessage = "Обработка...";
try
{
await Task.Run(() =>
{
_allSpravkaItems = new List<SpravkaItem>();
foreach (var entry in SpravkaFiles)
{
var items = _importService.ReadSpravka(entry.FilePath, entry.StartingNumber);
_allSpravkaItems.AddRange(items);
}
var declItems = _transformService.BuildDeclarationItems(_allSpravkaItems);
_codeLookupService.AssignCodes(declItems);
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} не найдено";
});
});
}
catch (Exception ex)
{
StatusMessage = $"Ошибка: {ex.Message}";
}
finally
{
IsProcessing = false;
}
}
[RelayCommand]
private void ApproveAllAuto()
{
// Кнопка «Принять все авто» — уже зелёные, действий не требуется.
// Можно добавить визуальное подтверждение.
StatusMessage = $"Подтверждено {AutoCount} авто-позиций. Проверьте жёлтые и красные.";
}
[RelayCommand]
private async Task ExportAsync()
{
if (!HasResults)
{
StatusMessage = "Нет данных для экспорта. Сначала выполните обработку.";
return;
}
var dialog = new Microsoft.Win32.SaveFileDialog
{
Title = "Сохранить результат",
Filter = "Excel файлы (*.xlsx)|*.xlsx",
FileName = $"Декларация_{DateTime.Now:yyyyMMdd_HHmm}.xlsx"
};
if (dialog.ShowDialog() != true) return;
IsProcessing = true;
StatusMessage = "Экспорт...";
try
{
var sheet3Rows = await Task.Run(() =>
_expandService.Expand(DeclarationItems, _allSpravkaItems));
await Task.Run(() =>
_exportService.Export(dialog.FileName, DeclarationItems, sheet3Rows));
StatusMessage = $"Файл сохранён: {Path.GetFileName(dialog.FileName)} " +
$"(Лист2: {DeclarationItems.Count} строк, Лист3: {sheet3Rows.Count} строк)";
}
catch (Exception ex)
{
StatusMessage = $"Ошибка экспорта: {ex.Message}";
}
finally
{
IsProcessing = false;
}
}
// Вызывается из UI при ручном изменении кода в таблице
public void OnCodeManuallyChanged(DeclarationItem item, string newCode)
{
if (string.IsNullOrWhiteSpace(item.TnVed) || string.IsNullOrWhiteSpace(newCode))
return;
item.DeclarationCode = newCode;
item.Confidence = ConfidenceLevel.Auto;
_codeLookupService.LearnFromManualEdit(item.TnVed, newCode);
OnPropertyChanged(nameof(AutoCount));
OnPropertyChanged(nameof(ReviewCount));
OnPropertyChanged(nameof(MissingCount));
}
}