diff --git a/src/ElectronicPharmacy/App.config b/src/ElectronicPharmacy/App.config
index 6eae6c4..761ce75 100644
--- a/src/ElectronicPharmacy/App.config
+++ b/src/ElectronicPharmacy/App.config
@@ -36,6 +36,9 @@
+
+
+
Электронная_Фармация.Properties.Settings>
\ No newline at end of file
diff --git a/src/ElectronicPharmacy/Classes/AppConfig.cs b/src/ElectronicPharmacy/Classes/AppConfig.cs
index f606bcd..459c3dc 100644
--- a/src/ElectronicPharmacy/Classes/AppConfig.cs
+++ b/src/ElectronicPharmacy/Classes/AppConfig.cs
@@ -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();
+ }
}
}
diff --git a/src/ElectronicPharmacy/Classes/InvoiceExportService.cs b/src/ElectronicPharmacy/Classes/InvoiceExportService.cs
new file mode 100644
index 0000000..39025fe
--- /dev/null
+++ b/src/ElectronicPharmacy/Classes/InvoiceExportService.cs
@@ -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
+{
+ ///
+ /// Exports a local invoice (header + items) to a CSV file in the configured folder.
+ ///
+ 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; }
+ }
+ }
+}
diff --git a/src/ElectronicPharmacy/ElectronicPharmacy.csproj b/src/ElectronicPharmacy/ElectronicPharmacy.csproj
index aace520..fd4daf9 100644
--- a/src/ElectronicPharmacy/ElectronicPharmacy.csproj
+++ b/src/ElectronicPharmacy/ElectronicPharmacy.csproj
@@ -106,6 +106,7 @@
+
diff --git a/src/ElectronicPharmacy/HelpForms/HF_Registration.Designer.cs b/src/ElectronicPharmacy/HelpForms/HF_Registration.Designer.cs
index b9a93d7..3447438 100644
--- a/src/ElectronicPharmacy/HelpForms/HF_Registration.Designer.cs
+++ b/src/ElectronicPharmacy/HelpForms/HF_Registration.Designer.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;
}
}
diff --git a/src/ElectronicPharmacy/HelpForms/HF_Registration.cs b/src/ElectronicPharmacy/HelpForms/HF_Registration.cs
index 8069c1f..74cc942 100644
--- a/src/ElectronicPharmacy/HelpForms/HF_Registration.cs
+++ b/src/ElectronicPharmacy/HelpForms/HF_Registration.cs
@@ -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;
}
diff --git a/src/ElectronicPharmacy/Properties/Settings.Designer.cs b/src/ElectronicPharmacy/Properties/Settings.Designer.cs
index c582df8..61c34aa 100644
--- a/src/ElectronicPharmacy/Properties/Settings.Designer.cs
+++ b/src/ElectronicPharmacy/Properties/Settings.Designer.cs
@@ -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;
+ }
+ }
+
}
}
diff --git a/src/ElectronicPharmacy/Properties/Settings.settings b/src/ElectronicPharmacy/Properties/Settings.settings
index b2c624d..b4863ca 100644
--- a/src/ElectronicPharmacy/Properties/Settings.settings
+++ b/src/ElectronicPharmacy/Properties/Settings.settings
@@ -26,5 +26,8 @@
False
+
+
+
\ No newline at end of file
diff --git a/src/ElectronicPharmacy/UserControls/UCInvoices.cs b/src/ElectronicPharmacy/UserControls/UCInvoices.cs
index 05fb61a..ba51fdc 100644
--- a/src/ElectronicPharmacy/UserControls/UCInvoices.cs
+++ b/src/ElectronicPharmacy/UserControls/UCInvoices.cs
@@ -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)