Release v1.0.23: global order numbers EX-####### (GLOBAL_SIGN).
Desktop reserves/sends EX numbers from server; supplier DBF gets GLOBAL_SIGN column.
This commit is contained in:
parent
d2a1a86794
commit
63abfd4e31
@ -143,6 +143,7 @@ namespace Электронная_Фармация.Classes
|
|||||||
string query = @"
|
string query = @"
|
||||||
SELECT
|
SELECT
|
||||||
Orders.Id_Order,
|
Orders.Id_Order,
|
||||||
|
Orders.OrderNumber,
|
||||||
Orders.ConsigneeName,
|
Orders.ConsigneeName,
|
||||||
OrderItems.supplier_price_id,
|
OrderItems.supplier_price_id,
|
||||||
OrderItems.DrugName,
|
OrderItems.DrugName,
|
||||||
@ -172,6 +173,9 @@ WHERE Orders.OrderState = 'НОВЫЙ'
|
|||||||
while (reader.Read())
|
while (reader.Read())
|
||||||
{
|
{
|
||||||
string orderId = reader["Id_Order"].ToString().Trim();
|
string orderId = reader["Id_Order"].ToString().Trim();
|
||||||
|
string orderNumber = reader["OrderNumber"] == DBNull.Value
|
||||||
|
? string.Empty
|
||||||
|
: reader["OrderNumber"].ToString().Trim();
|
||||||
string consigneeName = reader["ConsigneeName"] == DBNull.Value
|
string consigneeName = reader["ConsigneeName"] == DBNull.Value
|
||||||
? string.Empty
|
? string.Empty
|
||||||
: reader["ConsigneeName"].ToString().Trim();
|
: reader["ConsigneeName"].ToString().Trim();
|
||||||
@ -196,6 +200,9 @@ WHERE Orders.OrderState = 'НОВЫЙ'
|
|||||||
order = new BuyerOrderRequest
|
order = new BuyerOrderRequest
|
||||||
{
|
{
|
||||||
LocalOrderId = orderId,
|
LocalOrderId = orderId,
|
||||||
|
GlobalSign = OrderNumberHelper.IsValidEx(orderNumber)
|
||||||
|
? OrderNumberHelper.Normalize(orderNumber)
|
||||||
|
: null,
|
||||||
LocationId = locationId,
|
LocationId = locationId,
|
||||||
Comment = reader["Comment"] == DBNull.Value ? null : reader["Comment"].ToString()
|
Comment = reader["Comment"] == DBNull.Value ? null : reader["Comment"].ToString()
|
||||||
};
|
};
|
||||||
@ -324,10 +331,25 @@ WHERE Orders.OrderState = 'НОВЫЙ'
|
|||||||
throw new InvalidOperationException("Сервер не вернул order_id созданного заказа.");
|
throw new InvalidOperationException("Сервер не вернул order_id созданного заказа.");
|
||||||
}
|
}
|
||||||
|
|
||||||
MarkOrderSent(order.LocalOrderId, createdOrder.OrderID, createdOrder.Status);
|
// Старые серверы создавали Draft — дожимаем до Placed.
|
||||||
AppDebugLog.Info("DataSender", $"Заказ {order.LocalOrderId} отправлен");
|
var remoteStatus = createdOrder.Status;
|
||||||
|
if (!string.Equals(remoteStatus, "Placed", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
remoteStatus = await PlaceOrderAsync(client, createdOrder.OrderID) ?? remoteStatus;
|
||||||
|
}
|
||||||
|
|
||||||
|
MarkOrderSent(order.LocalOrderId, createdOrder.OrderID, remoteStatus, createdOrder.GlobalSign);
|
||||||
|
AppDebugLog.Info("DataSender",
|
||||||
|
$"Заказ {order.LocalOrderId} отправлен, remote={createdOrder.OrderID}, status={remoteStatus}, sign={createdOrder.GlobalSign}");
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
catch (InvalidOperationException ex)
|
||||||
|
{
|
||||||
|
stopwatch.Stop();
|
||||||
|
AppDebugLog.Error("DataSender", $"Ошибка отправки заказа {order.LocalOrderId}", ex);
|
||||||
|
ToastNotification.ShowError(ex.Message);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
stopwatch.Stop();
|
stopwatch.Stop();
|
||||||
@ -338,7 +360,67 @@ WHERE Orders.OrderState = 'НОВЫЙ'
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void MarkOrderSent(string localOrderId, string remoteOrderId, string remoteStatus)
|
private async Task<string> PlaceOrderAsync(HttpClient client, string remoteOrderId)
|
||||||
|
{
|
||||||
|
var placePath = $"{BuyerOrdersEndpoint}/{Uri.EscapeDataString(remoteOrderId)}/place";
|
||||||
|
var fullUrl = new Uri(new Uri(_baseUrl.TrimEnd('/') + "/"), placePath.TrimStart('/')).ToString();
|
||||||
|
var stopwatch = System.Diagnostics.Stopwatch.StartNew();
|
||||||
|
AppDebugLog.ApiRequest("POST", fullUrl, string.Empty, $"place={remoteOrderId}");
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using (var content = new StringContent(string.Empty, Encoding.UTF8, "application/json"))
|
||||||
|
using (HttpResponseMessage response = await client.PostAsync(placePath, content))
|
||||||
|
{
|
||||||
|
string responseString = await response.Content.ReadAsStringAsync();
|
||||||
|
stopwatch.Stop();
|
||||||
|
AppDebugLog.ApiResponse("POST", fullUrl, (int)response.StatusCode, stopwatch.ElapsedMilliseconds, responseString);
|
||||||
|
|
||||||
|
if (!response.IsSuccessStatusCode)
|
||||||
|
{
|
||||||
|
AppDebugLog.ApiHttpError(
|
||||||
|
"POST",
|
||||||
|
fullUrl,
|
||||||
|
(int)response.StatusCode,
|
||||||
|
responseString,
|
||||||
|
$"place={remoteOrderId}");
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
$"Не удалось оформить заказ на сервере (HTTP {(int)response.StatusCode}). " +
|
||||||
|
"Заказ остался черновиком (Draft).");
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using (var doc = JsonDocument.Parse(responseString))
|
||||||
|
{
|
||||||
|
if (doc.RootElement.TryGetProperty("status", out var statusEl))
|
||||||
|
{
|
||||||
|
return statusEl.GetString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// ignore parse — place succeeded
|
||||||
|
}
|
||||||
|
|
||||||
|
return "Placed";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (InvalidOperationException)
|
||||||
|
{
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
stopwatch.Stop();
|
||||||
|
AppDebugLog.ApiFailure("POST", fullUrl, stopwatch.ElapsedMilliseconds, ex);
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
$"Не удалось оформить заказ на сервере: {ex.Message}", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void MarkOrderSent(string localOrderId, string remoteOrderId, string remoteStatus, string globalSign)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(localOrderId))
|
if (string.IsNullOrWhiteSpace(localOrderId))
|
||||||
{
|
{
|
||||||
@ -351,12 +433,18 @@ WHERE Orders.OrderState = 'НОВЫЙ'
|
|||||||
using (var cmd = new SQLiteCommand(
|
using (var cmd = new SQLiteCommand(
|
||||||
@"UPDATE [Orders]
|
@"UPDATE [Orders]
|
||||||
SET [OrderState] = @state,
|
SET [OrderState] = @state,
|
||||||
[RemoteOrderId] = @remoteOrderId
|
[RemoteOrderId] = @remoteOrderId,
|
||||||
|
[OrderNumber] = CASE
|
||||||
|
WHEN @globalSign IS NOT NULL AND length(trim(@globalSign)) > 0 THEN @globalSign
|
||||||
|
ELSE [OrderNumber]
|
||||||
|
END
|
||||||
WHERE [Id_Order] = @id AND [OrderState] = 'НОВЫЙ'",
|
WHERE [Id_Order] = @id AND [OrderState] = 'НОВЫЙ'",
|
||||||
con))
|
con))
|
||||||
{
|
{
|
||||||
cmd.Parameters.AddWithValue("@state", "ОТПРАВЛЕН");
|
cmd.Parameters.AddWithValue("@state", "ОТПРАВЛЕН");
|
||||||
cmd.Parameters.AddWithValue("@remoteOrderId", remoteOrderId ?? string.Empty);
|
cmd.Parameters.AddWithValue("@remoteOrderId", remoteOrderId ?? string.Empty);
|
||||||
|
cmd.Parameters.AddWithValue("@globalSign",
|
||||||
|
string.IsNullOrWhiteSpace(globalSign) ? (object)DBNull.Value : globalSign.Trim());
|
||||||
cmd.Parameters.AddWithValue("@id", localOrderId);
|
cmd.Parameters.AddWithValue("@id", localOrderId);
|
||||||
cmd.ExecuteNonQuery();
|
cmd.ExecuteNonQuery();
|
||||||
}
|
}
|
||||||
@ -393,6 +481,9 @@ WHERE Orders.OrderState = 'НОВЫЙ'
|
|||||||
[JsonPropertyName("location_id")]
|
[JsonPropertyName("location_id")]
|
||||||
public string LocationId { get; set; }
|
public string LocationId { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("global_sign")]
|
||||||
|
public string GlobalSign { get; set; }
|
||||||
|
|
||||||
[JsonPropertyName("comment")]
|
[JsonPropertyName("comment")]
|
||||||
public string Comment { get; set; }
|
public string Comment { get; set; }
|
||||||
|
|
||||||
@ -422,5 +513,8 @@ WHERE Orders.OrderState = 'НОВЫЙ'
|
|||||||
|
|
||||||
[JsonPropertyName("status")]
|
[JsonPropertyName("status")]
|
||||||
public string Status { get; set; }
|
public string Status { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("global_sign")]
|
||||||
|
public string GlobalSign { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
157
src/ElectronicPharmacy/Classes/OrderNumberHelper.cs
Normal file
157
src/ElectronicPharmacy/Classes/OrderNumberHelper.cs
Normal file
@ -0,0 +1,157 @@
|
|||||||
|
using System;
|
||||||
|
using System.Data.SQLite;
|
||||||
|
using System.Net.Http;
|
||||||
|
using System.Net.Http.Headers;
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Электронная_Фармация.Properties;
|
||||||
|
|
||||||
|
namespace Электронная_Фармация.Classes
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Глобальные номера заказов вида EX-0000001.
|
||||||
|
/// Источник истины — сервер (никогда не повторяются между аптеками).
|
||||||
|
/// Offline fallback — локальный счётчик EX- (потом сервер может заменить при отправке).
|
||||||
|
/// </summary>
|
||||||
|
public static class OrderNumberHelper
|
||||||
|
{
|
||||||
|
private static readonly Regex ExPattern = new Regex(@"^EX-(\d{1,7})$", RegexOptions.IgnoreCase | RegexOptions.Compiled);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Выделяет номер для нового локального заказа.
|
||||||
|
/// Сначала пробует сервер /api/buyer/global-sign/next, иначе локальный EX-#######.
|
||||||
|
/// </summary>
|
||||||
|
public static string AllocateForNewOrder()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var fromServer = AllocateFromServerAsync().GetAwaiter().GetResult();
|
||||||
|
if (!string.IsNullOrWhiteSpace(fromServer))
|
||||||
|
{
|
||||||
|
return fromServer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
AppDebugLog.Info("OrderNumber", $"Сервер недоступен для GlobalSign: {ex.Message}");
|
||||||
|
}
|
||||||
|
|
||||||
|
return AllocateLocalFallback();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async Task<string> AllocateFromServerAsync()
|
||||||
|
{
|
||||||
|
var token = Settings.Default.stringToken;
|
||||||
|
var baseUrl = AppConfig.ApiBaseUrl;
|
||||||
|
if (string.IsNullOrWhiteSpace(token) || string.IsNullOrWhiteSpace(baseUrl))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
using (var client = new HttpClient { Timeout = TimeSpan.FromSeconds(8) })
|
||||||
|
{
|
||||||
|
client.BaseAddress = new Uri(baseUrl.TrimEnd('/') + "/");
|
||||||
|
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token.Trim());
|
||||||
|
client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
|
||||||
|
|
||||||
|
using (var response = await client.PostAsync("api/buyer/global-sign/next", new StringContent(string.Empty)).ConfigureAwait(false))
|
||||||
|
{
|
||||||
|
var body = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
|
||||||
|
if (!response.IsSuccessStatusCode)
|
||||||
|
{
|
||||||
|
AppDebugLog.Info("OrderNumber", $"global-sign/next HTTP {(int)response.StatusCode}: {body}");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
using (var doc = JsonDocument.Parse(body))
|
||||||
|
{
|
||||||
|
if (doc.RootElement.TryGetProperty("global_sign", out var el))
|
||||||
|
{
|
||||||
|
var sign = el.GetString();
|
||||||
|
if (IsValidEx(sign))
|
||||||
|
{
|
||||||
|
return Normalize(sign);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string AllocateLocalFallback()
|
||||||
|
{
|
||||||
|
long max = 0;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using (var con = new SQLiteConnection(AppConfig.SqliteConnectionString))
|
||||||
|
{
|
||||||
|
con.Open();
|
||||||
|
using (var cmd = new SQLiteCommand(
|
||||||
|
"SELECT OrderNumber FROM Orders WHERE OrderNumber LIKE 'EX-%'", con))
|
||||||
|
using (var reader = cmd.ExecuteReader())
|
||||||
|
{
|
||||||
|
while (reader.Read())
|
||||||
|
{
|
||||||
|
var n = ParseExNumber(reader[0]?.ToString());
|
||||||
|
if (n > max)
|
||||||
|
{
|
||||||
|
max = n;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
AppDebugLog.Info("OrderNumber", $"Локальный max EX-: {ex.Message}");
|
||||||
|
}
|
||||||
|
|
||||||
|
return FormatEx(max + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool IsValidEx(string value)
|
||||||
|
{
|
||||||
|
return ParseExNumber(value) > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string Normalize(string value)
|
||||||
|
{
|
||||||
|
var n = ParseExNumber(value);
|
||||||
|
return n > 0 ? FormatEx(n) : value?.Trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string FormatEx(long n)
|
||||||
|
{
|
||||||
|
if (n < 1)
|
||||||
|
{
|
||||||
|
n = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (n > 9999999)
|
||||||
|
{
|
||||||
|
return "EX-" + n.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
return "EX-" + n.ToString("D7");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static long ParseExNumber(string value)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(value))
|
||||||
|
{
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
var m = ExPattern.Match(value.Trim());
|
||||||
|
if (!m.Success)
|
||||||
|
{
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return long.TryParse(m.Groups[1].Value, out var n) ? n : 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -113,6 +113,7 @@
|
|||||||
<Compile Include="Classes\ConsigneeHelper.cs" />
|
<Compile Include="Classes\ConsigneeHelper.cs" />
|
||||||
<Compile Include="Classes\SqlSyntaxHighlighter.cs" />
|
<Compile Include="Classes\SqlSyntaxHighlighter.cs" />
|
||||||
<Compile Include="Classes\DataSender.cs" />
|
<Compile Include="Classes\DataSender.cs" />
|
||||||
|
<Compile Include="Classes\OrderNumberHelper.cs" />
|
||||||
<Compile Include="Classes\ErrorResponse.cs" />
|
<Compile Include="Classes\ErrorResponse.cs" />
|
||||||
<Compile Include="Classes\InvoiceExportService.cs" />
|
<Compile Include="Classes\InvoiceExportService.cs" />
|
||||||
<Compile Include="Classes\InvoiceRequestService.cs" />
|
<Compile Include="Classes\InvoiceRequestService.cs" />
|
||||||
|
|||||||
@ -32,6 +32,6 @@ using System.Runtime.InteropServices;
|
|||||||
// пїЅпїЅпїЅпїЅпїЅ пїЅпїЅпїЅпїЅпїЅпїЅ пїЅпїЅпїЅ пїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅ пїЅпїЅпїЅ пїЅпїЅпїЅпїЅпїЅпїЅпїЅ пїЅпїЅпїЅпїЅпїЅпїЅ пїЅпїЅпїЅпїЅпїЅпїЅ пїЅ пїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅ пїЅпїЅ пїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅ
|
// пїЅпїЅпїЅпїЅпїЅ пїЅпїЅпїЅпїЅпїЅпїЅ пїЅпїЅпїЅ пїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅ пїЅпїЅпїЅ пїЅпїЅпїЅпїЅпїЅпїЅпїЅ пїЅпїЅпїЅпїЅпїЅпїЅ пїЅпїЅпїЅпїЅпїЅпїЅ пїЅ пїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅ пїЅпїЅ пїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅ
|
||||||
// пїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅ "*", пїЅпїЅпїЅ пїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅ пїЅпїЅпїЅпїЅ:
|
// пїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅ "*", пїЅпїЅпїЅ пїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅ пїЅпїЅпїЅпїЅ:
|
||||||
// [assembly: AssemblyVersion("1.0.*")]
|
// [assembly: AssemblyVersion("1.0.*")]
|
||||||
[assembly: AssemblyVersion("1.0.22.0")]
|
[assembly: AssemblyVersion("1.0.23.0")]
|
||||||
[assembly: AssemblyFileVersion("1.0.22.0")]
|
[assembly: AssemblyFileVersion("1.0.23.0")]
|
||||||
|
|
||||||
|
|||||||
@ -554,7 +554,8 @@ group by SupplierName
|
|||||||
|
|
||||||
void createOrderForNextSuppliers(string SupName)
|
void createOrderForNextSuppliers(string SupName)
|
||||||
{
|
{
|
||||||
string OrderNumber = $"EP-{DateTime.Now:yyyyMMdd-HHmmss}-{Guid.NewGuid().ToString("N").Substring(0, 6)}";
|
// Глобальный номер EX-0000001… (сервер / локальный fallback).
|
||||||
|
string OrderNumber = OrderNumberHelper.AllocateForNewOrder();
|
||||||
string OrderDate = DateTime.Now.ToString("yyyy-MM-dd");
|
string OrderDate = DateTime.Now.ToString("yyyy-MM-dd");
|
||||||
_consigneeName = ParentForm?.ConsigneeName ?? _consigneeName ?? string.Empty;
|
_consigneeName = ParentForm?.ConsigneeName ?? _consigneeName ?? string.Empty;
|
||||||
var consigneeSafe = (_consigneeName ?? string.Empty).Replace("'", "''");
|
var consigneeSafe = (_consigneeName ?? string.Empty).Replace("'", "''");
|
||||||
|
|||||||
@ -1521,7 +1521,7 @@ BestBefore = '{BestBefore}'
|
|||||||
{
|
{
|
||||||
string connectionStringToLocalDB = AppConfig.SqliteConnectionString;
|
string connectionStringToLocalDB = AppConfig.SqliteConnectionString;
|
||||||
|
|
||||||
string OrderNumber = $"EP-{DateTime.Now.Millisecond.ToString()}{DateTime.Now.Date.ToString("mmdd")}";
|
string OrderNumber = OrderNumberHelper.AllocateForNewOrder();
|
||||||
string OrderDate = DateTime.Now.ToString("yyyy-MM-dd");//DateTime.Now.ToShortDateString();
|
string OrderDate = DateTime.Now.ToString("yyyy-MM-dd");//DateTime.Now.ToShortDateString();
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user