diff --git a/dist/Рлектронная-Фармация/Elfisa.UI.dll b/dist/Рлектронная-Фармация/Elfisa.UI.dll
index cb04318..e17bc77 100644
Binary files a/dist/Рлектронная-Фармация/Elfisa.UI.dll and b/dist/Рлектронная-Фармация/Elfisa.UI.dll differ
diff --git a/dist/Рлектронная-Фармация/Elfisa.UI.pdb b/dist/Рлектронная-Фармация/Elfisa.UI.pdb
index af1bdf3..a900d5b 100644
Binary files a/dist/Рлектронная-Фармация/Elfisa.UI.pdb and b/dist/Рлектронная-Фармация/Elfisa.UI.pdb differ
diff --git a/dist/Рлектронная-Фармация/Электронная Фармация.exe b/dist/Рлектронная-Фармация/Электронная Фармация.exe
index 77e7afe..bbda518 100644
Binary files a/dist/Рлектронная-Фармация/Электронная Фармация.exe and b/dist/Рлектронная-Фармация/Электронная Фармация.exe differ
diff --git a/dist/Рлектронная-Фармация/Электронная Фармация.pdb b/dist/Рлектронная-Фармация/Электронная Фармация.pdb
index 6616477..a442355 100644
Binary files a/dist/Рлектронная-Фармация/Электронная Фармация.pdb and b/dist/Рлектронная-Фармация/Электронная Фармация.pdb differ
diff --git a/src/ElectronicPharmacy/Classes/ApiClient.cs b/src/ElectronicPharmacy/Classes/ApiClient.cs
index f121e08..f955928 100644
--- a/src/ElectronicPharmacy/Classes/ApiClient.cs
+++ b/src/ElectronicPharmacy/Classes/ApiClient.cs
@@ -1,38 +1,38 @@
using System;
using System.Collections.Generic;
-using System.Linq;
+using System.Diagnostics;
using System.Net.Http;
+using System.Net.Http.Headers;
using System.Text;
using System.Threading.Tasks;
-using System.Net.Http.Json;
using System.Text.Json;
-using System.Text.Json.Serialization;
namespace Электронная_Фармация.Classes
{
public class ApiClient
{
+ private static readonly JsonSerializerOptions JsonOptions = new JsonSerializerOptions
+ {
+ PropertyNamingPolicy = null
+ };
+
private readonly HttpClient _httpClient;
private readonly string _baseUrl;
private string _token;
public ApiClient(string baseUrl = null)
{
- _baseUrl = (baseUrl ?? AppConfig.ApiBaseUrl).TrimEnd('/');
+ _baseUrl = AppConfig.NormalizeApiBaseUrl(baseUrl ?? AppConfig.ApiBaseUrl);
_httpClient = new HttpClient
{
- BaseAddress = new Uri(_baseUrl),
- Timeout = TimeSpan.FromSeconds(30)
+ Timeout = TimeSpan.FromSeconds(60)
};
+
+ AppDebugLog.Info("ApiClient", $"Создан клиент. BaseUrl={_baseUrl}, Timeout={_httpClient.Timeout.TotalSeconds}s");
}
- ///
- /// Выполняет авторизацию и сохраняет токен
- ///
- /// Имя пользователя
- /// Пароль
- /// JWT токен
- /// При ошибке авторизации
+ public string BaseUrl => _baseUrl;
+
public async Task LoginAsync(string username, string password)
{
var request = new LoginRequest
@@ -41,54 +41,65 @@ namespace Электронная_Фармация.Classes
Password = password
};
+ var path = "/auth/login";
+ var fullUrl = BuildFullUrl(path);
+ var requestJson = JsonSerializer.Serialize(request, JsonOptions);
+ var stopwatch = Stopwatch.StartNew();
+
+ AppDebugLog.ApiRequest("POST", fullUrl, requestJson, $"username={username}");
+
try
{
- var response = await _httpClient.PostAsJsonAsync("/auth/login", request);
+ using (var httpRequest = new HttpRequestMessage(HttpMethod.Post, fullUrl))
+ {
+ httpRequest.Content = new StringContent(requestJson, Encoding.UTF8, "application/json");
+ var response = await _httpClient.SendAsync(httpRequest);
+ var responseBody = await response.Content.ReadAsStringAsync();
+ stopwatch.Stop();
- if (response.IsSuccessStatusCode)
- {
- var loginResponse = await response.Content.ReadFromJsonAsync();
- _token = loginResponse.Token;
- return _token;
- }
- else if (response.StatusCode == System.Net.HttpStatusCode.Unauthorized)
- {
- var error = await response.Content.ReadFromJsonAsync();
- throw new UnauthorizedAccessException($"Ошибка авторизации: {error?.Error ?? "Неверные учетные данные"}");
- }
- else
- {
- var error = await response.Content.ReadFromJsonAsync();
- throw new HttpRequestException($"Ошибка HTTP {response.StatusCode}: {error?.Error ?? "Неизвестная ошибка"}");
+ AppDebugLog.ApiResponse("POST", fullUrl, (int)response.StatusCode, stopwatch.ElapsedMilliseconds, responseBody);
+ return ParseLoginResponse(response, responseBody);
}
}
- catch (TaskCanceledException)
+ catch (HttpRequestException ex) when (!ex.Message.StartsWith("Ошибка HTTP"))
{
- throw new HttpRequestException("Превышено время ожидания ответа сервера");
+ stopwatch.Stop();
+ AppDebugLog.ApiFailure("POST", fullUrl, stopwatch.ElapsedMilliseconds, ex);
+ throw WrapNetworkError(ex, "авторизацию");
+ }
+ catch (TaskCanceledException ex)
+ {
+ stopwatch.Stop();
+ AppDebugLog.ApiFailure("POST", fullUrl, stopwatch.ElapsedMilliseconds, ex);
+ throw WrapNetworkError(ex, "авторизацию");
+ }
+ catch (Exception ex)
+ {
+ stopwatch.Stop();
+ if (ex is UnauthorizedAccessException || ex is HttpRequestException)
+ {
+ AppDebugLog.ApiFailure("POST", fullUrl, stopwatch.ElapsedMilliseconds, ex);
+ throw;
+ }
+
+ AppDebugLog.ApiFailure("POST", fullUrl, stopwatch.ElapsedMilliseconds, ex);
+ throw new HttpRequestException($"Ошибка при авторизации ({_baseUrl}): {ex.Message}", ex);
}
}
- ///
- /// Устанавливает токен для последующих запросов
- ///
public void SetToken(string token)
{
- _token = token;
+ _token = string.IsNullOrWhiteSpace(token) ? null : token;
+ AppDebugLog.Info("ApiClient", $"Токен установлен: {AppDebugLog.MaskToken(_token)}");
}
public string Token => _token;
- ///
- /// Проверяет, авторизован ли клиент
- ///
public bool IsAuthenticated => !string.IsNullOrEmpty(_token);
public async Task GetPriceSummaryAsync(string supplierId = null, string regionId = null)
{
- if (!IsAuthenticated)
- {
- throw new InvalidOperationException("Необходимо выполнить авторизацию перед запросом данных");
- }
+ EnsureAuthenticated();
var queryParams = new List();
if (!string.IsNullOrEmpty(supplierId))
@@ -100,64 +111,147 @@ namespace Электронная_Фармация.Classes
queryParams.Add($"region_id={Uri.EscapeDataString(regionId)}");
}
- var url = "/api/supplier-prices/summary";
+ var path = "/api/supplier-prices/summary";
if (queryParams.Count > 0)
{
- url += "?" + string.Join("&", queryParams);
+ path += "?" + string.Join("&", queryParams);
}
- _httpClient.DefaultRequestHeaders.Authorization =
- new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", _token);
-
- try
- {
- var response = await _httpClient.GetAsync(url);
-
- if (response.IsSuccessStatusCode)
- {
- var summary = await response.Content.ReadFromJsonAsync();
- return summary;
- }
- else if (response.StatusCode == System.Net.HttpStatusCode.Unauthorized)
- {
- _token = null; // Токен истек или невалиден
- var error = await response.Content.ReadFromJsonAsync();
- throw new UnauthorizedAccessException($"Сессия истекла: {error?.Error ?? "Необходимо войти заново"}");
- }
- else
- {
- var error = await response.Content.ReadFromJsonAsync();
- throw new HttpRequestException($"Ошибка HTTP {response.StatusCode}: {error?.Error ?? "Неизвестная ошибка"}");
- }
-
-
-
- }
- catch (TaskCanceledException)
- {
- throw new HttpRequestException("Превышено время ожидания ответа сервера (таймаут 30 секунд)");
- }
+ var responseBody = await SendAuthorizedGetAsync(path, "загрузку прайса");
+ var summary = JsonSerializer.Deserialize(responseBody, JsonOptions);
+ var count = summary?.Summary?.Count ?? 0;
+ AppDebugLog.Info("ApiClient", $"Прайс получен: {count} позиций");
+ return summary;
}
public async Task> GetInvoicesAsync()
+ {
+ EnsureAuthenticated();
+
+ var responseBody = await SendAuthorizedGetAsync("/api/buyer/invoices", "загрузку накладных");
+ var items = JsonSerializer.Deserialize>(responseBody, JsonOptions) ?? new List();
+ AppDebugLog.Info("ApiClient", $"Накладные получены: {items.Count} шт.");
+ return items;
+ }
+
+ private void EnsureAuthenticated()
{
if (!IsAuthenticated)
{
- throw new InvalidOperationException("Необходимо выполнить авторизацию перед запросом накладных");
+ throw new InvalidOperationException("Необходимо выполнить авторизацию перед запросом данных");
}
+ }
- _httpClient.DefaultRequestHeaders.Authorization =
- new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", _token);
+ private async Task SendAuthorizedGetAsync(string path, string operationName)
+ {
+ var fullUrl = BuildFullUrl(path);
+ var stopwatch = Stopwatch.StartNew();
- var response = await _httpClient.GetAsync("/api/buyer/invoices");
- var body = await response.Content.ReadAsStringAsync();
- if (!response.IsSuccessStatusCode)
+ AppDebugLog.ApiRequest("GET", fullUrl, extra: $"token={AppDebugLog.MaskToken(_token)}");
+
+ try
{
- throw new HttpRequestException(
- $"Ошибка загрузки накладных: {(int)response.StatusCode} {response.ReasonPhrase}\n{body}");
+ using (var httpRequest = new HttpRequestMessage(HttpMethod.Get, fullUrl))
+ {
+ httpRequest.Headers.Authorization = new AuthenticationHeaderValue("Bearer", _token);
+ var response = await _httpClient.SendAsync(httpRequest);
+ var responseBody = await response.Content.ReadAsStringAsync();
+ stopwatch.Stop();
+
+ AppDebugLog.ApiResponse("GET", fullUrl, (int)response.StatusCode, stopwatch.ElapsedMilliseconds, responseBody);
+
+ if (response.IsSuccessStatusCode)
+ {
+ return responseBody;
+ }
+
+ if (response.StatusCode == System.Net.HttpStatusCode.Unauthorized)
+ {
+ _token = null;
+ var error = TryDeserializeError(responseBody);
+ throw new UnauthorizedAccessException($"Сессия истекла: {error?.Error ?? "Необходимо войти заново"}");
+ }
+
+ var httpError = TryDeserializeError(responseBody);
+ throw new HttpRequestException($"Ошибка HTTP {response.StatusCode}: {httpError?.Error ?? responseBody}");
+ }
+ }
+ catch (HttpRequestException ex) when (!ex.Message.StartsWith("Ошибка HTTP"))
+ {
+ stopwatch.Stop();
+ AppDebugLog.ApiFailure("GET", fullUrl, stopwatch.ElapsedMilliseconds, ex);
+ throw WrapNetworkError(ex, operationName);
+ }
+ catch (TaskCanceledException ex)
+ {
+ stopwatch.Stop();
+ AppDebugLog.ApiFailure("GET", fullUrl, stopwatch.ElapsedMilliseconds, ex);
+ throw WrapNetworkError(ex, operationName);
+ }
+ catch (Exception ex) when (!(ex is UnauthorizedAccessException) && !(ex is HttpRequestException))
+ {
+ stopwatch.Stop();
+ AppDebugLog.ApiFailure("GET", fullUrl, stopwatch.ElapsedMilliseconds, ex);
+ throw;
+ }
+ }
+
+ private string ParseLoginResponse(HttpResponseMessage response, string responseBody)
+ {
+ if (response.IsSuccessStatusCode)
+ {
+ var loginResponse = JsonSerializer.Deserialize(responseBody, JsonOptions);
+ if (loginResponse == null || string.IsNullOrWhiteSpace(loginResponse.Token))
+ {
+ throw new HttpRequestException($"Сервер {_baseUrl} вернул пустой токен авторизации");
+ }
+
+ _token = loginResponse.Token;
+ AppDebugLog.Info("ApiClient", $"Авторизация успешна. token={AppDebugLog.MaskToken(_token)}");
+ return _token;
}
- return JsonSerializer.Deserialize>(body) ?? new List();
+ if (response.StatusCode == System.Net.HttpStatusCode.Unauthorized)
+ {
+ var error = TryDeserializeError(responseBody);
+ throw new UnauthorizedAccessException($"Ошибка авторизации: {error?.Error ?? "Неверные учетные данные"}");
+ }
+
+ var httpError = TryDeserializeError(responseBody);
+ throw new HttpRequestException($"Ошибка HTTP {response.StatusCode}: {httpError?.Error ?? responseBody}");
+ }
+
+ private string BuildFullUrl(string path)
+ {
+ return new Uri(new Uri(_baseUrl + "/"), path.TrimStart('/')).ToString();
+ }
+
+ private static ErrorResponse TryDeserializeError(string body)
+ {
+ if (string.IsNullOrWhiteSpace(body))
+ {
+ return null;
+ }
+
+ try
+ {
+ return JsonSerializer.Deserialize(body, JsonOptions);
+ }
+ catch
+ {
+ return null;
+ }
+ }
+
+ private HttpRequestException WrapNetworkError(Exception ex, string operation)
+ {
+ var details = ex.InnerException != null && ex.InnerException.Message != ex.Message
+ ? $" ({ex.InnerException.Message})"
+ : string.Empty;
+
+ return new HttpRequestException(
+ $"Не удалось выполнить {operation}. Сервер: {_baseUrl}{details}. {ex.Message}",
+ ex);
}
}
diff --git a/src/ElectronicPharmacy/Classes/AppConfig.cs b/src/ElectronicPharmacy/Classes/AppConfig.cs
index d2cc970..505bc28 100644
--- a/src/ElectronicPharmacy/Classes/AppConfig.cs
+++ b/src/ElectronicPharmacy/Classes/AppConfig.cs
@@ -9,9 +9,32 @@ namespace Электронная_Фармация.Classes
///
public static class AppConfig
{
- public const string DefaultApiBaseUrl = "http://195.34.241.84:9988";
+ public const string DefaultApiBaseUrl = "https://24pharmdata.ru";
+ private const string LegacyApiBaseUrl = "http://195.34.241.84:9988";
private const string ApiBaseUrlEnvVar = "ELF_API_BASE_URL";
+ ///
+ /// Replaces known dead API URLs in saved user settings.
+ ///
+ public static void MigrateLegacySettings()
+ {
+ var current = Settings.Default.ApiBaseUrl;
+ if (string.IsNullOrWhiteSpace(current))
+ {
+ return;
+ }
+
+ var normalized = NormalizeApiBaseUrl(current);
+ if (string.Equals(normalized, LegacyApiBaseUrl, StringComparison.OrdinalIgnoreCase) ||
+ string.Equals(normalized, "http://195.34.241.84:9988", StringComparison.OrdinalIgnoreCase))
+ {
+ Settings.Default.ApiBaseUrl = DefaultApiBaseUrl;
+ Settings.Default.stringToken = string.Empty;
+ Settings.Default.Save();
+ AppDebugLog.Info("AppConfig", $"ApiBaseUrl мигрирован: {LegacyApiBaseUrl} -> {DefaultApiBaseUrl}");
+ }
+ }
+
public static string ApiBaseUrl
{
get
@@ -19,19 +42,36 @@ namespace Электронная_Фармация.Classes
var fromSettings = Settings.Default.ApiBaseUrl;
if (!string.IsNullOrWhiteSpace(fromSettings))
{
- return fromSettings.Trim().TrimEnd('/');
+ return NormalizeApiBaseUrl(fromSettings);
}
var fromEnv = Environment.GetEnvironmentVariable(ApiBaseUrlEnvVar);
if (!string.IsNullOrWhiteSpace(fromEnv))
{
- return fromEnv.Trim().TrimEnd('/');
+ return NormalizeApiBaseUrl(fromEnv);
}
return DefaultApiBaseUrl;
}
}
+ public static string NormalizeApiBaseUrl(string url)
+ {
+ if (string.IsNullOrWhiteSpace(url))
+ {
+ return DefaultApiBaseUrl;
+ }
+
+ url = url.Trim().TrimEnd('/');
+ if (!url.StartsWith("http://", StringComparison.OrdinalIgnoreCase) &&
+ !url.StartsWith("https://", StringComparison.OrdinalIgnoreCase))
+ {
+ url = "http://" + url;
+ }
+
+ return url;
+ }
+
public static string SqliteDbPath
{
get
diff --git a/src/ElectronicPharmacy/Classes/AppDebugLog.cs b/src/ElectronicPharmacy/Classes/AppDebugLog.cs
new file mode 100644
index 0000000..3974b87
--- /dev/null
+++ b/src/ElectronicPharmacy/Classes/AppDebugLog.cs
@@ -0,0 +1,202 @@
+using System;
+using System.Diagnostics;
+using System.IO;
+using System.Text;
+using System.Text.RegularExpressions;
+
+namespace Электронная_Фармация.Classes
+{
+ ///
+ /// File logger for API/auth/network debugging.
+ /// Logs are written to Logs/api-debug-YYYY-MM-DD.log next to the executable.
+ ///
+ public static class AppDebugLog
+ {
+ private static readonly object Sync = new object();
+ private const int MaxBodyLength = 4000;
+ private const string DisableEnvVar = "ELF_API_DEBUG";
+ private const string DisableMarkerFile = "api-debug.off";
+
+ public static bool IsEnabled
+ {
+ get
+ {
+ var env = Environment.GetEnvironmentVariable(DisableEnvVar);
+ if (string.Equals(env, "0", StringComparison.OrdinalIgnoreCase) ||
+ string.Equals(env, "false", StringComparison.OrdinalIgnoreCase))
+ {
+ return false;
+ }
+
+ var markerPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, DisableMarkerFile);
+ return !File.Exists(markerPath);
+ }
+ }
+
+ public static string LogDirectory =>
+ Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Logs");
+
+ public static string CurrentLogFilePath =>
+ Path.Combine(LogDirectory, $"api-debug-{DateTime.Now:yyyy-MM-dd}.log");
+
+ public static void Info(string category, string message)
+ {
+ Write("INFO", category, message, null);
+ }
+
+ public static void Warning(string category, string message)
+ {
+ Write("WARN", category, message, null);
+ }
+
+ public static void Error(string category, string message, Exception exception = null)
+ {
+ Write("ERROR", category, message, exception);
+ }
+
+ public static void ApiRequest(string method, string url, string requestBody = null, string extra = null)
+ {
+ var message = new StringBuilder();
+ message.Append(method).Append(" ").Append(url);
+ if (!string.IsNullOrWhiteSpace(extra))
+ {
+ message.Append(" | ").Append(extra);
+ }
+
+ if (!string.IsNullOrWhiteSpace(requestBody))
+ {
+ message.Append(" | body=").Append(Truncate(SanitizeJson(requestBody)));
+ }
+
+ Write("REQ", "Api", message.ToString(), null);
+ }
+
+ public static void ApiResponse(string method, string url, int statusCode, long elapsedMs, string responseBody = null)
+ {
+ var message = new StringBuilder();
+ message.Append(method).Append(" ").Append(url);
+ message.Append(" -> HTTP ").Append(statusCode);
+ message.Append(" (").Append(elapsedMs).Append(" ms)");
+
+ if (!string.IsNullOrWhiteSpace(responseBody))
+ {
+ message.Append(" | body=").Append(Truncate(SanitizeJson(responseBody)));
+ }
+
+ Write(statusCode >= 400 ? "RESP-ERR" : "RESP", "Api", message.ToString(), null);
+ }
+
+ public static void ApiFailure(string method, string url, long elapsedMs, Exception exception)
+ {
+ var message = $"{method} {url} failed after {elapsedMs} ms: {exception.Message}";
+ Write("FAIL", "Api", message, exception);
+ }
+
+ public static string MaskToken(string token)
+ {
+ if (string.IsNullOrWhiteSpace(token))
+ {
+ return "(empty)";
+ }
+
+ if (token.Length <= 8)
+ {
+ return "***";
+ }
+
+ return token.Substring(0, 4) + "..." + token.Substring(token.Length - 4);
+ }
+
+ public static string SanitizeJson(string json)
+ {
+ if (string.IsNullOrWhiteSpace(json))
+ {
+ return string.Empty;
+ }
+
+ var sanitized = Regex.Replace(
+ json,
+ @"""password""\s*:\s*""[^""]*""",
+ @"""password"":""***""",
+ RegexOptions.IgnoreCase);
+
+ sanitized = Regex.Replace(
+ sanitized,
+ @"""token""\s*:\s*""([^""]*)""",
+ m => @"""token"":""" + MaskToken(m.Groups[1].Value) + @"""",
+ RegexOptions.IgnoreCase);
+
+ return sanitized;
+ }
+
+ public static string FormatException(Exception exception)
+ {
+ if (exception == null)
+ {
+ return string.Empty;
+ }
+
+ var sb = new StringBuilder();
+ var current = exception;
+ var depth = 0;
+ while (current != null && depth < 8)
+ {
+ if (depth > 0)
+ {
+ sb.AppendLine();
+ sb.Append(" -> ");
+ }
+
+ sb.Append(current.GetType().Name).Append(": ").Append(current.Message);
+ current = current.InnerException;
+ depth++;
+ }
+
+ return sb.ToString();
+ }
+
+ private static void Write(string level, string category, string message, Exception exception)
+ {
+ if (!IsEnabled)
+ {
+ return;
+ }
+
+ try
+ {
+ var line = new StringBuilder();
+ line.Append(DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff"));
+ line.Append(" [").Append(level).Append("] ");
+ line.Append("[").Append(category).Append("] ");
+ line.Append(message);
+
+ if (exception != null)
+ {
+ line.Append(" | ").Append(FormatException(exception));
+ }
+
+ lock (Sync)
+ {
+ Directory.CreateDirectory(LogDirectory);
+ File.AppendAllText(CurrentLogFilePath, line + Environment.NewLine, Encoding.UTF8);
+ }
+
+ Debug.WriteLine(line.ToString());
+ }
+ catch
+ {
+ // Logging must never break the app.
+ }
+ }
+
+ private static string Truncate(string text)
+ {
+ if (string.IsNullOrEmpty(text) || text.Length <= MaxBodyLength)
+ {
+ return text ?? string.Empty;
+ }
+
+ return text.Substring(0, MaxBodyLength) + "...[truncated]";
+ }
+ }
+}
diff --git a/src/ElectronicPharmacy/Classes/DataSender.cs b/src/ElectronicPharmacy/Classes/DataSender.cs
index 9e13ae8..fb11e8d 100644
--- a/src/ElectronicPharmacy/Classes/DataSender.cs
+++ b/src/ElectronicPharmacy/Classes/DataSender.cs
@@ -197,6 +197,10 @@ WHERE Orders.OrderState = 'НОВЫЙ'
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
};
string json = JsonSerializer.Serialize(order, options);
+ var fullUrl = new Uri(new Uri(_baseUrl.TrimEnd('/') + "/"), BuyerOrdersEndpoint.TrimStart('/')).ToString();
+ var stopwatch = System.Diagnostics.Stopwatch.StartNew();
+
+ AppDebugLog.ApiRequest("POST", fullUrl, json, $"order={order.LocalOrderId}");
using (var client = new HttpClient())
{
@@ -204,18 +208,33 @@ WHERE Orders.OrderState = 'НОВЫЙ'
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", _token);
var content = new StringContent(json, Encoding.UTF8, "application/json");
- HttpResponseMessage response = await client.PostAsync(BuyerOrdersEndpoint, content);
- string responseString = await response.Content.ReadAsStringAsync();
-
- if (!response.IsSuccessStatusCode)
+ try
{
- ToastNotification.ShowError(
- $"Ошибка отправки заказа {order.LocalOrderId}: {(int)response.StatusCode} {response.ReasonPhrase}\n{responseString}");
+ HttpResponseMessage response = await client.PostAsync(BuyerOrdersEndpoint, content);
+ string responseString = await response.Content.ReadAsStringAsync();
+ stopwatch.Stop();
+
+ AppDebugLog.ApiResponse("POST", fullUrl, (int)response.StatusCode, stopwatch.ElapsedMilliseconds, responseString);
+
+ if (!response.IsSuccessStatusCode)
+ {
+ AppDebugLog.Error("DataSender", $"Ошибка отправки заказа {order.LocalOrderId}");
+ ToastNotification.ShowError(
+ $"Ошибка отправки заказа {order.LocalOrderId}: {(int)response.StatusCode} {response.ReasonPhrase}\n{responseString}");
+ return false;
+ }
+
+ MarkOrderSent(order.LocalOrderId);
+ AppDebugLog.Info("DataSender", $"Заказ {order.LocalOrderId} отправлен");
+ return true;
+ }
+ catch (Exception ex)
+ {
+ stopwatch.Stop();
+ AppDebugLog.ApiFailure("POST", fullUrl, stopwatch.ElapsedMilliseconds, ex);
+ ToastNotification.ShowError($"Ошибка сети при отправке заказа {order.LocalOrderId}: {ex.Message}");
return false;
}
-
- MarkOrderSent(order.LocalOrderId);
- return true;
}
}
diff --git a/src/ElectronicPharmacy/Classes/InvoiceRequestService.cs b/src/ElectronicPharmacy/Classes/InvoiceRequestService.cs
new file mode 100644
index 0000000..e4a37f7
--- /dev/null
+++ b/src/ElectronicPharmacy/Classes/InvoiceRequestService.cs
@@ -0,0 +1,340 @@
+using System;
+using System.Collections.Generic;
+using System.Data.SQLite;
+using System.Globalization;
+
+namespace Электронная_Фармация.Classes
+{
+ public sealed class InvoiceRequestService
+ {
+ public string CreateOrUpdateFromOrder(string orderId)
+ {
+ if (string.IsNullOrWhiteSpace(orderId))
+ {
+ throw new ArgumentException("Не указан заказ для формирования накладной.", nameof(orderId));
+ }
+
+ using (var con = new SQLiteConnection(AppConfig.SqliteConnectionString))
+ {
+ con.Open();
+
+ string orderNumber;
+ string supplierName;
+ string consigneeName;
+ string orderState;
+
+ using (var cmd = new SQLiteCommand(@"
+select
+ o.[OrderNumber],
+ o.[SupplierName],
+ ifnull(o.[ConsigneeName], '') as [ConsigneeName],
+ ifnull(o.[OrderState], '') as [OrderState]
+from [Orders] o
+where o.[Id_Order] = @orderId;", con))
+ {
+ cmd.Parameters.AddWithValue("@orderId", orderId);
+ using (var reader = cmd.ExecuteReader())
+ {
+ if (!reader.Read())
+ {
+ throw new InvalidOperationException("Выбранный заказ не найден.");
+ }
+
+ orderNumber = reader["OrderNumber"]?.ToString() ?? string.Empty;
+ supplierName = reader["SupplierName"]?.ToString() ?? string.Empty;
+ consigneeName = reader["ConsigneeName"]?.ToString() ?? string.Empty;
+ orderState = reader["OrderState"]?.ToString() ?? string.Empty;
+ }
+ }
+
+ if (!string.Equals(orderState, "ОТПРАВЛЕН", StringComparison.OrdinalIgnoreCase))
+ {
+ throw new InvalidOperationException("Получить накладную можно только для заказа со статусом ОТПРАВЛЕН.");
+ }
+
+ var items = new List();
+ using (var cmd = new SQLiteCommand(@"
+select
+ ifnull([es_code], 0) as [GoodCode],
+ [DrugName],
+ [Price],
+ ifnull([Zakaz], 0) as [Zakaz],
+ ifnull([RefuseItemsCount], 0) as [RefuseItemsCount],
+ ifnull([ExpiryPeriod], '') as [ExpiryPeriod]
+from [OrderItems]
+where [Id_Order] = @orderId
+order by [DrugName];", con))
+ {
+ cmd.Parameters.AddWithValue("@orderId", orderId);
+ using (var reader = cmd.ExecuteReader())
+ {
+ while (reader.Read())
+ {
+ int orderedQty = SafeInt(reader["Zakaz"]);
+ int refusedQty = SafeInt(reader["RefuseItemsCount"]);
+ int arrivedQty = Math.Max(orderedQty - refusedQty, 0);
+ if (arrivedQty <= 0)
+ {
+ continue;
+ }
+
+ var price = SafeDecimal(reader["Price"]);
+ items.Add(new InvoiceOrderItem
+ {
+ GoodCode = SafeInt(reader["GoodCode"]),
+ GoodName = reader["DrugName"]?.ToString() ?? string.Empty,
+ Price = price,
+ ArrivedQty = arrivedQty,
+ ExpiryPeriod = reader["ExpiryPeriod"]?.ToString() ?? string.Empty
+ });
+ }
+ }
+ }
+
+ if (items.Count == 0)
+ {
+ throw new InvalidOperationException("Для этого заказа нет фактически пришедших позиций.");
+ }
+
+ int invoiceId;
+ string invoiceNumber;
+ using (var findCmd = new SQLiteCommand(@"
+select [idInvoice], [InvoiceNumber]
+from [Invoice]
+where [SourceOrderId] = @orderId
+limit 1;", con))
+ {
+ findCmd.Parameters.AddWithValue("@orderId", orderId);
+ using (var reader = findCmd.ExecuteReader())
+ {
+ if (reader.Read())
+ {
+ invoiceId = Convert.ToInt32(reader["idInvoice"]);
+ invoiceNumber = reader["InvoiceNumber"]?.ToString() ?? string.Empty;
+ }
+ else
+ {
+ invoiceId = 0;
+ invoiceNumber = $"НКЛ-{orderNumber}";
+ }
+ }
+ }
+
+ var invoiceDate = DateTime.Now.ToString("yyyy-MM-dd");
+ var invoiceSum = 0m;
+ foreach (var item in items)
+ {
+ invoiceSum += item.Price * item.ArrivedQty;
+ }
+
+ if (invoiceId == 0)
+ {
+ using (var insertCmd = new SQLiteCommand(@"
+insert into [Invoice]
+(
+ [InvoiceNumber],
+ [InvoiceDate],
+ [CodeSupplier],
+ [CodeConsignees],
+ [SumWithoutNDS],
+ [SumWithNDS],
+ [SumNDS],
+ [SupplierName],
+ [ConsigneesName],
+ [InvoiceSum],
+ [RefuseSum],
+ [SourceOrderId],
+ [SourceOrderNumber],
+ [IsRequestedInvoice]
+)
+values
+(
+ @number,
+ @date,
+ 0,
+ 0,
+ @sum,
+ @sum,
+ 0,
+ @supplier,
+ @consignee,
+ @invoiceSum,
+ 0,
+ @sourceOrderId,
+ @sourceOrderNumber,
+ 1
+);
+select last_insert_rowid();", con))
+ {
+ insertCmd.Parameters.AddWithValue("@number", invoiceNumber);
+ insertCmd.Parameters.AddWithValue("@date", invoiceDate);
+ insertCmd.Parameters.AddWithValue("@sum", invoiceSum);
+ insertCmd.Parameters.AddWithValue("@supplier", supplierName);
+ insertCmd.Parameters.AddWithValue("@consignee", consigneeName);
+ insertCmd.Parameters.AddWithValue("@invoiceSum", invoiceSum);
+ insertCmd.Parameters.AddWithValue("@sourceOrderId", orderId);
+ insertCmd.Parameters.AddWithValue("@sourceOrderNumber", orderNumber);
+ invoiceId = Convert.ToInt32(insertCmd.ExecuteScalar());
+ }
+ }
+ else
+ {
+ using (var updateCmd = new SQLiteCommand(@"
+update [Invoice]
+set
+ [InvoiceDate] = @date,
+ [SumWithoutNDS] = @sum,
+ [SumWithNDS] = @sum,
+ [SumNDS] = 0,
+ [SupplierName] = @supplier,
+ [ConsigneesName] = @consignee,
+ [InvoiceSum] = @invoiceSum,
+ [RefuseSum] = 0,
+ [SourceOrderNumber] = @sourceOrderNumber,
+ [IsRequestedInvoice] = 1
+where [idInvoice] = @invoiceId;", con))
+ {
+ updateCmd.Parameters.AddWithValue("@date", invoiceDate);
+ updateCmd.Parameters.AddWithValue("@sum", invoiceSum);
+ updateCmd.Parameters.AddWithValue("@supplier", supplierName);
+ updateCmd.Parameters.AddWithValue("@consignee", consigneeName);
+ updateCmd.Parameters.AddWithValue("@invoiceSum", invoiceSum);
+ updateCmd.Parameters.AddWithValue("@sourceOrderNumber", orderNumber);
+ updateCmd.Parameters.AddWithValue("@invoiceId", invoiceId);
+ updateCmd.ExecuteNonQuery();
+ }
+
+ using (var deleteItemsCmd = new SQLiteCommand("delete from [InvoiceItem] where [idInvoice] = @invoiceId;", con))
+ {
+ deleteItemsCmd.Parameters.AddWithValue("@invoiceId", invoiceId);
+ deleteItemsCmd.ExecuteNonQuery();
+ }
+ }
+
+ foreach (var item in items)
+ {
+ decimal lineSum = item.Price * item.ArrivedQty;
+ using (var itemCmd = new SQLiteCommand(@"
+insert into [InvoiceItem]
+(
+ [idGood],
+ [GoodCode],
+ [Good],
+ [ProducerName],
+ [CountryName],
+ [idInvoice],
+ [GoodsGount],
+ [PriceSupplierWithoutNDS],
+ [PriceSupplierWithNDS],
+ [PriceProducerWithoutNDS],
+ [PriceProducerWithNDS],
+ [NDS],
+ [JNVLS],
+ [PriceReestr],
+ [SumWithoutNDS],
+ [SumWithNDS],
+ [SumNDS],
+ [Serial],
+ [BestBefore],
+ [Sertificate],
+ [Marked],
+ [GTIN]
+)
+values
+(
+ null,
+ @goodCode,
+ @good,
+ '',
+ '',
+ @invoiceId,
+ @qty,
+ @price,
+ @price,
+ null,
+ null,
+ 0,
+ 0,
+ null,
+ @sum,
+ @sum,
+ 0,
+ '',
+ @bestBefore,
+ '',
+ 0,
+ ''
+);", con))
+ {
+ itemCmd.Parameters.AddWithValue("@goodCode", item.GoodCode);
+ itemCmd.Parameters.AddWithValue("@good", item.GoodName);
+ itemCmd.Parameters.AddWithValue("@invoiceId", invoiceId);
+ itemCmd.Parameters.AddWithValue("@qty", item.ArrivedQty);
+ itemCmd.Parameters.AddWithValue("@price", item.Price);
+ itemCmd.Parameters.AddWithValue("@sum", lineSum);
+ itemCmd.Parameters.AddWithValue("@bestBefore", item.ExpiryPeriod);
+ itemCmd.ExecuteNonQuery();
+ }
+ }
+
+ return orderId;
+ }
+ }
+
+ private static int SafeInt(object value)
+ {
+ if (value == null || value == DBNull.Value)
+ {
+ return 0;
+ }
+
+ int result;
+ return int.TryParse(value.ToString(), NumberStyles.Integer, CultureInfo.InvariantCulture, out result)
+ || int.TryParse(value.ToString(), NumberStyles.Integer, CultureInfo.CurrentCulture, out result)
+ ? result
+ : 0;
+ }
+
+ private static decimal SafeDecimal(object value)
+ {
+ if (value == null || value == DBNull.Value)
+ {
+ return 0m;
+ }
+
+ var text = value.ToString()?.Trim();
+ if (string.IsNullOrWhiteSpace(text))
+ {
+ return 0m;
+ }
+
+ decimal result;
+ if (decimal.TryParse(text, NumberStyles.Number, CultureInfo.InvariantCulture, out result))
+ {
+ return result;
+ }
+
+ text = text.Replace(',', '.');
+ if (decimal.TryParse(text, NumberStyles.Number, CultureInfo.InvariantCulture, out result))
+ {
+ return result;
+ }
+
+ if (decimal.TryParse(text, NumberStyles.Number, CultureInfo.CurrentCulture, out result))
+ {
+ return result;
+ }
+
+ return 0m;
+ }
+
+ private sealed class InvoiceOrderItem
+ {
+ public int GoodCode { get; set; }
+ public string GoodName { get; set; }
+ public decimal Price { get; set; }
+ public int ArrivedQty { get; set; }
+ public string ExpiryPeriod { get; set; }
+ }
+ }
+}
diff --git a/src/ElectronicPharmacy/Classes/UiThemeHelper.cs b/src/ElectronicPharmacy/Classes/UiThemeHelper.cs
index e73634a..b8ab6d0 100644
--- a/src/ElectronicPharmacy/Classes/UiThemeHelper.cs
+++ b/src/ElectronicPharmacy/Classes/UiThemeHelper.cs
@@ -75,6 +75,14 @@ namespace Электронная_Фармация.Classes
textBox.BorderStyle = BorderStyle.FixedSingle;
textBox.Font = theme.FontRegular13;
}
+ else if (control is RichTextBox richTextBox)
+ {
+ richTextBox.BackColor = theme.BackgroundPrimary;
+ richTextBox.ForeColor = theme.TextPrimary;
+ richTextBox.BorderStyle = BorderStyle.None;
+ richTextBox.Font = theme.FontRegular13;
+ richTextBox.ReadOnly = true;
+ }
else if (control is NumericUpDown numeric)
{
numeric.BackColor = theme.InputBackground;
diff --git a/src/ElectronicPharmacy/ElectronicPharmacy.csproj b/src/ElectronicPharmacy/ElectronicPharmacy.csproj
index 9e67665..68e384c 100644
--- a/src/ElectronicPharmacy/ElectronicPharmacy.csproj
+++ b/src/ElectronicPharmacy/ElectronicPharmacy.csproj
@@ -103,8 +103,10 @@
+
+
diff --git a/src/ElectronicPharmacy/Form1.Designer.cs b/src/ElectronicPharmacy/Form1.Designer.cs
index 1e3da04..92c74e5 100644
--- a/src/ElectronicPharmacy/Form1.Designer.cs
+++ b/src/ElectronicPharmacy/Form1.Designer.cs
@@ -230,7 +230,7 @@
this.tsItemInvoices.Name = "tsItemInvoices";
this.tsItemInvoices.Size = new System.Drawing.Size(224, 32);
this.tsItemInvoices.Text = "Накладные";
- this.tsItemInvoices.Visible = false;
+ this.tsItemInvoices.Visible = true;
this.tsItemInvoices.Click += new System.EventHandler(this.tsItemInvoices_Click);
//
// toolStripSeparator1
diff --git a/src/ElectronicPharmacy/Form1.UiShell.cs b/src/ElectronicPharmacy/Form1.UiShell.cs
index da3ddd5..5c6a08d 100644
--- a/src/ElectronicPharmacy/Form1.UiShell.cs
+++ b/src/ElectronicPharmacy/Form1.UiShell.cs
@@ -51,6 +51,7 @@ namespace Электронная_Фармация
{
new SidebarItem { Key = "price", Text = "ПРАЙС-ЛИСТ", IconGlyph = "\uE8FD" },
new SidebarItem { Key = "orders", Text = "ЗАКАЗЫ", IconGlyph = "\uE719" },
+ new SidebarItem { Key = "invoices", Text = "НАКЛАДНЫЕ", IconGlyph = "\uE8A5" },
new SidebarItem { Key = "upload", Text = "ЗАГРУЗИТЬ", IconGlyph = "\uE896" },
new SidebarItem { Key = "send", Text = "ОТПРАВИТЬ", IconGlyph = "\uE725" },
new SidebarItem { Key = "refusals", Text = "ОТКАЗЫ", IconGlyph = "\uE711" },
@@ -207,6 +208,9 @@ namespace Электронная_Фармация
case "orders":
OpenOrdersTab();
break;
+ case "invoices":
+ OpenInvoicesTab();
+ break;
case "upload":
tsBtnDownloadData_Click(this, EventArgs.Empty);
_appHeader.SetActive("upload");
@@ -300,6 +304,11 @@ namespace Электронная_Фармация
return;
}
+ if (sender is Control clicked && HasDedicatedContextMenu(clicked))
+ {
+ return;
+ }
+
_documentCloseTabMenuItem.Enabled = !string.IsNullOrEmpty(_documentTabStrip.ActiveTabId);
_documentCloseAllTabsMenuItem.Enabled = _documentTabStrip.Tabs.Count > 0;
@@ -307,6 +316,19 @@ namespace Электронная_Фармация
_documentTabMenu.Show(screenPoint);
}
+ private static bool HasDedicatedContextMenu(Control control)
+ {
+ for (var current = control; current != null; current = current.Parent)
+ {
+ if (current.ContextMenuStrip != null)
+ {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
private void CloseDocumentTab(string tabId)
{
if (_documentContents.TryGetValue(tabId, out var control))
@@ -343,6 +365,24 @@ namespace Электронная_Фармация
orders.Show();
}
+ public void OpenInvoicesTab(string sourceOrderId = null)
+ {
+ var existing = _documentTabStrip.FindByNavKey("invoices");
+ if (existing != null &&
+ _documentContents.TryGetValue(existing.Id, out var existingControl) &&
+ existingControl is UCInvoices existingInvoices)
+ {
+ existingInvoices.SetSourceOrderFilter(sourceOrderId);
+ _documentTabStrip.SelectTab(existing.Id);
+ ShowDocumentTab(existing.Id);
+ _appHeader.SetActive("invoices");
+ return;
+ }
+
+ var invoices = new UCInvoices(sourceOrderId);
+ OpenDocumentTab("Накладные", "invoices", invoices, allowDuplicate: false);
+ }
+
private void OpenRefusalsTab()
{
var refusals = new UCRefusals();
diff --git a/src/ElectronicPharmacy/Form1.cs b/src/ElectronicPharmacy/Form1.cs
index da97301..b3b25b9 100644
--- a/src/ElectronicPharmacy/Form1.cs
+++ b/src/ElectronicPharmacy/Form1.cs
@@ -146,8 +146,7 @@ namespace Электронная_Фармация
private void tsItemInvoices_Click(object sender, EventArgs e)
{
- var ucInvoices = new UCInvoices();
- OpenDocumentTab("Накладные", "invoices", ucInvoices, allowDuplicate: true);
+ OpenInvoicesTab();
}
private void tsItemPriceList_Click(object sender, EventArgs e)
diff --git a/src/ElectronicPharmacy/Forms/FCheckDatabaseIntegrary.cs b/src/ElectronicPharmacy/Forms/FCheckDatabaseIntegrary.cs
index ac68457..1b867fe 100644
--- a/src/ElectronicPharmacy/Forms/FCheckDatabaseIntegrary.cs
+++ b/src/ElectronicPharmacy/Forms/FCheckDatabaseIntegrary.cs
@@ -393,6 +393,13 @@ create table if not exists [OrderItems] (
cmdCreateNotExistingsTables.ExecuteNonQuery();
TryAddColumn(conForCheckTables, "Orders", "ConsigneeName", "nvarchar(128)");
+ TryAddColumn(conForCheckTables, "Invoice", "SupplierName", "nvarchar(256)");
+ TryAddColumn(conForCheckTables, "Invoice", "ConsigneesName", "nvarchar(128)");
+ TryAddColumn(conForCheckTables, "Invoice", "InvoiceSum", "nvarchar(64)");
+ TryAddColumn(conForCheckTables, "Invoice", "RefuseSum", "nvarchar(64)");
+ TryAddColumn(conForCheckTables, "Invoice", "SourceOrderId", "integer");
+ TryAddColumn(conForCheckTables, "Invoice", "SourceOrderNumber", "nvarchar(36)");
+ TryAddColumn(conForCheckTables, "Invoice", "IsRequestedInvoice", "int(1)");
}
catch (Exception ex)
{
diff --git a/src/ElectronicPharmacy/HelpForms/HF_DownloadDataFromServer.cs b/src/ElectronicPharmacy/HelpForms/HF_DownloadDataFromServer.cs
index 062bf0b..44af562 100644
--- a/src/ElectronicPharmacy/HelpForms/HF_DownloadDataFromServer.cs
+++ b/src/ElectronicPharmacy/HelpForms/HF_DownloadDataFromServer.cs
@@ -1,16 +1,8 @@
using System;
using System.Collections.Generic;
-using System.ComponentModel;
-using System.ComponentModel.DataAnnotations;
using System.Data;
using System.Data.SQLite;
-using System.Drawing;
-using System.Linq;
using System.Net.Http;
-using System.Text;
-//using System.Net.Http.json;
-using System.Text.Json;
-using System.Text.Json.Serialization;
using System.Threading.Tasks;
using System.Windows.Forms;
using Электронная_Фармация.Classes;
@@ -25,198 +17,165 @@ namespace Электронная_Фармация.HelpForms
InitializeComponent();
}
- string login = Settings.Default.stringLogin.ToString();
- string password = Settings.Default.stringPassword.ToString();
-
- string connectionStringToLocalDB = AppConfig.SqliteConnectionString;
+ private readonly string _login = Settings.Default.stringLogin ?? string.Empty;
+ private readonly string _password = Settings.Default.stringPassword ?? string.Empty;
+ private readonly string _connectionStringToLocalDb = AppConfig.SqliteConnectionString;
private async void HF_DownloadDataFromServer_Load(object sender, EventArgs e)
{
UiThemeHelper.ApplyToControlTree(this);
btnCloseThis.Enabled = false;
- rtxtDebug.Visible = false;
dataGridView1.Visible = false;
+ rtxtDebug.Visible = true;
+ rtxtDebug.ReadOnly = true;
var client = new ApiClient();
+ AppendStatus($"Сервер: {client.BaseUrl}");
+ AppendStatus($"Лог: {AppDebugLog.CurrentLogFilePath}");
+ AppDebugLog.Info("Download", $"Старт загрузки прайса. Сервер={client.BaseUrl}");
+
try
{
- if (!string.IsNullOrWhiteSpace(Settings.Default.stringToken))
- {
- client.SetToken(Settings.Default.stringToken);
- rtxtDebug.Text += "Используется сохранённый токен...\n";
- }
- else
- {
- rtxtDebug.Text += "Выполняю авторизацию...\n";
- var token = await client.LoginAsync(login, password);
- Settings.Default.stringToken = token;
- Settings.Default.Save();
- rtxtDebug.Text += "Авторизация успешно пройдена\n";
- }
+ await EnsureAuthenticatedAsync(client);
- rtxtDebug.Text += "\nЗагружаю сводный прайс для всех поставщиков...";
- var allSuppliersSummary = await client.GetPriceSummaryAsync(
- supplierId: null,
- regionId: string.IsNullOrWhiteSpace(Settings.Default.RegionId) ? null : Settings.Default.RegionId);
- rtxtDebug.Text += ($"✓ Получено {allSuppliersSummary.Summary.Count} позиций\n");
+ AppendStatus("Загружаю сводный прайс для всех поставщиков...");
+ var allSuppliersSummary = await GetPriceSummaryWithRetryAsync(client);
+ AppendStatus($"Получено {allSuppliersSummary.Summary?.Count ?? 0} позиций");
- DataTable tablePrice = new DataTable();
+ var tablePrice = BuildPriceTable(allSuppliersSummary);
+ SavePriceTable(tablePrice);
- tablePrice.Columns.Add("supplier_price_id");
- tablePrice.Columns.Add("guid_es");
- tablePrice.Columns.Add("es_code");
- tablePrice.Columns.Add("supplier_id");
- tablePrice.Columns.Add("DrugName");
- tablePrice.Columns.Add("SupplierName");
- tablePrice.Columns.Add("Price");
- tablePrice.Columns.Add("Quantity");
+ DialogResult = DialogResult.OK;
+ AppendStatus($"Готово. Сохранено {tablePrice.Rows.Count} позиций в локальную базу.");
+ ToastNotification.ShowSuccess($"Прайс обновлён: {tablePrice.Rows.Count} позиций");
+ }
+ catch (UnauthorizedAccessException ex)
+ {
+ AppendStatus($"Ошибка авторизации: {ex.Message}");
+ AppDebugLog.Error("Download", "Ошибка авторизации при загрузке прайса", ex);
+ ToastNotification.ShowError($"Ошибка авторизации: {ex.Message}");
+ }
+ catch (HttpRequestException ex)
+ {
+ AppendStatus($"Ошибка сети: {ex.Message}");
+ AppDebugLog.Error("Download", "Ошибка сети при загрузке прайса", ex);
+ ToastNotification.ShowError($"Ошибка сети: {ex.Message}");
+ }
+ catch (Exception ex)
+ {
+ AppendStatus($"Ошибка загрузки прайса: {ex.Message}");
+ AppDebugLog.Error("Download", "Неожиданная ошибка при загрузке прайса", ex);
+ ToastNotification.ShowError($"Ошибка загрузки прайса: {ex.Message}");
+ }
- tablePrice.Columns.Add(new DataColumn
- {
- ColumnName = "ExpiryPeriod",
- DataType = typeof(string),
- AllowDBNull = true
- });
+ btnCloseThis.Enabled = true;
+ }
- tablePrice.Columns.Add(new DataColumn
- {
- ColumnName = "Description",
- DataType = typeof(string),
- AllowDBNull = true
- });
+ private async Task EnsureAuthenticatedAsync(ApiClient client)
+ {
+ if (!string.IsNullOrWhiteSpace(Settings.Default.stringToken))
+ {
+ client.SetToken(Settings.Default.stringToken);
+ AppendStatus("Используется сохранённый токен...");
+ return;
+ }
- //DataColumn colExpiryPeriod = new DataColumn("ExpiryPeriod", typeof(DateTime));
- //DataColumn colDescription = new DataColumn("Description", typeof(string));
+ await LoginAndSaveTokenAsync(client);
+ }
- //colExpiryPeriod.AllowDBNull = true;
- //colDescription.AllowDBNull = true;
+ private async Task LoginAndSaveTokenAsync(ApiClient client)
+ {
+ if (string.IsNullOrWhiteSpace(_login) || string.IsNullOrWhiteSpace(_password))
+ {
+ throw new UnauthorizedAccessException(
+ "Не заданы логин и пароль. Выполните авторизацию в меню «Регистрация».");
+ }
+ AppendStatus("Выполняю авторизацию...");
+ var token = await client.LoginAsync(_login, _password);
+ Settings.Default.stringToken = token;
+ Settings.Default.Save();
+ AppendStatus("Авторизация успешно пройдена");
+ }
- ////tablePrice.Columns.Add(colExpiryPeriod);
- //tablePrice.Columns.Add(colDescription);
+ private async Task GetPriceSummaryWithRetryAsync(ApiClient client)
+ {
+ var regionId = string.IsNullOrWhiteSpace(Settings.Default.RegionId)
+ ? null
+ : Settings.Default.RegionId;
+ try
+ {
+ return await client.GetPriceSummaryAsync(supplierId: null, regionId: regionId);
+ }
+ catch (UnauthorizedAccessException)
+ {
+ AppendStatus("Токен недействителен, повторная авторизация...");
+ Settings.Default.stringToken = string.Empty;
+ Settings.Default.Save();
+ client.SetToken(null);
+ await LoginAndSaveTokenAsync(client);
+ return await client.GetPriceSummaryAsync(supplierId: null, regionId: regionId);
+ }
+ }
- tablePrice.Columns.Add("Zakaz");
- tablePrice.Columns.Add("SummaZakaza");
+ private static DataTable BuildPriceTable(PriceSummaryResponse allSuppliersSummary)
+ {
+ var tablePrice = new DataTable();
+ tablePrice.Columns.Add("supplier_price_id");
+ tablePrice.Columns.Add("guid_es");
+ tablePrice.Columns.Add("es_code");
+ tablePrice.Columns.Add("supplier_id");
+ tablePrice.Columns.Add("DrugName");
+ tablePrice.Columns.Add("SupplierName");
+ tablePrice.Columns.Add("Price");
+ tablePrice.Columns.Add("Quantity");
+ tablePrice.Columns.Add(new DataColumn
+ {
+ ColumnName = "ExpiryPeriod",
+ DataType = typeof(string),
+ AllowDBNull = true
+ });
+ tablePrice.Columns.Add(new DataColumn
+ {
+ ColumnName = "Description",
+ DataType = typeof(string),
+ AllowDBNull = true
+ });
+ tablePrice.Columns.Add("Zakaz");
+ tablePrice.Columns.Add("SummaZakaza");
+ if (allSuppliersSummary?.Summary == null)
+ {
+ return tablePrice;
+ }
- #region полная таблица
- //tablePrice.Columns.Add("GuidEs");
- //tablePrice.Columns.Add("SupplierId");
- //tablePrice.Columns.Add("SupplierName");
- //tablePrice.Columns.Add("DrugName");
- //tablePrice.Columns.Add("Inn");
- //tablePrice.Columns.Add("CureForm");
- //tablePrice.Columns.Add("Barcode");
- //tablePrice.Columns.Add("Price");
- //tablePrice.Columns.Add("Quantity");
- //tablePrice.Columns.Add("RegionId");
- //tablePrice.Columns.Add("RegionName");
- //tablePrice.Columns.Add("LastPriceDate");
- //tablePrice.Columns.Add("MatchMethod");
- //tablePrice.Columns.Add("MatchConfidence");
- //tablePrice.Columns.Add("TradeName");
- //tablePrice.Columns.Add("Dosage");
- //tablePrice.Columns.Add("RegistryPrice");
- //tablePrice.Columns.Add("InstructionGuid");
- //tablePrice.Columns.Add("Description");
- //tablePrice.Columns.Add("StoringCondition");
- //tablePrice.Columns.Add("ExpiryPeriod");
- //tablePrice.Columns.Add("ProducerName");
- //tablePrice.Columns.Add("RegistryDate");
- //tablePrice.Columns.Add("EsCode");
- //tablePrice.Columns.Add("RegistryStatus");
- #endregion
+ foreach (var item in allSuppliersSummary.Summary)
+ {
+ var row = tablePrice.NewRow();
+ row["supplier_price_id"] = item.SupplierPriceID.ToString();
+ row["guid_es"] = item.GuidEs.ToString();
+ row["es_code"] = item.EsCode.ToString();
+ row["supplier_id"] = item.SupplierId.ToString();
+ row["DrugName"] = item.DrugName.ToString();
+ row["SupplierName"] = item.SupplierName.ToString();
+ row["Price"] = item.Price.ToString();
+ row["Quantity"] = item.Quantity.ToString();
+ row["ExpiryPeriod"] = item.ExpiryPeriod == null ? (object)DBNull.Value : item.ExpiryPeriod.ToString();
+ row["Description"] = item.Description == null ? (object)DBNull.Value : item.Description.ToString();
+ row["Zakaz"] = string.Empty;
+ row["SummaZakaza"] = string.Empty;
+ tablePrice.Rows.Add(row);
+ }
- if (allSuppliersSummary.Summary != null)
- {
+ return tablePrice;
+ }
- foreach (var item in allSuppliersSummary.Summary)
- {
-
- DataRow row = tablePrice.NewRow();
-
- row["supplier_price_id"] = item.SupplierPriceID.ToString();
- row["guid_es"] = item.GuidEs.ToString();
- row["es_code"] = item.EsCode.ToString();
- row["supplier_id"] = item.SupplierId.ToString();
- row["DrugName"] = item.DrugName.ToString();
- row["SupplierName"] = item.SupplierName.ToString();
- row["Price"] = item.Price.ToString();
- row["Quantity"] = item.Quantity.ToString();
-
- if (item.ExpiryPeriod == null)
- { row["ExpiryPeriod"] = DBNull.Value; }
- else { row["ExpiryPeriod"] = item.ExpiryPeriod.ToString(); }
- ;
-
- if (item.Description == null)
- { row["Description"] = DBNull.Value; }
- else { row["Description"] = item.Description.ToString(); }
- ;
-
- row["Zakaz"] = string.Empty;
- row["SummaZakaza"] = string.Empty;
-
- tablePrice.Rows.Add(row);
-
- // DataSetPriceList.Tables.Add(tablePrice);
- // DataSetPriceList.WriteXml("PriceList.xml", XmlWriteMode.WriteSchema);
-
-
-
- //SQLiteDataAdapter sqlite_data = new SQLiteDataAdapter();
- //new SQLiteCommandBuilder(sqlite_data);
- ////sqlite_data.Fill(tablePrice);
- //sqlite_data.Update(tablePrice);
- //dataGridView1.DataSource = tablePrice;
-
-
-
- #region сводный прайс и статистика
- //// Шаг 3: Получение сводного прайса для конкретного поставщика
- //if (allSuppliersSummary.Summary.Any())
- //{
- // var firstSupplierId = allSuppliersSummary.Summary
- // .FirstOrDefault(s => !string.IsNullOrEmpty(s.SupplierId))?.SupplierId;
-
- // if (!string.IsNullOrEmpty(firstSupplierId))
- // {
- // Console.WriteLine($"\nЗагружаю прайс для поставщика {firstSupplierId}...");
- // var supplierSummary = await client.GetPriceSummaryAsync(supplierId: firstSupplierId);
- // Console.WriteLine($"✓ Получено {supplierSummary.Summary.Count} позиций для поставщика\n");
- // }
- //}
-
- //// Шаг 4: Статистика
- //Console.WriteLine("\n=== Статистика ===");
- //var withPrice = allSuppliersSummary.Summary.Where(s => s.Price.HasValue).ToList();
- //var withQuantity = allSuppliersSummary.Summary.Where(s => s.Quantity.HasValue).ToList();
-
- //Console.WriteLine($"Всего позиций: {allSuppliersSummary.Summary.Count}");
- //Console.WriteLine($"С ценой: {withPrice.Count}");
- //Console.WriteLine($"С количеством: {withQuantity.Count}");
-
-
- //if (withPrice.Any())
- //{
- // var avgPrice = withPrice.Average(s => s.Price.Value);
- // var minPrice = withPrice.Min(s => s.Price.Value);
- // var maxPrice = withPrice.Max(s => s.Price.Value);
-
- // Console.WriteLine($"Средняя цена: {avgPrice:F2} ₽");
- // Console.WriteLine($"Минимальная цена: {minPrice:F2} ₽");
- // Console.WriteLine($"Максимальная цена: {maxPrice:F2} ₽");
- //}
- #endregion
- }
- }
-
- string commandToDelete = "delete from [PriceList]";
- string commandTODeleteTempOrder = "delete from [TempOrderItems]";
-
-
- string commandToInsert = @"INSERT INTO [PriceList] (
+ private void SavePriceTable(DataTable tablePrice)
+ {
+ const string commandToDelete = "delete from [PriceList]";
+ const string commandToDeleteTempOrder = "delete from [TempOrderItems]";
+ const string commandToInsert = @"INSERT INTO [PriceList] (
[guid_es],
[es_code],
[supplier_id],
@@ -244,73 +203,70 @@ VALUES
@Zakaz,
@SummaZakaza,
@supplier_price_id
-)
-";
+)";
- using (SQLiteConnection sqliteCon = new SQLiteConnection(connectionStringToLocalDB))
+ using (var sqliteCon = new SQLiteConnection(_connectionStringToLocalDb))
+ {
+ sqliteCon.Open();
+
+ using (var cmdDeleteBeforeInsert = new SQLiteCommand(commandToDelete, sqliteCon))
+ using (var cmdDeleteTempOrder = new SQLiteCommand(commandToDeleteTempOrder, sqliteCon))
+ {
+ cmdDeleteBeforeInsert.ExecuteNonQuery();
+ cmdDeleteTempOrder.ExecuteNonQuery();
+ }
+
+ using (var transaction = sqliteCon.BeginTransaction())
+ {
+ foreach (DataRow row in tablePrice.Rows)
+ {
+ using (var cmd = new SQLiteCommand(commandToInsert, sqliteCon, transaction))
{
- sqliteCon.Open();
-
- SQLiteCommand cmdDeleteBeforeInsert = new SQLiteCommand(commandToDelete, sqliteCon);
- SQLiteCommand cmdDeleteTempOrder = new SQLiteCommand(commandTODeleteTempOrder, sqliteCon);
-
- cmdDeleteBeforeInsert.ExecuteNonQuery();
- cmdDeleteTempOrder.ExecuteNonQuery();
-
- using (var transaction = sqliteCon.BeginTransaction())
- {
- foreach (DataRow row in tablePrice.Rows)
- {
- using (SQLiteCommand cmd = new SQLiteCommand(commandToInsert, sqliteCon, transaction))
- {
- cmd.Parameters.AddWithValue("@guid_es", row["guid_es"]);
- cmd.Parameters.AddWithValue("@es_code", row["es_code"]);
- cmd.Parameters.AddWithValue("@supplier_id", row["supplier_id"]);
- cmd.Parameters.AddWithValue("@DrugName", row["DrugName"]);
- cmd.Parameters.AddWithValue("@SupplierName", row["SupplierName"]);
- cmd.Parameters.AddWithValue("@Price", row["Price"]);
- cmd.Parameters.AddWithValue("@Quantity", row["Quantity"]);
- cmd.Parameters.AddWithValue("@ExpiryPeriod", row["ExpiryPeriod"]);
- cmd.Parameters.AddWithValue("@Description", row["Description"]);
- cmd.Parameters.AddWithValue("@Zakaz", row["Zakaz"]);
- cmd.Parameters.AddWithValue("@SummaZakaza", row["SummaZakaza"]);
- cmd.Parameters.AddWithValue("@supplier_price_id", row["supplier_price_id"]);
- cmd.ExecuteNonQuery();
- }
- }
- transaction.Commit();
- }
- sqliteCon.Close();
+ cmd.Parameters.AddWithValue("@guid_es", row["guid_es"]);
+ cmd.Parameters.AddWithValue("@es_code", row["es_code"]);
+ cmd.Parameters.AddWithValue("@supplier_id", row["supplier_id"]);
+ cmd.Parameters.AddWithValue("@DrugName", row["DrugName"]);
+ cmd.Parameters.AddWithValue("@SupplierName", row["SupplierName"]);
+ cmd.Parameters.AddWithValue("@Price", row["Price"]);
+ cmd.Parameters.AddWithValue("@Quantity", row["Quantity"]);
+ cmd.Parameters.AddWithValue("@ExpiryPeriod", row["ExpiryPeriod"]);
+ cmd.Parameters.AddWithValue("@Description", row["Description"]);
+ cmd.Parameters.AddWithValue("@Zakaz", row["Zakaz"]);
+ cmd.Parameters.AddWithValue("@SummaZakaza", row["SummaZakaza"]);
+ cmd.Parameters.AddWithValue("@supplier_price_id", row["supplier_price_id"]);
+ cmd.ExecuteNonQuery();
}
+ }
- DialogResult = DialogResult.OK;
- ToastNotification.ShowSuccess($"Прайс обновлён: {tablePrice.Rows.Count} позиций");
+ transaction.Commit();
+ }
}
- catch (UnauthorizedAccessException ex)
+ }
+
+ private void AppendStatus(string message)
+ {
+ if (InvokeRequired)
{
- ToastNotification.ShowError($"Ошибка авторизации: {ex.Message}");
- }
- catch (HttpRequestException ex)
- {
- ToastNotification.ShowError($"Ошибка сети: {ex.Message}");
- }
- catch (Exception ex)
- {
- ToastNotification.ShowError($"Ошибка загрузки прайса: {ex.Message}");
+ BeginInvoke(new Action(AppendStatus), message);
+ return;
}
- btnCloseThis.Enabled = true;
+ rtxtDebug.AppendText(message + Environment.NewLine);
+ rtxtDebug.SelectionStart = rtxtDebug.TextLength;
+ rtxtDebug.ScrollToCaret();
+ rtxtDebug.Refresh();
+ AppDebugLog.Info("Download", message);
}
private void HF_DownloadDataFromServer_FormClosing(object sender, FormClosingEventArgs e)
{
- this.Dispose();
- this.Close();
+ Dispose();
+ Close();
}
private void btnCloseThis_Click(object sender, EventArgs e)
{
- this.Close();
+ Close();
}
}
}
diff --git a/src/ElectronicPharmacy/HelpForms/HF_Registration.cs b/src/ElectronicPharmacy/HelpForms/HF_Registration.cs
index bbd23c1..924ba2f 100644
--- a/src/ElectronicPharmacy/HelpForms/HF_Registration.cs
+++ b/src/ElectronicPharmacy/HelpForms/HF_Registration.cs
@@ -56,7 +56,8 @@ namespace Электронная_Фармация.HelpForms
loginRequest.Username = strLogin.Trim();
loginRequest.Password = strPassword.Trim();
- var client = new ApiClient(apiUrl);
+ var client = new ApiClient(AppConfig.NormalizeApiBaseUrl(apiUrl));
+ AppDebugLog.Info("Auth", $"Попытка авторизации. Сервер={client.BaseUrl}, login={loginRequest.Username}");
try
{
@@ -66,9 +67,10 @@ namespace Электронная_Фармация.HelpForms
Settings.Default.stringPassword = strPassword;
Settings.Default.stringToken = token;
Settings.Default.LocationId = txtLocationId.Text.Trim();
- Settings.Default.ApiBaseUrl = txtApiUrl.Text.Trim();
+ Settings.Default.ApiBaseUrl = client.BaseUrl;
Settings.Default.Save();
+ AppDebugLog.Info("Auth", $"Авторизация успешна. token={AppDebugLog.MaskToken(token)}");
ToastNotification.ShowSuccess("Авторизация успешно выполнена");
//MessageBox.Show("Данные были сохранены", "Регистрация программы успешно выполнена", MessageBoxButtons.OK, MessageBoxIcon.Information);
@@ -79,13 +81,13 @@ namespace Электронная_Фармация.HelpForms
}
catch (UnauthorizedAccessException ex)
{
+ AppDebugLog.Error("Auth", "Ошибка авторизации", ex);
ToastNotification.ShowError($"Ошибка авторизации: {ex.Message}");
- //MessageBox.Show($"Ошибка авторизации: {ex.Message}");
}
catch (HttpRequestException ex)
{
+ AppDebugLog.Error("Auth", "Ошибка сети при авторизации", ex);
ToastNotification.ShowError($"Ошибка сети: {ex.Message}");
- //MessageBox.Show($"Ошибка сети: {ex.Message}");
}
}
else
diff --git a/src/ElectronicPharmacy/Program.cs b/src/ElectronicPharmacy/Program.cs
index 4dcc9e3..a6db27c 100644
--- a/src/ElectronicPharmacy/Program.cs
+++ b/src/ElectronicPharmacy/Program.cs
@@ -1,5 +1,7 @@
using System;
using System.Windows.Forms;
+using System.Net;
+using Электронная_Фармация.Classes;
namespace Электронная_Фармация
{
@@ -8,6 +10,9 @@ namespace Электронная_Фармация
[STAThread]
static void Main()
{
+ ServicePointManager.SecurityProtocol |= SecurityProtocolType.Tls12;
+ AppConfig.MigrateLegacySettings();
+ AppDebugLog.Info("App", $"Запуск. ApiBaseUrl={AppConfig.ApiBaseUrl}, Log={AppDebugLog.CurrentLogFilePath}");
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
Application.Run(new ElectroPharmacy());
diff --git a/src/ElectronicPharmacy/Properties/Settings.Designer.cs b/src/ElectronicPharmacy/Properties/Settings.Designer.cs
index e0ea4f7..c582df8 100644
--- a/src/ElectronicPharmacy/Properties/Settings.Designer.cs
+++ b/src/ElectronicPharmacy/Properties/Settings.Designer.cs
@@ -85,7 +85,7 @@ namespace Электронная_Фармация.Properties {
[global::System.Configuration.UserScopedSettingAttribute()]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
- [global::System.Configuration.DefaultSettingValueAttribute("http://195.34.241.84:9988")]
+ [global::System.Configuration.DefaultSettingValueAttribute("https://24pharmdata.ru")]
public string ApiBaseUrl {
get {
return ((string)(this["ApiBaseUrl"]));
diff --git a/src/ElectronicPharmacy/Properties/Settings.settings b/src/ElectronicPharmacy/Properties/Settings.settings
index d747674..b2c624d 100644
--- a/src/ElectronicPharmacy/Properties/Settings.settings
+++ b/src/ElectronicPharmacy/Properties/Settings.settings
@@ -18,7 +18,7 @@
- http://195.34.241.84:9988
+ https://24pharmdata.ru
diff --git a/src/ElectronicPharmacy/UserControls/FOrders.cs b/src/ElectronicPharmacy/UserControls/FOrders.cs
index 2ffea97..de59de3 100644
--- a/src/ElectronicPharmacy/UserControls/FOrders.cs
+++ b/src/ElectronicPharmacy/UserControls/FOrders.cs
@@ -46,13 +46,54 @@ namespace Электронная_Фармация.UserControls
ApplyToolbarLayout();
ModernButtonStyles.ApplyClear(btnClearGoodSearch);
ModernButtonStyles.ApplyClear(btnClearOrderNumberFilter);
+ InitializeOrderContextMenu();
label3.BringToFront();
panelToolbar.Resize += (_, __) => ApplyToolbarLayout();
}
+ private void InitializeOrderContextMenu()
+ {
+ if (_ordersMenu != null)
+ {
+ return;
+ }
+
+ _getInvoiceMenuItem = new ToolStripMenuItem("Получить накладную");
+ _getInvoiceMenuItem.Click += (_, __) => RequestInvoiceForSelectedOrder();
+
+ _ordersMenu = new ContextMenuStrip();
+ _ordersMenu.Items.Add(_getInvoiceMenuItem);
+ _ordersMenu.Opening += OrdersMenu_Opening;
+
+ DGVOrders.ContextMenuStrip = _ordersMenu;
+ }
+
+ private void OrdersMenu_Opening(object sender, CancelEventArgs e)
+ {
+ var pt = DGVOrders.PointToClient(Cursor.Position);
+ var hit = DGVOrders.HitTest(pt.X, pt.Y);
+ if (hit.RowIndex < 0 || hit.RowIndex >= DGVOrders.Rows.Count)
+ {
+ e.Cancel = true;
+ return;
+ }
+
+ DGVOrders.ClearSelection();
+ DGVOrders.Rows[hit.RowIndex].Selected = true;
+ if (hit.ColumnIndex >= 0)
+ {
+ DGVOrders.CurrentCell = DGVOrders.Rows[hit.RowIndex].Cells[hit.ColumnIndex];
+ }
+
+ var status = DGVOrders.Rows[hit.RowIndex].Cells["Статус"]?.Value?.ToString();
+ _getInvoiceMenuItem.Enabled = string.Equals(status, "ОТПРАВЛЕН", StringComparison.OrdinalIgnoreCase);
+ }
+
private ModernComboBox _comboOrderStatus;
private ModernComboBox _comboConsignees;
private ModernButton _btnSendOrder;
+ private ContextMenuStrip _ordersMenu;
+ private ToolStripMenuItem _getInvoiceMenuItem;
private void InitOrderToolbar()
{
@@ -319,6 +360,44 @@ namespace Электронная_Фармация.UserControls
ApplyOrderFilters();
}
+ private void RequestInvoiceForSelectedOrder()
+ {
+ if (DGVOrders.SelectedRows.Count == 0)
+ {
+ ToastNotification.ShowCustom("Выберите отправленный заказ.", Color.DarkOrange, Color.White);
+ return;
+ }
+
+ var selectedRow = DGVOrders.SelectedRows[0];
+ var status = selectedRow.Cells["Статус"]?.Value?.ToString();
+ if (!string.Equals(status, "ОТПРАВЛЕН", StringComparison.OrdinalIgnoreCase))
+ {
+ ToastNotification.ShowCustom("Накладную можно получить только для отправленного заказа.", Color.DarkOrange, Color.White);
+ return;
+ }
+
+ var orderId = selectedRow.Cells["ИД заказа"]?.Value?.ToString();
+ if (string.IsNullOrWhiteSpace(orderId))
+ {
+ ToastNotification.ShowError("Не удалось определить идентификатор заказа.");
+ return;
+ }
+
+ try
+ {
+ var service = new InvoiceRequestService();
+ service.CreateOrUpdateFromOrder(orderId);
+ ToastNotification.ShowSuccess("Накладная добавлена в раздел \"Накладные\".");
+
+ var shell = FindForm() as ElectroPharmacy;
+ shell?.OpenInvoicesTab(orderId);
+ }
+ catch (Exception ex)
+ {
+ ToastNotification.ShowError($"Не удалось получить накладную: {ex.Message}");
+ }
+ }
+
void LoadOrders(string dateFrom, string dateTo, string filters)
{
string connectionStringToLocalDB = AppConfig.SqliteConnectionString;
diff --git a/src/ElectronicPharmacy/UserControls/UCInvoices.cs b/src/ElectronicPharmacy/UserControls/UCInvoices.cs
index 9a2b20c..8d2a801 100644
--- a/src/ElectronicPharmacy/UserControls/UCInvoices.cs
+++ b/src/ElectronicPharmacy/UserControls/UCInvoices.cs
@@ -15,9 +15,28 @@ namespace Электронная_Фармация.UserControls
{
public partial class UCInvoices : UserControl
{
+ private string _initialSourceOrderId;
+ private ContextMenuStrip _invoiceMenu;
+ private ToolStripMenuItem _exportInvoiceMenuItem;
+
public UCInvoices()
+ : this(null)
+ {
+ }
+
+ public UCInvoices(string sourceOrderId)
{
InitializeComponent();
+ _initialSourceOrderId = sourceOrderId;
+ }
+
+ public void SetSourceOrderFilter(string sourceOrderId)
+ {
+ _initialSourceOrderId = sourceOrderId;
+ if (IsHandleCreated)
+ {
+ getDatesFromCalendar();
+ }
}
void getDatesFromCalendar()
@@ -94,52 +113,80 @@ namespace Электронная_Фармация.UserControls
void loadInvoicesList(string InvoicesDateFrom, string InvoicesDateTo, string dateFromYearFormat)
{
-
string connectionStringToLocalDB = AppConfig.SqliteConnectionString;
- string filterAddons = $"where i.InvoiceDate between date('{InvoicesDateFrom}') and date('{InvoicesDateTo}')";
-
- if (txtInvoiceNumber.Text != "" & txtInvoiceNumber.Text != "Номер накладной")
- {
- filterAddons += $" and i.InvoiceNumber like '%{txtInvoiceNumber.Text}%'";
- }
-
- if (comboSuppliers.Text != "" & comboSuppliers.Text != "Поставщик")
- {
- filterAddons += $" and s.SuppliersName like '%{comboSuppliers.Text}%'";
- }
-
- if (comboConsignees.Text != "" & comboConsignees.Text != "Грузополучатель")
- {
- filterAddons += $" and c.ConsigneesName like '%{comboConsignees.Text}%'";
- }
-
- string qShowMeListOfInvoices = $@"
-select i.InvoiceNumber as [Номер],
-i.invoiceDate as [Дата],
-s.SuppliersName as [Поставщик],
-c.ConsigneesName as [Грузополучатель],
-c.ConsigneesAddress as [Адрес ГП],
-i.SumWithoutNDS as [Сумма без НДС],
-i.SumWithNDS as [Сумма с НДС],
-i.SumNDS as [Сумма НДС]
-from [Invoice] i
-inner join [Suppliers] s on s.codeSuppliers = i.CodeSupplier
-inner join [Consignees] c on c.codeConsignees = i.CodeConsignees
-{filterAddons}
-order by i.InvoiceDate asc
-";
-
using (SQLiteConnection conInvoices = new SQLiteConnection(connectionStringToLocalDB))
{
try
{
conInvoices.Open();
- SQLiteCommand cmdShowInvoicesList = new SQLiteCommand(qShowMeListOfInvoices, conInvoices);
+ var sql = @"
+select
+ i.[idInvoice] as [ИД накладной],
+ i.[InvoiceNumber] as [Номер],
+ i.[InvoiceDate] as [Дата],
+ coalesce(nullif(i.[SupplierName], ''), (select s.[SuppliersName] from [Suppliers] s where s.[codeSuppliers] = i.[CodeSupplier])) as [Поставщик],
+ coalesce(nullif(i.[ConsigneesName], ''), (select c.[ConsigneesName] from [Consignees] c where c.[codeConsignees] = i.[CodeConsignees])) as [Грузополучатель],
+ i.[SourceOrderNumber] as [Номер заказа],
+ coalesce(i.[InvoiceSum], i.[SumWithNDS], i.[SumWithoutNDS], 0) as [Сумма],
+ coalesce(i.[RefuseSum], 0) as [Сумма отказа]
+from [Invoice] i
+where date(i.[InvoiceDate]) between date(@dateFrom) and date(@dateTo)";
+
+ if (txtInvoiceNumber.Text != "" && txtInvoiceNumber.Text != "Номер накладной")
+ {
+ sql += " and i.[InvoiceNumber] like @invoiceNumber";
+ }
+
+ if (comboSuppliers.Text != "" && comboSuppliers.Text != "Поставщик")
+ {
+ sql += " and coalesce(nullif(i.[SupplierName], ''), (select s.[SuppliersName] from [Suppliers] s where s.[codeSuppliers] = i.[CodeSupplier])) like @supplierName";
+ }
+
+ if (comboConsignees.Text != "" && comboConsignees.Text != "Грузополучатель")
+ {
+ sql += " and coalesce(nullif(i.[ConsigneesName], ''), (select c.[ConsigneesName] from [Consignees] c where c.[codeConsignees] = i.[CodeConsignees])) like @consigneeName";
+ }
+
+ if (!string.IsNullOrWhiteSpace(_initialSourceOrderId))
+ {
+ sql += " and cast(i.[SourceOrderId] as text) = @sourceOrderId";
+ }
+
+ sql += " order by date(i.[InvoiceDate]) desc, i.[idInvoice] desc;";
+
+ SQLiteCommand cmdShowInvoicesList = new SQLiteCommand(sql, conInvoices);
+ cmdShowInvoicesList.Parameters.AddWithValue("@dateFrom", InvoicesDateFrom);
+ cmdShowInvoicesList.Parameters.AddWithValue("@dateTo", InvoicesDateTo);
+ if (txtInvoiceNumber.Text != "" && txtInvoiceNumber.Text != "Номер накладной")
+ {
+ cmdShowInvoicesList.Parameters.AddWithValue("@invoiceNumber", $"%{txtInvoiceNumber.Text}%");
+ }
+
+ if (comboSuppliers.Text != "" && comboSuppliers.Text != "Поставщик")
+ {
+ cmdShowInvoicesList.Parameters.AddWithValue("@supplierName", $"%{comboSuppliers.Text}%");
+ }
+
+ if (comboConsignees.Text != "" && comboConsignees.Text != "Грузополучатель")
+ {
+ cmdShowInvoicesList.Parameters.AddWithValue("@consigneeName", $"%{comboConsignees.Text}%");
+ }
+
+ if (!string.IsNullOrWhiteSpace(_initialSourceOrderId))
+ {
+ cmdShowInvoicesList.Parameters.AddWithValue("@sourceOrderId", _initialSourceOrderId);
+ }
+
DataTable tableInvoices = new DataTable();
SQLiteDataAdapter daInvoicesList = new SQLiteDataAdapter(cmdShowInvoicesList);
daInvoicesList.Fill(tableInvoices);
dgvInvoice.DataSource = tableInvoices;
+
+ if (dgvInvoice.Columns.Count > 0)
+ {
+ dgvInvoice.Columns["ИД накладной"].Visible = false;
+ }
}
catch (Exception ex)
{
@@ -162,6 +209,7 @@ order by i.InvoiceDate asc
{
UiThemeHelper.ApplyToControlTree(this);
Font = new Font("Segoe UI", 9.75f, FontStyle.Regular);
+ InitializeInvoiceContextMenu();
button1.Visible = true;
button1.Text = "Обновить с сервера";
@@ -196,6 +244,43 @@ order by i.InvoiceDate asc
}
}
+ private void InitializeInvoiceContextMenu()
+ {
+ if (_invoiceMenu != null)
+ {
+ return;
+ }
+
+ _exportInvoiceMenuItem = new ToolStripMenuItem("Экспорт");
+ _exportInvoiceMenuItem.Click += (_, __) => ExportSelectedInvoice();
+
+ _invoiceMenu = new ContextMenuStrip();
+ _invoiceMenu.Items.Add(_exportInvoiceMenuItem);
+ _invoiceMenu.Opening += InvoiceMenu_Opening;
+
+ dgvInvoice.ContextMenuStrip = _invoiceMenu;
+ }
+
+ private void InvoiceMenu_Opening(object sender, CancelEventArgs e)
+ {
+ var pt = dgvInvoice.PointToClient(Cursor.Position);
+ var hit = dgvInvoice.HitTest(pt.X, pt.Y);
+ if (hit.RowIndex < 0 || hit.RowIndex >= dgvInvoice.Rows.Count)
+ {
+ e.Cancel = true;
+ return;
+ }
+
+ dgvInvoice.ClearSelection();
+ dgvInvoice.Rows[hit.RowIndex].Selected = true;
+ if (hit.ColumnIndex >= 0)
+ {
+ dgvInvoice.CurrentCell = dgvInvoice.Rows[hit.RowIndex].Cells[hit.ColumnIndex];
+ }
+
+ _exportInvoiceMenuItem.Enabled = true;
+ }
+
private void dtpDateTo_Leave(object sender, EventArgs e)
{
@@ -226,28 +311,22 @@ order by i.InvoiceDate asc
void selectInvoice()
{
- if (dgvInvoice.Rows.Count > 0)
+ if (dgvInvoice.Rows.Count > 0 && dgvInvoice.CurrentCell != null)
{
int SelectedInvoiceIndex = dgvInvoice.CurrentCell.RowIndex;
- string SelectedInvoiceNumber = dgvInvoice.Rows[SelectedInvoiceIndex].Cells[0].Value.ToString();
- loadInvoiceItems(SelectedInvoiceNumber);
+ string SelectedInvoiceId = dgvInvoice.Rows[SelectedInvoiceIndex].Cells["ИД накладной"]?.Value?.ToString() ?? string.Empty;
+ loadInvoiceItems(SelectedInvoiceId);
}
}
- void loadInvoiceItems(string InvoiceNumber)
+ void loadInvoiceItems(string invoiceId)
{
- string filterAddons = string.Empty;
-
-
- string InvNum = InvoiceNumber;
if (dgvInvoice.Rows.Count > 0)
{
- if (InvNum != "" | InvoiceNumber != string.Empty)
+ if (!string.IsNullOrWhiteSpace(invoiceId))
{
- filterAddons = $"where i.InvoiceNumber = '{InvNum}'";
-
string connectionStringToLocalDB = AppConfig.SqliteConnectionString;
- string qShowMeInvoiceItems = $@"
+ string qShowMeInvoiceItems = @"
select
ii.GoodCode as [Код товара],
ii.Good as [Товар],
@@ -270,20 +349,22 @@ ii.Sertificate as [Сертификат],
ii.Marked as [Признак маркировки],
ii.GTIN as [GTIN]
from InvoiceItem ii
-inner join Invoice i on i.idInvoice = ii.idInvoice
-{filterAddons}
-";
+where ii.idInvoice = @invoiceId;";
using (SQLiteConnection conInvoiceItems = new SQLiteConnection(connectionStringToLocalDB))
{
try
{
conInvoiceItems.Open();
SQLiteCommand cmdShowMeInvoiceItems = new SQLiteCommand(qShowMeInvoiceItems, conInvoiceItems);
+ cmdShowMeInvoiceItems.Parameters.AddWithValue("@invoiceId", invoiceId);
DataTable tableInvoiceItems = new DataTable();
SQLiteDataAdapter daInvoiceItems = new SQLiteDataAdapter(cmdShowMeInvoiceItems);
daInvoiceItems.Fill(tableInvoiceItems);
dgvInvoiceItems.DataSource = tableInvoiceItems;
- dgvInvoiceItems.Columns[10].ValueType = typeof(bool);
+ if (dgvInvoiceItems.Columns.Count > 10)
+ {
+ dgvInvoiceItems.Columns[10].ValueType = typeof(bool);
+ }
}
catch (Exception ex)
{
@@ -303,6 +384,21 @@ inner join Invoice i on i.idInvoice = ii.idInvoice
}
}
+ private void ExportSelectedInvoice()
+ {
+ if (dgvInvoice.SelectedRows.Count == 0)
+ {
+ ToastNotification.ShowCustom("Выберите накладную для экспорта.", Color.DarkOrange, Color.White);
+ return;
+ }
+
+ var invoiceNumber = dgvInvoice.SelectedRows[0].Cells["Номер"]?.Value?.ToString() ?? string.Empty;
+ ToastNotification.ShowCustom(
+ $"Экспорт накладной {invoiceNumber} будет добавлен после согласования формата.",
+ Color.SteelBlue,
+ Color.White);
+ }
+
private void txtInvoiceNumber_KeyPress(object sender, KeyPressEventArgs e)
{
if (e.KeyChar == (char)Keys.Enter)
diff --git a/src/ElectronicPharmacy/UserControls/UCPriceList.cs b/src/ElectronicPharmacy/UserControls/UCPriceList.cs
index 4a7b5b4..a34dafb 100644
--- a/src/ElectronicPharmacy/UserControls/UCPriceList.cs
+++ b/src/ElectronicPharmacy/UserControls/UCPriceList.cs
@@ -824,15 +824,46 @@ and tempOrder.GoodName = PriceList.GoodName
private void dgvPriceList_KeyPress(object sender, KeyPressEventArgs e)
{
- int RowIndex = dgvPriceList.SelectedRows[0].Index;
+ int RowIndex = -1;
+ string idPriceListItem = string.Empty;
- string idPriceListItem = dgvPriceList.Rows[RowIndex].Cells[11].Value.ToString();
+ if (dgvPriceList.SelectedRows.Count > 0)
+ {
+ RowIndex = dgvPriceList.SelectedRows[0].Index;
+ if (RowIndex >= 0 && RowIndex < dgvPriceList.Rows.Count)
+ {
+ idPriceListItem = dgvPriceList.Rows[RowIndex].Cells[11].Value?.ToString() ?? string.Empty;
+ }
+ }
+
+ if (e.KeyChar == (Char)Keys.Back && GoodsFilter.Length > 0)
+ {
+ GoodsFilter = GoodsFilter.Substring(0, GoodsFilter.Length - 1);
+ txtSearchGoodNameInPriceList.Text = GoodsFilter;
+ ApplyPriceListFilters();
+
+ if (GoodsFilter.Length == 0)
+ {
+ timerForResetSearch.Enabled = false;
+ timerForResetSearch.Stop();
+ }
+ else
+ {
+ timerForResetSearch.Enabled = true;
+ timerForResetSearch.Start();
+ }
+
+ return;
+ }
if (timerForResetSearch.Enabled == false)
{
- deleteGood(RowIndex, idPriceListItem);
- timerForResetSearch.Enabled = true;
- timerForResetSearch.Start();
+ if (RowIndex >= 0 && !string.IsNullOrEmpty(idPriceListItem))
+ {
+ deleteGood(RowIndex, idPriceListItem);
+ timerForResetSearch.Enabled = true;
+ timerForResetSearch.Start();
+ }
}
if ((e.KeyChar >= 'A' && e.KeyChar <= 'Z') || (e.KeyChar >= 'a' && e.KeyChar <= 'z') || (e.KeyChar >= 'А' && e.KeyChar <= 'Я') || (e.KeyChar >= 'а' && e.KeyChar <= 'я') || (e.KeyChar == (Char)Keys.Subtract))
@@ -874,6 +905,7 @@ and tempOrder.GoodName = PriceList.GoodName
}
#endregion
}
+ return;
}
else if ((e.KeyChar >= (Char)Keys.D1 && e.KeyChar <= (Char)Keys.D9) ||
(e.KeyChar >= (Char)Keys.NumPad1 && e.KeyChar <= (Char)Keys.NumPad9) || (e.KeyChar == (Char)Keys.D0 && dgvPriceList.Rows[RowIndex].Cells[10].Value.ToString().Length > 1) || (e.KeyChar == (Char)Keys.NumPad0 && dgvPriceList.Rows[RowIndex].Cells[10].Value.ToString().Length > 1))
@@ -923,9 +955,11 @@ and tempOrder.GoodName = PriceList.GoodName
txtSearchGoodNameInPriceList.Text = string.Empty;
GoodsFilter = string.Empty;
ApplyPriceListFilters();
+ return;
}
else if (e.KeyChar == (Char)Keys.Back)
{
+ if (RowIndex < 0) return;
string countToBuy = dgvPriceList.Rows[RowIndex].Cells[9].Value.ToString();
if (countToBuy.Length > 1)
@@ -945,14 +979,16 @@ and tempOrder.GoodName = PriceList.GoodName
}
else if (e.KeyChar == (Char)Keys.Delete)
{
+ if (RowIndex < 0 || string.IsNullOrEmpty(idPriceListItem)) return;
deleteGood(RowIndex, idPriceListItem);
}
- else if (dgvPriceList.Rows[RowIndex].Cells[9].Value.ToString() == string.Empty && e.KeyChar == (Char)Keys.D0)
+ else if (RowIndex >= 0 && dgvPriceList.Rows[RowIndex].Cells[9].Value?.ToString() == string.Empty && e.KeyChar == (Char)Keys.D0)
{
}
else if (e.KeyChar == (Char)Keys.Down || e.KeyChar == (Char)Keys.Up)
{
+ if (RowIndex < 0 || string.IsNullOrEmpty(idPriceListItem)) return;
timerForResetSearch.Enabled = false;
timerForResetSearch.Stop();
@@ -966,7 +1002,9 @@ and tempOrder.GoodName = PriceList.GoodName
}
}
- string summaZakaza = dgvPriceList.Rows[RowIndex].Cells[10].Value.ToString();
+ if (RowIndex < 0 || RowIndex >= dgvPriceList.Rows.Count) return;
+
+ string summaZakaza = dgvPriceList.Rows[RowIndex].Cells[10].Value?.ToString() ?? string.Empty;
if (summaZakaza.Trim().Length > 0)
@@ -1662,6 +1700,8 @@ and SumOrderedItems = '{SumOrder}'";
private void timerForResetSearch_Tick(object sender, EventArgs e)
{
txtSearchGoodNameInPriceList.Text = string.Empty;
+ GoodsFilter = string.Empty;
+ ApplyPriceListFilters();
timerForResetSearch.Enabled = false;
timerForResetSearch.Stop();
}