feat: WPF-приложение для автоматизации оформления деклараций
- Чтение СПРАВКИ из Excel (ClosedXML), поддержка нескольких файлов - Группировка по ТН ВЭД: схлопывание строк с суммированием кол-ва/веса/суммы - Автоназначение кодов деклараций по справочнику ТН ВЭД (87 пар) - Цветовая маркировка: зелёный/жёлтый/красный по уровню уверенности - Самообучение: ручной выбор кода сохраняется в tnved_codes.json - Формирование Лист3 с разворачиванием строк по рег. номерам (ключевая функция) - Экспорт Лист2 + Лист3 в Excel
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
bin/
|
||||
obj/
|
||||
*.user
|
||||
.vs/
|
||||
*.suo
|
||||
@@ -0,0 +1,9 @@
|
||||
<Application x:Class="DeclarationAutomatization.App"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:local="clr-namespace:DeclarationAutomatization"
|
||||
Startup="OnStartup">
|
||||
<Application.Resources>
|
||||
|
||||
</Application.Resources>
|
||||
</Application>
|
||||
@@ -0,0 +1,40 @@
|
||||
using System.Windows;
|
||||
using DeclarationAutomatization.Services;
|
||||
using DeclarationAutomatization.ViewModels;
|
||||
using DeclarationAutomatization.Views;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace DeclarationAutomatization;
|
||||
|
||||
public partial class App : Application
|
||||
{
|
||||
private ServiceProvider? _serviceProvider;
|
||||
|
||||
private void OnStartup(object sender, StartupEventArgs e)
|
||||
{
|
||||
var services = new ServiceCollection();
|
||||
ConfigureServices(services);
|
||||
_serviceProvider = services.BuildServiceProvider();
|
||||
|
||||
var mainWindow = _serviceProvider.GetRequiredService<MainWindow>();
|
||||
mainWindow.Show();
|
||||
}
|
||||
|
||||
private static void ConfigureServices(IServiceCollection services)
|
||||
{
|
||||
services.AddSingleton<ExcelImportService>();
|
||||
services.AddSingleton<TransformService>();
|
||||
services.AddSingleton<RulesPersistenceService>();
|
||||
services.AddSingleton<CodeLookupService>();
|
||||
services.AddSingleton<Sheet3ExpandService>();
|
||||
services.AddSingleton<ExcelExportService>();
|
||||
services.AddSingleton<MainViewModel>();
|
||||
services.AddSingleton<MainWindow>();
|
||||
}
|
||||
|
||||
protected override void OnExit(ExitEventArgs e)
|
||||
{
|
||||
_serviceProvider?.Dispose();
|
||||
base.OnExit(e);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
using System.Windows;
|
||||
|
||||
[assembly:ThemeInfo(
|
||||
ResourceDictionaryLocation.None, //where theme specific resource dictionaries are located
|
||||
//(used if a resource is not found in the page,
|
||||
// or application resource dictionaries)
|
||||
ResourceDictionaryLocation.SourceAssembly //where the generic resource dictionary is located
|
||||
//(used if a resource is not found in the page,
|
||||
// app, or any theme specific resource dictionaries)
|
||||
)]
|
||||
@@ -0,0 +1,89 @@
|
||||
[
|
||||
{ "TnVed": "3406000000", "Codes": ["0"] },
|
||||
{ "TnVed": "3506100000", "Codes": ["0"] },
|
||||
{ "TnVed": "3926100000", "Codes": ["0"] },
|
||||
{ "TnVed": "4202390000", "Codes": ["0"] },
|
||||
{ "TnVed": "4810998000", "Codes": ["0"] },
|
||||
{ "TnVed": "4811419000", "Codes": ["0"] },
|
||||
{ "TnVed": "4901100000", "Codes": ["0"] },
|
||||
{ "TnVed": "4903000000", "Codes": ["0"] },
|
||||
{ "TnVed": "4911990000", "Codes": ["0"] },
|
||||
{ "TnVed": "7117900000", "Codes": ["0"] },
|
||||
{ "TnVed": "9011800000", "Codes": ["0"] },
|
||||
{ "TnVed": "9503004100", "Codes": ["0"] },
|
||||
{ "TnVed": "9503004900", "Codes": ["0"] },
|
||||
{ "TnVed": "9503005500", "Codes": ["0"] },
|
||||
{ "TnVed": "9503006100", "Codes": ["0"] },
|
||||
{ "TnVed": "9503007000", "Codes": ["0"] },
|
||||
{ "TnVed": "9503009500", "Codes": ["0"] },
|
||||
{ "TnVed": "9503009909", "Codes": ["0"] },
|
||||
{ "TnVed": "9504908009", "Codes": ["0"] },
|
||||
{ "TnVed": "9505900000", "Codes": ["0"] },
|
||||
{ "TnVed": "9603301000", "Codes": ["0"] },
|
||||
{ "TnVed": "9608109200", "Codes": ["0"] },
|
||||
{ "TnVed": "9608200000", "Codes": ["0"] },
|
||||
{ "TnVed": "9609901000", "Codes": ["0"] },
|
||||
{ "TnVed": "9610000000", "Codes": ["0"] },
|
||||
{ "TnVed": "9503002100", "Codes": ["1000"] },
|
||||
{ "TnVed": "9503003000", "Codes": ["1000"] },
|
||||
{ "TnVed": "9503007500", "Codes": ["1000"] },
|
||||
{ "TnVed": "9503007900", "Codes": ["1000"] },
|
||||
{ "TnVed": "9503008500", "Codes": ["1000"] },
|
||||
{ "TnVed": "9609909000", "Codes": ["1000"] },
|
||||
{ "TnVed": "9619008109", "Codes": ["1000"] },
|
||||
{ "TnVed": "3924900001", "Codes": ["1100"] },
|
||||
{ "TnVed": "3924900009", "Codes": ["1100"] },
|
||||
{ "TnVed": "9603210000", "Codes": ["1100"] },
|
||||
{ "TnVed": "8509800000", "Codes": ["1900"] },
|
||||
{ "TnVed": "8516797000", "Codes": ["1900"] },
|
||||
{ "TnVed": "3304200000", "Codes": ["3200"] },
|
||||
{ "TnVed": "3824999608", "Codes": ["5000"] },
|
||||
{ "TnVed": "1901100000", "Codes": ["9000"] },
|
||||
{ "TnVed": "3919900000", "Codes": ["9000"] },
|
||||
{ "TnVed": "3926909200", "Codes": ["9000"] },
|
||||
{ "TnVed": "4016920000", "Codes": ["9000"] },
|
||||
{ "TnVed": "4202929800", "Codes": ["9000"] },
|
||||
{ "TnVed": "4820103000", "Codes": ["9000"] },
|
||||
{ "TnVed": "4820900000", "Codes": ["9000"] },
|
||||
{ "TnVed": "6102309000", "Codes": ["9000"] },
|
||||
{ "TnVed": "6103220000", "Codes": ["9000"] },
|
||||
{ "TnVed": "6103420001", "Codes": ["9000"] },
|
||||
{ "TnVed": "6104220000", "Codes": ["9000", "9900", "1100"] },
|
||||
{ "TnVed": "6104420000", "Codes": ["9000"] },
|
||||
{ "TnVed": "6104620000", "Codes": ["9000"] },
|
||||
{ "TnVed": "6107110000", "Codes": ["9000"] },
|
||||
{ "TnVed": "6107210000", "Codes": ["9000"] },
|
||||
{ "TnVed": "6107910000", "Codes": ["9000"] },
|
||||
{ "TnVed": "6108310000", "Codes": ["9000"] },
|
||||
{ "TnVed": "6108910000", "Codes": ["9000"] },
|
||||
{ "TnVed": "6110201000", "Codes": ["9000"] },
|
||||
{ "TnVed": "6110209100", "Codes": ["9000"] },
|
||||
{ "TnVed": "6110209900", "Codes": ["9000", "9900"] },
|
||||
{ "TnVed": "6110309900", "Codes": ["9000"] },
|
||||
{ "TnVed": "6112311000", "Codes": ["9000"] },
|
||||
{ "TnVed": "6115290000", "Codes": ["9000"] },
|
||||
{ "TnVed": "6115950000", "Codes": ["9000"] },
|
||||
{ "TnVed": "6117808009", "Codes": ["9000"] },
|
||||
{ "TnVed": "6203431900", "Codes": ["9000"] },
|
||||
{ "TnVed": "6204623100", "Codes": ["9000"] },
|
||||
{ "TnVed": "6204623900", "Codes": ["9000"] },
|
||||
{ "TnVed": "6205200000", "Codes": ["9000"] },
|
||||
{ "TnVed": "6210300000", "Codes": ["9000"] },
|
||||
{ "TnVed": "6210500000", "Codes": ["9000"] },
|
||||
{ "TnVed": "9503001009", "Codes": ["9000"] },
|
||||
{ "TnVed": "9503003500", "Codes": ["9000"] },
|
||||
{ "TnVed": "9503006900", "Codes": ["9000"] },
|
||||
{ "TnVed": "9506701000", "Codes": ["9000"] },
|
||||
{ "TnVed": "9608101000", "Codes": ["9000"] },
|
||||
{ "TnVed": "3213100000", "Codes": ["9100"] },
|
||||
{ "TnVed": "6402993900", "Codes": ["9100"] },
|
||||
{ "TnVed": "3926909709", "Codes": ["9900"] },
|
||||
{ "TnVed": "6109100000", "Codes": ["9900"] },
|
||||
{ "TnVed": "6111209000", "Codes": ["9900"] },
|
||||
{ "TnVed": "6209300000", "Codes": ["9900"] },
|
||||
{ "TnVed": "9506999000", "Codes": ["9900"] },
|
||||
{ "TnVed": "9615900000", "Codes": ["9900"] },
|
||||
{ "TnVed": "9609101000", "Codes": ["9921"] },
|
||||
{ "TnVed": "9609109000", "Codes": ["9929"] },
|
||||
{ "TnVed": "6404199000", "Codes": ["9939"] }
|
||||
]
|
||||
@@ -0,0 +1,24 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>WinExe</OutputType>
|
||||
<TargetFramework>net9.0-windows</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<UseWPF>true</UseWPF>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="ClosedXML" Version="0.105.0" />
|
||||
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.4.2" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="10.0.5" />
|
||||
</ItemGroup>
|
||||
|
||||
<!-- Копировать справочник ТН ВЭД рядом с exe -->
|
||||
<ItemGroup>
|
||||
<Content Include="Data\tnved_codes.json">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,10 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace DeclarationAutomatization.Models;
|
||||
|
||||
public class CodeLookupEntry
|
||||
{
|
||||
public string TnVed { get; set; } = "";
|
||||
// Список кодов (обычно 1, реже 2–3 при неоднозначности)
|
||||
public List<string> Codes { get; set; } = new();
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
namespace DeclarationAutomatization.Models;
|
||||
|
||||
public enum ConfidenceLevel
|
||||
{
|
||||
Auto, // код назначен однозначно — зелёный
|
||||
Review, // несколько вариантов — жёлтый
|
||||
Missing // ТН ВЭД не найден в справочнике — красный
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
using System.Collections.Generic;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
|
||||
namespace DeclarationAutomatization.Models;
|
||||
|
||||
public partial class DeclarationItem : ObservableObject
|
||||
{
|
||||
public int SequentialNumber { get; init; }
|
||||
public string Description { get; init; } = "";
|
||||
public string TnVed { get; init; } = "";
|
||||
public string CountryId { get; init; } = "";
|
||||
public decimal Quantity { get; init; }
|
||||
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; } = "";
|
||||
|
||||
[ObservableProperty]
|
||||
private string _declarationCode = "";
|
||||
|
||||
[ObservableProperty]
|
||||
private ConfidenceLevel _confidence = ConfidenceLevel.Missing;
|
||||
|
||||
// Все возможные коды при неоднозначном ТН ВЭД (для выпадающего списка)
|
||||
public List<string> CandidateCodes { get; set; } = new();
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
namespace DeclarationAutomatization.Models;
|
||||
|
||||
public class Sheet3Row
|
||||
{
|
||||
public int SequentialNumber { get; init; }
|
||||
public string ClassifierCode { get; init; } = "09035";
|
||||
public string RegNumber { get; init; } = "";
|
||||
public string RegDate { get; init; } = "";
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
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);
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
namespace DeclarationAutomatization.Models;
|
||||
|
||||
public class SpravkaItem
|
||||
{
|
||||
public int SequentialNumber { get; init; } // п/п (назначается приложением)
|
||||
public string Description { get; init; } = ""; // Наименование, характеристика...
|
||||
public string ProductCode { get; init; } = ""; // Код товара
|
||||
public string TnVed { get; init; } = ""; // ТН ВЭД
|
||||
public string CountryId { get; init; } = ""; // Страна ID
|
||||
public string Country { get; init; } = ""; // Страна
|
||||
public string Unit { get; init; } = ""; // Ед. Изм.
|
||||
public decimal Quantity { get; init; } // Количество, шт.
|
||||
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; } = ""; // Дата регистрационного номера
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using DeclarationAutomatization.Models;
|
||||
|
||||
namespace DeclarationAutomatization.Services;
|
||||
|
||||
public class CodeLookupService
|
||||
{
|
||||
private readonly RulesPersistenceService _persistence;
|
||||
private List<CodeLookupEntry> _entries;
|
||||
|
||||
public CodeLookupService(RulesPersistenceService persistence)
|
||||
{
|
||||
_persistence = persistence;
|
||||
_entries = _persistence.Load();
|
||||
}
|
||||
|
||||
public void Reload() => _entries = _persistence.Load();
|
||||
|
||||
// Назначает декларационные коды всем позициям в списке.
|
||||
public void AssignCodes(IEnumerable<DeclarationItem> items)
|
||||
{
|
||||
foreach (var item in items)
|
||||
AssignCode(item);
|
||||
}
|
||||
|
||||
private void AssignCode(DeclarationItem item)
|
||||
{
|
||||
var entry = _entries.FirstOrDefault(e => e.TnVed == item.TnVed);
|
||||
|
||||
if (entry == null || entry.Codes.Count == 0)
|
||||
{
|
||||
item.DeclarationCode = "";
|
||||
item.Confidence = ConfidenceLevel.Missing;
|
||||
item.CandidateCodes = new List<string>();
|
||||
return;
|
||||
}
|
||||
|
||||
if (entry.Codes.Count == 1)
|
||||
{
|
||||
item.DeclarationCode = entry.Codes[0];
|
||||
item.Confidence = ConfidenceLevel.Auto;
|
||||
item.CandidateCodes = new List<string>(entry.Codes);
|
||||
return;
|
||||
}
|
||||
|
||||
// Несколько кодов — неоднозначность
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
// Фиксирует ручной выбор кода декларантом
|
||||
public void LearnFromManualEdit(string tnVed, string chosenCode)
|
||||
{
|
||||
_persistence.LearnCode(_entries, tnVed, chosenCode);
|
||||
}
|
||||
|
||||
public List<CodeLookupEntry> GetAllEntries() => _entries;
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Drawing;
|
||||
using ClosedXML.Excel;
|
||||
using DeclarationAutomatization.Models;
|
||||
|
||||
namespace DeclarationAutomatization.Services;
|
||||
|
||||
public class ExcelExportService
|
||||
{
|
||||
public void Export(
|
||||
string outputPath,
|
||||
IEnumerable<DeclarationItem> declarationItems,
|
||||
IEnumerable<Sheet3Row> sheet3Rows)
|
||||
{
|
||||
using var workbook = new XLWorkbook();
|
||||
|
||||
WriteSheet2(workbook, declarationItems);
|
||||
WriteSheet3(workbook, sheet3Rows);
|
||||
|
||||
workbook.SaveAs(outputPath);
|
||||
}
|
||||
|
||||
private static void WriteSheet2(XLWorkbook workbook, IEnumerable<DeclarationItem> items)
|
||||
{
|
||||
var ws = workbook.Worksheets.Add("Лист2");
|
||||
|
||||
// Заголовки
|
||||
var headers = new[]
|
||||
{
|
||||
"п/п",
|
||||
"Наименование, характеристика, сорт, артикул товара",
|
||||
"ТН ВЭД",
|
||||
"Страна ID",
|
||||
"Количество,шт.",
|
||||
"Сумма с учетом НДС, руб. РФ",
|
||||
"ВЕС брутто, КГ",
|
||||
"Масса нетто, КГ",
|
||||
"Код декларации",
|
||||
"Регистрационный номер",
|
||||
"Дата регистрационного номера"
|
||||
};
|
||||
|
||||
for (int c = 0; c < headers.Length; c++)
|
||||
{
|
||||
var cell = ws.Cell(1, c + 1);
|
||||
cell.Value = headers[c];
|
||||
cell.Style.Font.Bold = true;
|
||||
cell.Style.Fill.BackgroundColor = XLColor.LightGray;
|
||||
}
|
||||
|
||||
int row = 2;
|
||||
decimal totalAmount = 0, totalGross = 0, totalNet = 0;
|
||||
|
||||
foreach (var item in items)
|
||||
{
|
||||
ws.Cell(row, 1).Value = item.SequentialNumber;
|
||||
ws.Cell(row, 2).Value = item.Description;
|
||||
ws.Cell(row, 3).Value = item.TnVed;
|
||||
ws.Cell(row, 4).Value = item.CountryId;
|
||||
ws.Cell(row, 5).Value = item.Quantity;
|
||||
ws.Cell(row, 6).Value = item.AmountWithVat;
|
||||
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
|
||||
{
|
||||
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;
|
||||
|
||||
totalAmount += item.AmountWithVat;
|
||||
totalGross += item.GrossWeight;
|
||||
totalNet += item.NetWeight;
|
||||
row++;
|
||||
}
|
||||
|
||||
// Итоговая строка
|
||||
ws.Cell(row, 5).Value = "ИТОГО:";
|
||||
ws.Cell(row, 5).Style.Font.Bold = true;
|
||||
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;
|
||||
|
||||
ws.Columns().AdjustToContents();
|
||||
ws.Column(2).Width = 50; // описание — широкая колонка
|
||||
}
|
||||
|
||||
private static void WriteSheet3(XLWorkbook workbook, IEnumerable<Sheet3Row> rows)
|
||||
{
|
||||
var ws = workbook.Worksheets.Add("Лист3");
|
||||
|
||||
var headers = new[]
|
||||
{
|
||||
"п/п",
|
||||
"Наименование (классификатор)",
|
||||
"Регистрационный номер",
|
||||
"Дата регистрационного номера"
|
||||
};
|
||||
|
||||
for (int c = 0; c < headers.Length; c++)
|
||||
{
|
||||
var cell = ws.Cell(1, c + 1);
|
||||
cell.Value = headers[c];
|
||||
cell.Style.Font.Bold = true;
|
||||
cell.Style.Fill.BackgroundColor = XLColor.LightGray;
|
||||
}
|
||||
|
||||
int row = 2;
|
||||
foreach (var r in rows)
|
||||
{
|
||||
ws.Cell(row, 1).Value = r.SequentialNumber;
|
||||
ws.Cell(row, 2).Value = r.ClassifierCode;
|
||||
ws.Cell(row, 3).Value = r.RegNumber;
|
||||
ws.Cell(row, 4).Value = r.RegDate;
|
||||
row++;
|
||||
}
|
||||
|
||||
ws.Columns().AdjustToContents();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using ClosedXML.Excel;
|
||||
using DeclarationAutomatization.Models;
|
||||
|
||||
namespace DeclarationAutomatization.Services;
|
||||
|
||||
public class ExcelImportService
|
||||
{
|
||||
// Читает лист СПРАВКА из файла Excel.
|
||||
// startingNumber — первый п/п для строк этого файла.
|
||||
// Возвращает список SpravkaItem с назначенными п/п.
|
||||
public List<SpravkaItem> ReadSpravka(string filePath, int startingNumber)
|
||||
{
|
||||
var result = new List<SpravkaItem>();
|
||||
|
||||
using var workbook = new XLWorkbook(filePath);
|
||||
|
||||
// Ищем лист "СПРАВКА" (регистронезависимо)
|
||||
IXLWorksheet? sheet = null;
|
||||
foreach (var ws in workbook.Worksheets)
|
||||
{
|
||||
if (ws.Name.Equals("СПРАВКА", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
sheet = ws;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (sheet == null)
|
||||
throw new InvalidOperationException($"Лист 'СПРАВКА' не найден в файле: {filePath}");
|
||||
|
||||
// Ищем строку заголовка: первая строка, где ячейка B содержит "Наименование"
|
||||
int headerRow = FindHeaderRow(sheet);
|
||||
if (headerRow < 0)
|
||||
throw new InvalidOperationException("Не найдена строка заголовка в листе СПРАВКА (ожидалась ячейка с 'Наименование')");
|
||||
|
||||
int sequentialNumber = startingNumber;
|
||||
|
||||
for (int row = headerRow + 1; row <= sheet.LastRowUsed()?.RowNumber(); row++)
|
||||
{
|
||||
var cellB = sheet.Cell(row, 2).GetString().Trim();
|
||||
if (string.IsNullOrWhiteSpace(cellB))
|
||||
continue; // пропускаем пустые строки
|
||||
|
||||
var item = new SpravkaItem
|
||||
{
|
||||
SequentialNumber = sequentialNumber++,
|
||||
Description = cellB,
|
||||
ProductCode = sheet.Cell(row, 5).GetString().Trim(),
|
||||
TnVed = sheet.Cell(row, 8).GetString().Trim(),
|
||||
CountryId = sheet.Cell(row, 11).GetString().Trim(),
|
||||
Country = sheet.Cell(row, 12).GetString().Trim(),
|
||||
Unit = sheet.Cell(row, 13).GetString().Trim(),
|
||||
Quantity = ParseDecimal(sheet.Cell(row, 14)),
|
||||
AmountWithVat = ParseDecimal(sheet.Cell(row, 15)),
|
||||
GrossWeight = ParseDecimal(sheet.Cell(row, 16)),
|
||||
NetWeight = ParseDecimal(sheet.Cell(row, 17)),
|
||||
RegNumber = sheet.Cell(row, 18).GetString().Trim(),
|
||||
RegDate = FormatDate(sheet.Cell(row, 19)),
|
||||
};
|
||||
|
||||
result.Add(item);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static int FindHeaderRow(IXLWorksheet sheet)
|
||||
{
|
||||
int lastRow = Math.Min(sheet.LastRowUsed()?.RowNumber() ?? 200, 200);
|
||||
for (int r = 1; r <= lastRow; r++)
|
||||
{
|
||||
var val = sheet.Cell(r, 2).GetString();
|
||||
if (val.Contains("Наименование", StringComparison.OrdinalIgnoreCase))
|
||||
return r;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
private static decimal ParseDecimal(IXLCell cell)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (cell.IsEmpty()) return 0;
|
||||
var val = cell.GetString().Trim().Replace(',', '.');
|
||||
return decimal.TryParse(val, System.Globalization.NumberStyles.Any,
|
||||
System.Globalization.CultureInfo.InvariantCulture, out var d) ? d : 0;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
private static string FormatDate(IXLCell cell)
|
||||
{
|
||||
if (cell.IsEmpty()) return "";
|
||||
try
|
||||
{
|
||||
if (cell.DataType == XLDataType.DateTime)
|
||||
return cell.GetDateTime().ToString("dd.MM.yyyy");
|
||||
}
|
||||
catch { }
|
||||
return cell.GetString().Trim();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text.Json;
|
||||
using DeclarationAutomatization.Models;
|
||||
|
||||
namespace DeclarationAutomatization.Services;
|
||||
|
||||
public class RulesPersistenceService
|
||||
{
|
||||
private readonly string _rulesPath;
|
||||
|
||||
public RulesPersistenceService()
|
||||
{
|
||||
// Храним рядом с исполняемым файлом приложения
|
||||
var appDir = AppContext.BaseDirectory;
|
||||
_rulesPath = Path.Combine(appDir, "Data", "tnved_codes.json");
|
||||
}
|
||||
|
||||
public List<CodeLookupEntry> Load()
|
||||
{
|
||||
if (!File.Exists(_rulesPath))
|
||||
return new List<CodeLookupEntry>();
|
||||
|
||||
try
|
||||
{
|
||||
var json = File.ReadAllText(_rulesPath);
|
||||
return JsonSerializer.Deserialize<List<CodeLookupEntry>>(json)
|
||||
?? new List<CodeLookupEntry>();
|
||||
}
|
||||
catch
|
||||
{
|
||||
return new List<CodeLookupEntry>();
|
||||
}
|
||||
}
|
||||
|
||||
public void Save(IEnumerable<CodeLookupEntry> entries)
|
||||
{
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(_rulesPath)!);
|
||||
var json = JsonSerializer.Serialize(entries.ToList(),
|
||||
new JsonSerializerOptions { WriteIndented = true });
|
||||
File.WriteAllText(_rulesPath, json);
|
||||
}
|
||||
|
||||
// Обновляет (или добавляет) запись: запоминает выбранный декларантом код
|
||||
public void LearnCode(List<CodeLookupEntry> entries, string tnVed, string chosenCode)
|
||||
{
|
||||
var existing = entries.FirstOrDefault(e => e.TnVed == tnVed);
|
||||
if (existing == null)
|
||||
{
|
||||
entries.Add(new CodeLookupEntry
|
||||
{
|
||||
TnVed = tnVed,
|
||||
Codes = new List<string> { chosenCode }
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
// Перемещаем выбранный код на первое место (самообучение)
|
||||
existing.Codes.Remove(chosenCode);
|
||||
existing.Codes.Insert(0, chosenCode);
|
||||
}
|
||||
|
||||
Save(entries);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
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)
|
||||
{
|
||||
// Индекс: ТнВэд → уникальные (РегНомер, Дата)
|
||||
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)
|
||||
{
|
||||
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
|
||||
{
|
||||
result.Add(new Sheet3Row
|
||||
{
|
||||
SequentialNumber = item.SequentialNumber,
|
||||
ClassifierCode = "09035",
|
||||
RegNumber = "",
|
||||
RegDate = "",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using DeclarationAutomatization.Models;
|
||||
|
||||
namespace DeclarationAutomatization.Services;
|
||||
|
||||
public class TransformService
|
||||
{
|
||||
// Группирует строки СПРАВКИ по ТН ВЭД → одна позиция Листа2.
|
||||
// Количество, сумма, вес — суммируются.
|
||||
// Рег. номер / дата — берётся первый непустой в группе.
|
||||
// п/п — нумерация по порядку с 1.
|
||||
public List<DeclarationItem> BuildDeclarationItems(IEnumerable<SpravkaItem> allItems)
|
||||
{
|
||||
int sequentialNumber = 1;
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,218 @@
|
||||
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));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.Windows.Data;
|
||||
using System.Windows.Media;
|
||||
using DeclarationAutomatization.Models;
|
||||
|
||||
namespace DeclarationAutomatization.Views;
|
||||
|
||||
public class ConfidenceToColorConverter : IValueConverter
|
||||
{
|
||||
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
if (value is ConfidenceLevel level)
|
||||
{
|
||||
return level switch
|
||||
{
|
||||
ConfidenceLevel.Auto => new SolidColorBrush(Color.FromRgb(198, 239, 206)), // зелёный
|
||||
ConfidenceLevel.Review => new SolidColorBrush(Color.FromRgb(255, 235, 156)), // жёлтый
|
||||
ConfidenceLevel.Missing => new SolidColorBrush(Color.FromRgb(255, 199, 206)), // красный
|
||||
_ => Brushes.White
|
||||
};
|
||||
}
|
||||
return Brushes.White;
|
||||
}
|
||||
|
||||
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
=> throw new NotImplementedException();
|
||||
}
|
||||
@@ -0,0 +1,236 @@
|
||||
<Window x:Class="DeclarationAutomatization.Views.MainWindow"
|
||||
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"
|
||||
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"/>
|
||||
<Setter Property="CornerRadius" Value="4"/>
|
||||
<Setter Property="Padding" Value="12"/>
|
||||
<Setter Property="Margin" Value="0,0,0,10"/>
|
||||
<Setter Property="Background" Value="#FAFAFA"/>
|
||||
</Style>
|
||||
|
||||
<Style x:Key="PrimaryButton" TargetType="Button">
|
||||
<Setter Property="Padding" Value="16,8"/>
|
||||
<Setter Property="Margin" Value="4,0"/>
|
||||
<Setter Property="Background" Value="#0078D4"/>
|
||||
<Setter Property="Foreground" Value="White"/>
|
||||
<Setter Property="BorderThickness" Value="0"/>
|
||||
<Setter Property="Cursor" Value="Hand"/>
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="Button">
|
||||
<Border Background="{TemplateBinding Background}"
|
||||
CornerRadius="4" Padding="{TemplateBinding Padding}">
|
||||
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
||||
</Border>
|
||||
<ControlTemplate.Triggers>
|
||||
<Trigger Property="IsMouseOver" Value="True">
|
||||
<Setter Property="Background" Value="#006CBE"/>
|
||||
</Trigger>
|
||||
<Trigger Property="IsEnabled" Value="False">
|
||||
<Setter Property="Background" Value="#AAAAAA"/>
|
||||
</Trigger>
|
||||
</ControlTemplate.Triggers>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
<Style x:Key="SecondaryButton" TargetType="Button" BasedOn="{StaticResource PrimaryButton}">
|
||||
<Setter Property="Background" Value="#6B7280"/>
|
||||
</Style>
|
||||
</Window.Resources>
|
||||
|
||||
<Grid Margin="16">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="*"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<!-- Заголовок -->
|
||||
<TextBlock Grid.Row="0" Text="Автоматизация оформления деклараций"
|
||||
FontSize="18" FontWeight="Bold" Margin="0,0,0,16"
|
||||
Foreground="#1F2937"/>
|
||||
|
||||
<!-- Шаг 1: Загрузка файлов -->
|
||||
<Border Grid.Row="1" Style="{StaticResource SectionBorder}">
|
||||
<StackPanel>
|
||||
<TextBlock Style="{StaticResource HeaderStyle}" Text="1. Файлы СПРАВКИ"/>
|
||||
|
||||
<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>
|
||||
|
||||
<Button Content="+ Добавить файл СПРАВКИ"
|
||||
Command="{Binding AddSpravkaFileCommand}"
|
||||
Style="{StaticResource SecondaryButton}"
|
||||
HorizontalAlignment="Left"/>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!-- Шаг 2: Кнопки действий -->
|
||||
<Border Grid.Row="2" Style="{StaticResource SectionBorder}">
|
||||
<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}"/>
|
||||
<Button Content="💾 Экспорт"
|
||||
Command="{Binding ExportCommand}"
|
||||
Style="{StaticResource PrimaryButton}"
|
||||
IsEnabled="{Binding HasResults}"/>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!-- Таблица позиций -->
|
||||
<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 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.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"/>
|
||||
|
||||
<!-- Редактируемая колонка кода — 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>
|
||||
</Border>
|
||||
|
||||
<!-- Строка статуса -->
|
||||
<Border Grid.Row="4" Background="#F3F4F6" CornerRadius="4" Padding="12,6" Margin="0,4,0,0">
|
||||
<Grid>
|
||||
<TextBlock Text="{Binding StatusMessage}" VerticalAlignment="Center"
|
||||
FontSize="12" Foreground="#374151"/>
|
||||
<ProgressBar IsIndeterminate="True" Height="4"
|
||||
VerticalAlignment="Bottom"
|
||||
Visibility="{Binding IsProcessing, Converter={StaticResource BoolToVis}}"/>
|
||||
</Grid>
|
||||
</Border>
|
||||
</Grid>
|
||||
</Window>
|
||||
@@ -0,0 +1,40 @@
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using DeclarationAutomatization.Models;
|
||||
using DeclarationAutomatization.ViewModels;
|
||||
|
||||
namespace DeclarationAutomatization.Views;
|
||||
|
||||
public partial class MainWindow : Window
|
||||
{
|
||||
private readonly MainViewModel _viewModel;
|
||||
|
||||
public MainWindow(MainViewModel viewModel)
|
||||
{
|
||||
InitializeComponent();
|
||||
_viewModel = viewModel;
|
||||
DataContext = viewModel;
|
||||
|
||||
// Обрабатываем завершение редактирования кода в таблице
|
||||
ItemsGrid.CellEditEnding += OnCellEditEnding;
|
||||
}
|
||||
|
||||
private void OnCellEditEnding(object? sender, DataGridCellEditEndingEventArgs e)
|
||||
{
|
||||
if (e.EditAction != DataGridEditAction.Commit) return;
|
||||
if (e.Row.Item is not DeclarationItem item) return;
|
||||
|
||||
// Колонка "Код декларации" (индекс 8)
|
||||
if (e.Column.DisplayIndex != 8) return;
|
||||
|
||||
string? newCode = null;
|
||||
|
||||
if (e.EditingElement is ComboBox combo)
|
||||
newCode = combo.Text?.Trim();
|
||||
else if (e.EditingElement is TextBox tb)
|
||||
newCode = tb.Text?.Trim();
|
||||
|
||||
if (!string.IsNullOrEmpty(newCode))
|
||||
_viewModel.OnCodeManuallyChanged(item, newCode);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user