697ae44519
- Загрузка одного .xlsx (СПРАВКА + Лист1 в одном файле) - Рег. номера берутся из СПРАВКА автоматически, группируются по (ТН ВЭД, Страна) - Отдельный опциональный справочник кодов (CodesImportService) - Фильтры: Все / Авто / Проверить / Нет кода / Без нетто - Таблица сразу отсортирована по ТН ВЭД с живым обновлением п/п - Начальный п/п: поле в UI, автоинкремент после экспорта - Лист2: рамки, жёлтая строка при >1 рег. номере, итого по кол-ву - Лист3: ТН ВЭД в колонке B вместо константы 09035 - Красный ErrorMessage под кнопкой загрузки, удалён SpravkaFileEntry
301 lines
11 KiB
C#
301 lines
11 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 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 List<DeclarationItem> _allItems = new();
|
|
|
|
// Отфильтрованный список для отображения в таблице
|
|
public ObservableCollection<DeclarationItem> FilteredItems { 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(IsFilterAll))]
|
|
[NotifyPropertyChangedFor(nameof(IsFilterAuto))]
|
|
[NotifyPropertyChangedFor(nameof(IsFilterReview))]
|
|
[NotifyPropertyChangedFor(nameof(IsFilterMissing))]
|
|
[NotifyPropertyChangedFor(nameof(IsFilterNoNetWeight))]
|
|
private string _activeFilter = "All";
|
|
|
|
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";
|
|
|
|
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(
|
|
Sheet1ImportService sheet1ImportService,
|
|
ExcelImportService spravkaImportService,
|
|
CodesImportService codesImportService,
|
|
TransformService transformService,
|
|
CodeLookupService codeLookupService,
|
|
Sheet3ExpandService expandService,
|
|
ExcelExportService exportService)
|
|
{
|
|
_sheet1ImportService = sheet1ImportService;
|
|
_spravkaImportService = spravkaImportService;
|
|
_codesImportService = codesImportService;
|
|
_transformService = transformService;
|
|
_codeLookupService = codeLookupService;
|
|
_expandService = expandService;
|
|
_exportService = exportService;
|
|
}
|
|
|
|
[RelayCommand]
|
|
private async Task OpenDeclarationFileAsync()
|
|
{
|
|
var dialog = new Microsoft.Win32.OpenFileDialog
|
|
{
|
|
Title = "Выберите файл декларации",
|
|
Filter = "Excel файлы (*.xlsx)|*.xlsx",
|
|
};
|
|
if (dialog.ShowDialog() != true) return;
|
|
|
|
_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)
|
|
{
|
|
_codeLookupService.AssignCodes(_allItems);
|
|
RefreshFilter();
|
|
NotifyStats();
|
|
}
|
|
}
|
|
|
|
[RelayCommand]
|
|
private void LoadCodesFile()
|
|
{
|
|
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 (string.IsNullOrEmpty(_filePath)) return;
|
|
|
|
IsProcessing = true;
|
|
HasResults = false;
|
|
ErrorMessage = "";
|
|
StatusMessage = "Обработка...";
|
|
|
|
try
|
|
{
|
|
await Task.Run(() =>
|
|
{
|
|
var groups = _sheet1ImportService.ReadSheet1(_filePath);
|
|
var spravkaItems = _spravkaImportService.ReadSpravka(_filePath, 1);
|
|
var items = _transformService.BuildDeclarationItems(groups, spravkaItems);
|
|
|
|
_codeLookupService.AssignCodes(items);
|
|
|
|
Application.Current.Dispatcher.Invoke(() =>
|
|
{
|
|
_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)
|
|
{
|
|
_filePath = "";
|
|
DeclarationFileName = "";
|
|
ErrorMessage = FriendlyError(ex);
|
|
StatusMessage = "";
|
|
}
|
|
finally
|
|
{
|
|
IsProcessing = false;
|
|
}
|
|
}
|
|
|
|
[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
|
|
{
|
|
// _allItems уже отсортированы и перенумерованы — просто экспортируем
|
|
var sheet3Rows = await Task.Run(() => _expandService.Expand(_allItems));
|
|
await Task.Run(() => _exportService.Export(dialog.FileName, _allItems, sheet3Rows));
|
|
|
|
// Сдвигаем стартовый номер для следующей справки
|
|
StartingNumber += _allItems.Count;
|
|
|
|
StatusMessage = $"Файл сохранён: {Path.GetFileName(dialog.FileName)} " +
|
|
$"(Лист2: {_allItems.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);
|
|
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));
|
|
}
|
|
}
|