Add invoice export folder setting and CSV export from context menu.
Users can choose the export directory in auth settings; right-click on an invoice saves a UTF-8 CSV into that folder. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
5e24cc95b0
commit
9ee5561f3d
@ -36,6 +36,9 @@
|
||||
<setting name="stringToken" serializeAs="String">
|
||||
<value />
|
||||
</setting>
|
||||
<setting name="InvoiceExportPath" serializeAs="String">
|
||||
<value />
|
||||
</setting>
|
||||
</Электронная_Фармация.Properties.Settings>
|
||||
</userSettings>
|
||||
</configuration>
|
||||
@ -108,5 +108,16 @@ namespace Электронная_Фармация.Classes
|
||||
Settings.Default.LocationId = (locationId ?? string.Empty).Trim();
|
||||
Settings.Default.Save();
|
||||
}
|
||||
|
||||
public static string InvoiceExportPath
|
||||
{
|
||||
get { return Settings.Default.InvoiceExportPath ?? string.Empty; }
|
||||
}
|
||||
|
||||
public static void SetInvoiceExportPath(string path)
|
||||
{
|
||||
Settings.Default.InvoiceExportPath = (path ?? string.Empty).Trim();
|
||||
Settings.Default.Save();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
194
src/ElectronicPharmacy/Classes/InvoiceExportService.cs
Normal file
194
src/ElectronicPharmacy/Classes/InvoiceExportService.cs
Normal file
@ -0,0 +1,194 @@
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using System.Windows.Forms;
|
||||
using System.Data.SQLite;
|
||||
|
||||
namespace Электронная_Фармация.Classes
|
||||
{
|
||||
/// <summary>
|
||||
/// Exports a local invoice (header + items) to a CSV file in the configured folder.
|
||||
/// </summary>
|
||||
public sealed class InvoiceExportService
|
||||
{
|
||||
public string ExportInvoice(string invoiceId, IWin32Window owner = null)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(invoiceId))
|
||||
{
|
||||
throw new ArgumentException("Не указана накладная для экспорта.", nameof(invoiceId));
|
||||
}
|
||||
|
||||
var folder = ResolveExportFolder(owner);
|
||||
if (string.IsNullOrWhiteSpace(folder))
|
||||
{
|
||||
throw new InvalidOperationException("Не указана папка для экспорта накладных.");
|
||||
}
|
||||
|
||||
if (!Directory.Exists(folder))
|
||||
{
|
||||
Directory.CreateDirectory(folder);
|
||||
}
|
||||
|
||||
InvoiceHeader header;
|
||||
using (var con = new SQLiteConnection(AppConfig.SqliteConnectionString))
|
||||
{
|
||||
con.Open();
|
||||
header = LoadHeader(con, invoiceId);
|
||||
var itemsSql = @"
|
||||
select
|
||||
ifnull([GoodCode], '') as [GoodCode],
|
||||
ifnull([Good], '') as [Good],
|
||||
ifnull([ProducerName], '') as [ProducerName],
|
||||
ifnull([GoodsGount], 0) as [Qty],
|
||||
ifnull([PriceSupplierWithNDS], ifnull([PriceSupplierWithoutNDS], 0)) as [Price],
|
||||
ifnull([SumWithNDS], ifnull([SumWithoutNDS], 0)) as [Sum],
|
||||
ifnull([BestBefore], '') as [BestBefore],
|
||||
ifnull([Serial], '') as [Serial]
|
||||
from [InvoiceItem]
|
||||
where [idInvoice] = @invoiceId
|
||||
order by [Good];";
|
||||
|
||||
var fileName = BuildFileName(header);
|
||||
var filePath = Path.Combine(folder, fileName);
|
||||
|
||||
var sb = new StringBuilder();
|
||||
sb.AppendLine("Номер накладной;Дата;Поставщик;Грузополучатель;Номер заказа;Сумма");
|
||||
sb.AppendLine(string.Join(";",
|
||||
Csv(header.Number),
|
||||
Csv(header.Date),
|
||||
Csv(header.Supplier),
|
||||
Csv(header.Consignee),
|
||||
Csv(header.OrderNumber),
|
||||
Csv(header.Sum)));
|
||||
sb.AppendLine();
|
||||
sb.AppendLine("Код товара;Товар;Производитель;Количество;Цена;Сумма;Срок годности;Серия");
|
||||
|
||||
using (var cmd = new SQLiteCommand(itemsSql, con))
|
||||
{
|
||||
cmd.Parameters.AddWithValue("@invoiceId", invoiceId);
|
||||
using (var reader = cmd.ExecuteReader())
|
||||
{
|
||||
while (reader.Read())
|
||||
{
|
||||
sb.AppendLine(string.Join(";",
|
||||
Csv(reader["GoodCode"]),
|
||||
Csv(reader["Good"]),
|
||||
Csv(reader["ProducerName"]),
|
||||
Csv(reader["Qty"]),
|
||||
Csv(reader["Price"]),
|
||||
Csv(reader["Sum"]),
|
||||
Csv(reader["BestBefore"]),
|
||||
Csv(reader["Serial"])));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// UTF-8 BOM — Excel корректно открывает кириллицу.
|
||||
File.WriteAllText(filePath, sb.ToString(), new UTF8Encoding(encoderShouldEmitUTF8Identifier: true));
|
||||
AppDebugLog.Info("InvoiceExport", $"Накладная {header.Number} экспортирована: {filePath}");
|
||||
return filePath;
|
||||
}
|
||||
}
|
||||
|
||||
public static string ResolveExportFolder(IWin32Window owner = null)
|
||||
{
|
||||
var folder = AppConfig.InvoiceExportPath;
|
||||
if (!string.IsNullOrWhiteSpace(folder) && Directory.Exists(folder))
|
||||
{
|
||||
return folder;
|
||||
}
|
||||
|
||||
using (var dialog = new FolderBrowserDialog())
|
||||
{
|
||||
dialog.Description = "Выберите папку для экспорта накладных";
|
||||
dialog.ShowNewFolderButton = true;
|
||||
if (!string.IsNullOrWhiteSpace(folder) && Directory.Exists(folder))
|
||||
{
|
||||
dialog.SelectedPath = folder;
|
||||
}
|
||||
|
||||
if (dialog.ShowDialog(owner) != DialogResult.OK || string.IsNullOrWhiteSpace(dialog.SelectedPath))
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
AppConfig.SetInvoiceExportPath(dialog.SelectedPath);
|
||||
return dialog.SelectedPath;
|
||||
}
|
||||
}
|
||||
|
||||
private static InvoiceHeader LoadHeader(SQLiteConnection con, string invoiceId)
|
||||
{
|
||||
using (var cmd = new SQLiteCommand(@"
|
||||
select
|
||||
ifnull([InvoiceNumber], '') as [Number],
|
||||
ifnull([InvoiceDate], '') as [Date],
|
||||
ifnull([SupplierName], '') as [Supplier],
|
||||
ifnull([ConsigneesName], '') as [Consignee],
|
||||
ifnull([SourceOrderNumber], '') as [OrderNumber],
|
||||
ifnull([InvoiceSum], ifnull([SumWithNDS], 0)) as [Sum]
|
||||
from [Invoice]
|
||||
where [idInvoice] = @invoiceId
|
||||
limit 1;", con))
|
||||
{
|
||||
cmd.Parameters.AddWithValue("@invoiceId", invoiceId);
|
||||
using (var reader = cmd.ExecuteReader())
|
||||
{
|
||||
if (!reader.Read())
|
||||
{
|
||||
throw new InvalidOperationException("Накладная не найдена.");
|
||||
}
|
||||
|
||||
return new InvoiceHeader
|
||||
{
|
||||
Number = reader["Number"]?.ToString() ?? string.Empty,
|
||||
Date = reader["Date"]?.ToString() ?? string.Empty,
|
||||
Supplier = reader["Supplier"]?.ToString() ?? string.Empty,
|
||||
Consignee = reader["Consignee"]?.ToString() ?? string.Empty,
|
||||
OrderNumber = reader["OrderNumber"]?.ToString() ?? string.Empty,
|
||||
Sum = reader["Sum"]?.ToString() ?? "0"
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static string BuildFileName(InvoiceHeader header)
|
||||
{
|
||||
var number = SanitizeFileName(string.IsNullOrWhiteSpace(header.Number) ? "invoice" : header.Number);
|
||||
var stamp = DateTime.Now.ToString("yyyyMMdd-HHmmss", CultureInfo.InvariantCulture);
|
||||
return $"Накладная_{number}_{stamp}.csv";
|
||||
}
|
||||
|
||||
private static string SanitizeFileName(string value)
|
||||
{
|
||||
foreach (var c in Path.GetInvalidFileNameChars())
|
||||
{
|
||||
value = value.Replace(c, '_');
|
||||
}
|
||||
|
||||
return value.Trim();
|
||||
}
|
||||
|
||||
private static string Csv(object value)
|
||||
{
|
||||
var text = value?.ToString() ?? string.Empty;
|
||||
if (text.IndexOfAny(new[] { ';', '"', '\r', '\n' }) >= 0)
|
||||
{
|
||||
return "\"" + text.Replace("\"", "\"\"") + "\"";
|
||||
}
|
||||
|
||||
return text;
|
||||
}
|
||||
|
||||
private sealed class InvoiceHeader
|
||||
{
|
||||
public string Number { get; set; }
|
||||
public string Date { get; set; }
|
||||
public string Supplier { get; set; }
|
||||
public string Consignee { get; set; }
|
||||
public string OrderNumber { get; set; }
|
||||
public string Sum { get; set; }
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -106,6 +106,7 @@
|
||||
<Compile Include="Classes\AppDebugLog.cs" />
|
||||
<Compile Include="Classes\DataSender.cs" />
|
||||
<Compile Include="Classes\ErrorResponse.cs" />
|
||||
<Compile Include="Classes\InvoiceExportService.cs" />
|
||||
<Compile Include="Classes\InvoiceRequestService.cs" />
|
||||
<Compile Include="Classes\InvoiceSyncService.cs" />
|
||||
<Compile Include="Classes\LoginRequest.cs" />
|
||||
|
||||
@ -40,6 +40,9 @@
|
||||
this.label3 = new System.Windows.Forms.Label();
|
||||
this.label5 = new System.Windows.Forms.Label();
|
||||
this.txtApiUrl = new Elfisa.UI.Controls.ModernTextBox();
|
||||
this.labelExportPath = new System.Windows.Forms.Label();
|
||||
this.txtInvoiceExportPath = new Elfisa.UI.Controls.ModernTextBox();
|
||||
this.btnBrowseExportPath = new Elfisa.UI.Controls.ModernButton();
|
||||
this.panel1.SuspendLayout();
|
||||
this.SuspendLayout();
|
||||
//
|
||||
@ -79,9 +82,55 @@
|
||||
this.txtPassword.Size = new System.Drawing.Size(375, 34);
|
||||
this.txtPassword.TabIndex = 3;
|
||||
//
|
||||
// label5
|
||||
//
|
||||
this.label5.AutoSize = true;
|
||||
this.label5.Font = new System.Drawing.Font("Segoe UI", 12F);
|
||||
this.label5.Location = new System.Drawing.Point(23, 225);
|
||||
this.label5.Name = "label5";
|
||||
this.label5.Size = new System.Drawing.Size(103, 28);
|
||||
this.label5.TabIndex = 11;
|
||||
this.label5.Text = "URL API";
|
||||
//
|
||||
// txtApiUrl
|
||||
//
|
||||
this.txtApiUrl.Font = new System.Drawing.Font("Segoe UI", 12F);
|
||||
this.txtApiUrl.Location = new System.Drawing.Point(132, 225);
|
||||
this.txtApiUrl.Name = "txtApiUrl";
|
||||
this.txtApiUrl.Size = new System.Drawing.Size(271, 34);
|
||||
this.txtApiUrl.TabIndex = 12;
|
||||
//
|
||||
// labelExportPath
|
||||
//
|
||||
this.labelExportPath.AutoSize = true;
|
||||
this.labelExportPath.Font = new System.Drawing.Font("Segoe UI", 12F);
|
||||
this.labelExportPath.Location = new System.Drawing.Point(23, 272);
|
||||
this.labelExportPath.Name = "labelExportPath";
|
||||
this.labelExportPath.Size = new System.Drawing.Size(280, 28);
|
||||
this.labelExportPath.TabIndex = 13;
|
||||
this.labelExportPath.Text = "Папка экспорта накладных";
|
||||
//
|
||||
// txtInvoiceExportPath
|
||||
//
|
||||
this.txtInvoiceExportPath.Font = new System.Drawing.Font("Segoe UI", 10F);
|
||||
this.txtInvoiceExportPath.Location = new System.Drawing.Point(28, 304);
|
||||
this.txtInvoiceExportPath.Name = "txtInvoiceExportPath";
|
||||
this.txtInvoiceExportPath.Size = new System.Drawing.Size(320, 30);
|
||||
this.txtInvoiceExportPath.TabIndex = 14;
|
||||
//
|
||||
// btnBrowseExportPath
|
||||
//
|
||||
this.btnBrowseExportPath.Font = new System.Drawing.Font("Segoe UI Semibold", 10F, System.Drawing.FontStyle.Bold);
|
||||
this.btnBrowseExportPath.Location = new System.Drawing.Point(354, 304);
|
||||
this.btnBrowseExportPath.Name = "btnBrowseExportPath";
|
||||
this.btnBrowseExportPath.Size = new System.Drawing.Size(49, 30);
|
||||
this.btnBrowseExportPath.TabIndex = 15;
|
||||
this.btnBrowseExportPath.Text = "...";
|
||||
this.btnBrowseExportPath.Click += new System.EventHandler(this.btnBrowseExportPath_Click);
|
||||
//
|
||||
// btnConfirmRegistration
|
||||
//
|
||||
this.btnConfirmRegistration.Location = new System.Drawing.Point(188, 300);
|
||||
this.btnConfirmRegistration.Location = new System.Drawing.Point(188, 356);
|
||||
this.btnConfirmRegistration.Name = "btnConfirmRegistration";
|
||||
this.btnConfirmRegistration.Size = new System.Drawing.Size(215, 43);
|
||||
this.btnConfirmRegistration.TabIndex = 5;
|
||||
@ -90,7 +139,7 @@
|
||||
//
|
||||
// btnCloseThis
|
||||
//
|
||||
this.btnCloseThis.Location = new System.Drawing.Point(28, 300);
|
||||
this.btnCloseThis.Location = new System.Drawing.Point(28, 356);
|
||||
this.btnCloseThis.Name = "btnCloseThis";
|
||||
this.btnCloseThis.Size = new System.Drawing.Size(125, 43);
|
||||
this.btnCloseThis.TabIndex = 6;
|
||||
@ -102,7 +151,7 @@
|
||||
this.checkTokenGained.AutoCheck = false;
|
||||
this.checkTokenGained.AutoSize = true;
|
||||
this.checkTokenGained.Font = new System.Drawing.Font("Segoe UI", 12F);
|
||||
this.checkTokenGained.Location = new System.Drawing.Point(28, 260);
|
||||
this.checkTokenGained.Location = new System.Drawing.Point(28, 420);
|
||||
this.checkTokenGained.Name = "checkTokenGained";
|
||||
this.checkTokenGained.Size = new System.Drawing.Size(171, 32);
|
||||
this.checkTokenGained.TabIndex = 7;
|
||||
@ -133,29 +182,14 @@
|
||||
this.label3.Text = "АВТОРИЗАЦИЯ";
|
||||
this.label3.TextAlign = System.Drawing.ContentAlignment.MiddleRight;
|
||||
//
|
||||
// label5
|
||||
//
|
||||
this.label5.AutoSize = true;
|
||||
this.label5.Font = new System.Drawing.Font("Segoe UI", 12F);
|
||||
this.label5.Location = new System.Drawing.Point(23, 225);
|
||||
this.label5.Name = "label5";
|
||||
this.label5.Size = new System.Drawing.Size(103, 28);
|
||||
this.label5.TabIndex = 11;
|
||||
this.label5.Text = "URL API";
|
||||
//
|
||||
// txtApiUrl
|
||||
//
|
||||
this.txtApiUrl.Font = new System.Drawing.Font("Segoe UI", 12F);
|
||||
this.txtApiUrl.Location = new System.Drawing.Point(132, 225);
|
||||
this.txtApiUrl.Name = "txtApiUrl";
|
||||
this.txtApiUrl.Size = new System.Drawing.Size(271, 34);
|
||||
this.txtApiUrl.TabIndex = 12;
|
||||
//
|
||||
// HF_Registration
|
||||
//
|
||||
this.AutoScaleDimensions = new System.Drawing.SizeF(8F, 16F);
|
||||
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
|
||||
this.ClientSize = new System.Drawing.Size(436, 360);
|
||||
this.ClientSize = new System.Drawing.Size(436, 420);
|
||||
this.Controls.Add(this.btnBrowseExportPath);
|
||||
this.Controls.Add(this.txtInvoiceExportPath);
|
||||
this.Controls.Add(this.labelExportPath);
|
||||
this.Controls.Add(this.txtApiUrl);
|
||||
this.Controls.Add(this.label5);
|
||||
this.Controls.Add(this.panel1);
|
||||
@ -192,5 +226,8 @@
|
||||
private System.Windows.Forms.Label label3;
|
||||
private System.Windows.Forms.Label label5;
|
||||
private Elfisa.UI.Controls.ModernTextBox txtApiUrl;
|
||||
private System.Windows.Forms.Label labelExportPath;
|
||||
private Elfisa.UI.Controls.ModernTextBox txtInvoiceExportPath;
|
||||
private Elfisa.UI.Controls.ModernButton btnBrowseExportPath;
|
||||
}
|
||||
}
|
||||
|
||||
@ -25,6 +25,27 @@ namespace Электронная_Фармация.HelpForms
|
||||
Close();
|
||||
}
|
||||
|
||||
private void btnBrowseExportPath_Click(object sender, EventArgs e)
|
||||
{
|
||||
using (var dialog = new FolderBrowserDialog())
|
||||
{
|
||||
dialog.Description = "Выберите папку для экспорта накладных";
|
||||
dialog.ShowNewFolderButton = true;
|
||||
if (!string.IsNullOrWhiteSpace(txtInvoiceExportPath.Text)
|
||||
&& System.IO.Directory.Exists(txtInvoiceExportPath.Text.Trim()))
|
||||
{
|
||||
dialog.SelectedPath = txtInvoiceExportPath.Text.Trim();
|
||||
}
|
||||
|
||||
if (dialog.ShowDialog(this) == DialogResult.OK)
|
||||
{
|
||||
txtInvoiceExportPath.Text = dialog.SelectedPath;
|
||||
AppConfig.SetInvoiceExportPath(dialog.SelectedPath);
|
||||
ToastNotification.ShowSuccess("Папка экспорта накладных сохранена");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async void btnConfirmRegistration_Click(object sender, EventArgs e)
|
||||
{
|
||||
btnConfirmRegistration.Enabled = false;
|
||||
@ -39,6 +60,7 @@ namespace Электронная_Фармация.HelpForms
|
||||
// на старый сохранённый/дефолтный адрес, а не на введённый пользователем.
|
||||
string apiUrl = txtApiUrl.Text.Trim();
|
||||
Settings.Default.ApiBaseUrl = apiUrl;
|
||||
AppConfig.SetInvoiceExportPath(txtInvoiceExportPath.Text);
|
||||
Settings.Default.Save();
|
||||
|
||||
LoginRequest loginRequest = new LoginRequest();
|
||||
@ -56,6 +78,7 @@ namespace Электронная_Фармация.HelpForms
|
||||
Settings.Default.stringPassword = strPassword;
|
||||
Settings.Default.stringToken = token;
|
||||
Settings.Default.ApiBaseUrl = client.BaseUrl;
|
||||
AppConfig.SetInvoiceExportPath(txtInvoiceExportPath.Text);
|
||||
Settings.Default.Save();
|
||||
|
||||
AppDebugLog.Info("Auth", $"Авторизация успешна. token={AppDebugLog.MaskToken(token)}");
|
||||
@ -104,6 +127,7 @@ namespace Электронная_Фармация.HelpForms
|
||||
txtApiUrl.Text = string.IsNullOrWhiteSpace(Settings.Default.ApiBaseUrl)
|
||||
? AppConfig.DefaultApiBaseUrl
|
||||
: Settings.Default.ApiBaseUrl;
|
||||
txtInvoiceExportPath.Text = AppConfig.InvoiceExportPath;
|
||||
|
||||
checkTokenGained.Checked = doWeHaveAToken.Length > 0;
|
||||
}
|
||||
|
||||
@ -119,5 +119,17 @@ namespace Электронная_Фармация.Properties {
|
||||
}
|
||||
}
|
||||
|
||||
[global::System.Configuration.UserScopedSettingAttribute()]
|
||||
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
|
||||
[global::System.Configuration.DefaultSettingValueAttribute("")]
|
||||
public string InvoiceExportPath {
|
||||
get {
|
||||
return ((string)(this["InvoiceExportPath"]));
|
||||
}
|
||||
set {
|
||||
this["InvoiceExportPath"] = value;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@ -26,5 +26,8 @@
|
||||
<Setting Name="UseDarkTheme" Type="System.Boolean" Scope="User">
|
||||
<Value Profile="(Default)">False</Value>
|
||||
</Setting>
|
||||
<Setting Name="InvoiceExportPath" Type="System.String" Scope="User">
|
||||
<Value Profile="(Default)" />
|
||||
</Setting>
|
||||
</Settings>
|
||||
</SettingsFile>
|
||||
@ -406,7 +406,7 @@ where date(i.[InvoiceDate]) between date(@dateFrom) and date(@dateTo)";
|
||||
return;
|
||||
}
|
||||
|
||||
_exportInvoiceMenuItem = new ToolStripMenuItem("Экспорт");
|
||||
_exportInvoiceMenuItem = new ToolStripMenuItem("Экспортировать в папку");
|
||||
_exportInvoiceMenuItem.Click += (_, __) => ExportSelectedInvoice();
|
||||
|
||||
_invoiceMenu = new ContextMenuStrip();
|
||||
@ -547,11 +547,27 @@ where ii.idInvoice = @invoiceId;";
|
||||
return;
|
||||
}
|
||||
|
||||
var invoiceNumber = dgvInvoice.SelectedRows[0].Cells["Номер"]?.Value?.ToString() ?? string.Empty;
|
||||
ToastNotification.ShowCustom(
|
||||
$"Экспорт накладной {invoiceNumber} будет добавлен после согласования формата.",
|
||||
Color.SteelBlue,
|
||||
Color.White);
|
||||
var row = dgvInvoice.SelectedRows[0];
|
||||
var invoiceId = row.Cells["ИД накладной"]?.Value?.ToString() ?? string.Empty;
|
||||
var invoiceNumber = row.Cells["Номер"]?.Value?.ToString() ?? string.Empty;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(invoiceId))
|
||||
{
|
||||
ToastNotification.ShowCustom("Не удалось определить накладную для экспорта.", Color.DarkOrange, Color.White);
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var exportService = new InvoiceExportService();
|
||||
var filePath = exportService.ExportInvoice(invoiceId, FindForm());
|
||||
ToastNotification.ShowSuccess($"Накладная {invoiceNumber} сохранена:\n{filePath}");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
AppDebugLog.Error("Invoices", $"Ошибка экспорта накладной {invoiceNumber}", ex);
|
||||
ToastNotification.ShowError($"Не удалось экспортировать накладную: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private void txtInvoiceNumber_KeyPress(object sender, KeyPressEventArgs e)
|
||||
|
||||
Loading…
Reference in New Issue
Block a user