Add per-pharmacy carts and client-side summed discount pricing.

Apply available buyer/price-list/region discounts to summary.price on download, keep consignee carts separate, and harden SQLite startup/migrations.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Magomed 2026-07-30 14:07:47 +03:00
parent 458b70e036
commit fd5bd18c66
20 changed files with 1436 additions and 92 deletions

View File

@ -157,6 +157,164 @@ namespace Электронная_Фармация.Classes
}
}
public async Task<List<BuyerDto>> GetBuyersAsync()
{
EnsureAuthenticated();
var responseBody = await SendAuthorizedGetAsync("/api/buyers", "загрузку покупателей");
return DeserializeBuyers(responseBody);
}
public async Task<BuyerDto> GetBuyerByIdAsync(string buyerId)
{
EnsureAuthenticated();
if (string.IsNullOrWhiteSpace(buyerId))
{
throw new ArgumentException("Не указан buyer_id.", nameof(buyerId));
}
var responseBody = await SendAuthorizedGetAsync(
$"/api/buyers/{Uri.EscapeDataString(buyerId.Trim())}",
"загрузку покупателя");
try
{
return JsonSerializer.Deserialize<BuyerDto>(responseBody, JsonOptions);
}
catch (Exception ex)
{
AppDebugLog.Error("ApiClient", "Не удалось разобрать покупателя", ex);
throw new HttpRequestException($"Некорректный ответ сервера при загрузке покупателя: {ex.Message}", ex);
}
}
public async Task<List<BuyerPriceListAssignmentDto>> GetBuyerPriceListsAsync(string buyerId)
{
EnsureAuthenticated();
if (string.IsNullOrWhiteSpace(buyerId))
{
throw new ArgumentException("Не указан buyer_id.", nameof(buyerId));
}
var responseBody = await SendAuthorizedGetAsync(
$"/api/buyers/{Uri.EscapeDataString(buyerId.Trim())}/price-lists",
"загрузку назначений прайсов покупателя");
return DeserializeBuyerPriceLists(responseBody);
}
public async Task<List<PriceListDto>> GetPriceListsAsync()
{
EnsureAuthenticated();
var responseBody = await SendAuthorizedGetAsync("/api/price-lists", "загрузку прайс-листов");
return DeserializePriceLists(responseBody);
}
public async Task<List<PriceListRegionDto>> GetPriceListRegionsAsync(string priceListId)
{
EnsureAuthenticated();
if (string.IsNullOrWhiteSpace(priceListId))
{
throw new ArgumentException("Не указан price_list_id.", nameof(priceListId));
}
var path = "/api/sc/price-list-regions?price_list_id=" + Uri.EscapeDataString(priceListId.Trim());
var responseBody = await SendAuthorizedGetAsync(path, "загрузку региональных наценок прайса");
return DeserializePriceListRegions(responseBody);
}
private static List<BuyerDto> DeserializeBuyers(string responseBody)
{
try
{
using (var doc = JsonDocument.Parse(responseBody))
{
if (doc.RootElement.ValueKind == JsonValueKind.Array)
{
return JsonSerializer.Deserialize<List<BuyerDto>>(responseBody, JsonOptions)
?? new List<BuyerDto>();
}
var wrapped = JsonSerializer.Deserialize<BuyersResponse>(responseBody, JsonOptions);
return wrapped?.Buyers ?? wrapped?.Items ?? new List<BuyerDto>();
}
}
catch (Exception ex)
{
AppDebugLog.Error("ApiClient", "Не удалось разобрать список покупателей", ex);
throw new HttpRequestException($"Некорректный ответ сервера при загрузке покупателей: {ex.Message}", ex);
}
}
private static List<BuyerPriceListAssignmentDto> DeserializeBuyerPriceLists(string responseBody)
{
try
{
using (var doc = JsonDocument.Parse(responseBody))
{
if (doc.RootElement.ValueKind == JsonValueKind.Array)
{
return JsonSerializer.Deserialize<List<BuyerPriceListAssignmentDto>>(responseBody, JsonOptions)
?? new List<BuyerPriceListAssignmentDto>();
}
var wrapped = JsonSerializer.Deserialize<BuyerPriceListsResponse>(responseBody, JsonOptions);
return wrapped?.Items ?? wrapped?.PriceLists ?? new List<BuyerPriceListAssignmentDto>();
}
}
catch (Exception ex)
{
AppDebugLog.Error("ApiClient", "Не удалось разобрать назначения прайсов покупателя", ex);
throw new HttpRequestException(
$"Некорректный ответ сервера при загрузке назначений прайсов: {ex.Message}",
ex);
}
}
private static List<PriceListDto> DeserializePriceLists(string responseBody)
{
try
{
using (var doc = JsonDocument.Parse(responseBody))
{
if (doc.RootElement.ValueKind == JsonValueKind.Array)
{
return JsonSerializer.Deserialize<List<PriceListDto>>(responseBody, JsonOptions)
?? new List<PriceListDto>();
}
var wrapped = JsonSerializer.Deserialize<PriceListsResponse>(responseBody, JsonOptions);
return wrapped?.PriceLists ?? wrapped?.Items ?? new List<PriceListDto>();
}
}
catch (Exception ex)
{
AppDebugLog.Error("ApiClient", "Не удалось разобрать прайс-листы", ex);
throw new HttpRequestException($"Некорректный ответ сервера при загрузке прайс-листов: {ex.Message}", ex);
}
}
private static List<PriceListRegionDto> DeserializePriceListRegions(string responseBody)
{
try
{
using (var doc = JsonDocument.Parse(responseBody))
{
if (doc.RootElement.ValueKind == JsonValueKind.Array)
{
return JsonSerializer.Deserialize<List<PriceListRegionDto>>(responseBody, JsonOptions)
?? new List<PriceListRegionDto>();
}
}
return new List<PriceListRegionDto>();
}
catch (Exception ex)
{
AppDebugLog.Error("ApiClient", "Не удалось разобрать региональные наценки прайса", ex);
throw new HttpRequestException(
$"Некорректный ответ сервера при загрузке региональных наценок: {ex.Message}",
ex);
}
}
public async Task<List<InvoiceApiItem>> GetInvoicesAsync()
{
var orders = await GetBuyerOrdersAsync(status: "Placed", limit: 200);

View File

@ -12,6 +12,8 @@ namespace Электронная_Фармация.Classes
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";
private static bool _sqliteInteropPrepared;
private static readonly object _sqliteInteropLock = new object();
/// <summary>
/// Replaces known dead API URLs in saved user settings.
@ -95,7 +97,11 @@ namespace Электронная_Фармация.Classes
public static string SqliteConnectionString
{
get { return $"Data Source={SqliteDbPath};Version=3;New=False;"; }
get
{
EnsureSQLiteInteropInBaseDir();
return $"Data Source={SqliteDbPath};Version=3;New=False;";
}
}
public static string LocationId
@ -119,5 +125,57 @@ namespace Электронная_Фармация.Classes
Settings.Default.InvoiceExportPath = (path ?? string.Empty).Trim();
Settings.Default.Save();
}
private static void EnsureSQLiteInteropInBaseDir()
{
// System.Data.SQLite ожидает SQLite.Interop.dll рядом с exe.
// В релизной поставке DLL часто лежит в подпапках x86/x64.
if (_sqliteInteropPrepared)
{
return;
}
lock (_sqliteInteropLock)
{
if (_sqliteInteropPrepared)
{
return;
}
var baseDir = AppDomain.CurrentDomain.BaseDirectory;
var targetDll = Path.Combine(baseDir, "SQLite.Interop.dll");
if (File.Exists(targetDll))
{
_sqliteInteropPrepared = true;
return;
}
// AnyCPU в .NET Framework обычно выбирает разрядность процесса автоматически.
var archFolder = Environment.Is64BitProcess ? "x64" : "x86";
var sourceDll = Path.Combine(baseDir, archFolder, "SQLite.Interop.dll");
var sourcePdb = Path.Combine(baseDir, archFolder, "SQLite.Interop.pdb");
try
{
if (File.Exists(sourceDll))
{
Directory.CreateDirectory(baseDir);
File.Copy(sourceDll, targetDll, overwrite: true);
var targetPdb = Path.Combine(baseDir, "SQLite.Interop.pdb");
if (File.Exists(sourcePdb))
{
File.Copy(sourcePdb, targetPdb, overwrite: true);
}
}
}
catch
{
// Если копирование не удалось (права/readonly/и т.п.), дальше будет явное исключение от SQLite.
}
_sqliteInteropPrepared = true;
}
}
}
}

View File

@ -0,0 +1,137 @@
using System.Collections.Generic;
using System.Text.Json.Serialization;
namespace Электроннаяармация.Classes
{
public sealed class BuyerDto
{
[JsonPropertyName("buyer_id")]
public string BuyerId { get; set; }
[JsonPropertyName("id")]
public string Id { get; set; }
[JsonPropertyName("username")]
public string Username { get; set; }
[JsonPropertyName("login")]
public string Login { get; set; }
[JsonPropertyName("name")]
public string Name { get; set; }
public string ResolvedId =>
!string.IsNullOrWhiteSpace(BuyerId) ? BuyerId.Trim() :
!string.IsNullOrWhiteSpace(Id) ? Id.Trim() :
string.Empty;
}
public sealed class BuyersResponse
{
[JsonPropertyName("buyers")]
public List<BuyerDto> Buyers { get; set; }
[JsonPropertyName("items")]
public List<BuyerDto> Items { get; set; }
}
public sealed class BuyerPriceListAssignmentDto
{
[JsonPropertyName("price_list_id")]
public string PriceListId { get; set; }
[JsonPropertyName("name")]
public string Name { get; set; }
[JsonPropertyName("supplier_id")]
public string SupplierId { get; set; }
[JsonPropertyName("supplier_name")]
public string SupplierName { get; set; }
/// <summary>Клиентская скидка на этот прайс.</summary>
[JsonPropertyName("markup_pct")]
public decimal? MarkupPct { get; set; }
/// <summary>Базовая скидка самого прайса.</summary>
[JsonPropertyName("default_markup_pct")]
public decimal? DefaultMarkupPct { get; set; }
}
public sealed class BuyerPriceListsResponse
{
[JsonPropertyName("items")]
public List<BuyerPriceListAssignmentDto> Items { get; set; }
[JsonPropertyName("price_lists")]
public List<BuyerPriceListAssignmentDto> PriceLists { get; set; }
}
public sealed class PriceListDto
{
[JsonPropertyName("price_list_id")]
public string PriceListId { get; set; }
[JsonPropertyName("id")]
public string Id { get; set; }
[JsonPropertyName("name")]
public string Name { get; set; }
[JsonPropertyName("supplier_id")]
public string SupplierId { get; set; }
[JsonPropertyName("supplier_name")]
public string SupplierName { get; set; }
[JsonPropertyName("default_markup_pct")]
public decimal? DefaultMarkupPct { get; set; }
public string ResolvedId =>
!string.IsNullOrWhiteSpace(PriceListId) ? PriceListId.Trim() :
!string.IsNullOrWhiteSpace(Id) ? Id.Trim() :
string.Empty;
}
public sealed class PriceListsResponse
{
[JsonPropertyName("price_lists")]
public List<PriceListDto> PriceLists { get; set; }
[JsonPropertyName("items")]
public List<PriceListDto> Items { get; set; }
}
public sealed class PriceListRegionDto
{
[JsonPropertyName("region_id")]
public string RegionId { get; set; }
[JsonPropertyName("region_name")]
public string RegionName { get; set; }
[JsonPropertyName("in_work")]
public bool? InWork { get; set; }
[JsonPropertyName("markup_pct")]
public decimal? MarkupPct { get; set; }
[JsonPropertyName("base_price_type")]
public string BasePriceType { get; set; }
}
/// <summary>
/// Скидки, доступные для одной позиции прайса.
/// Отсутствующий источник = 0.
/// </summary>
public sealed class ItemDiscountParts
{
public string PriceListId { get; set; }
public decimal PriceListDiscountPct { get; set; }
public decimal RegionDiscountPct { get; set; }
public decimal ClientDiscountPct { get; set; }
public decimal TotalDiscountPct =>
PriceListDiscountPct + RegionDiscountPct + ClientDiscountPct;
}
}

View File

@ -214,5 +214,61 @@ namespace Электронная_Фармация.Classes
return result;
}
/// <summary>
/// Синхронизирует поля заказа в `PriceList` (Zakaz/SummaZakaza) с корзиной выбранной аптеки.
/// Данные корзины хранятся в TempOrderItems с фильтром по ConsigneeName, а PriceList обновляется
/// только для текущего активного просмотра.
/// </summary>
public static void SyncCartToPriceList(string consigneeName)
{
var consignee = (consigneeName ?? string.Empty).Trim().Replace("'", "''");
using (var connection = new SQLiteConnection(AppConfig.SqliteConnectionString))
{
connection.Open();
using (var tx = connection.BeginTransaction())
{
using (var clear = new SQLiteCommand(
@"UPDATE [PriceList]
SET [Zakaz] = NULL, [SummaZakaza] = NULL;", connection, tx))
{
clear.ExecuteNonQuery();
}
using (var sync = new SQLiteCommand(
@"UPDATE [PriceList]
SET
[Zakaz] = (
SELECT t.[Zakaz]
FROM [TempOrderItems] t
WHERE t.[ConsigneeName] = @consignee
AND t.[id_PriceList_Item] = [PriceList].[id_PriceList_Item]
LIMIT 1
),
[SummaZakaza] = (
SELECT t.[SummaZakaza]
FROM [TempOrderItems] t
WHERE t.[ConsigneeName] = @consignee
AND t.[id_PriceList_Item] = [PriceList].[id_PriceList_Item]
LIMIT 1
)
WHERE EXISTS (
SELECT 1
FROM [TempOrderItems] t
WHERE t.[ConsigneeName] = @consignee
AND t.[id_PriceList_Item] = [PriceList].[id_PriceList_Item]
);",
connection,
tx))
{
sync.Parameters.AddWithValue("@consignee", consignee);
sync.ExecuteNonQuery();
}
tx.Commit();
}
}
}
}
}

View File

@ -0,0 +1,321 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Электроннаяармация.Properties;
namespace Электроннаяармация.Classes
{
/// <summary>
/// Собирает доступные скидки покупателя/прайса/региона и вешает на позицию то, что пришло.
/// Отсутствующий источник = 0.
/// </summary>
public static class DiscountResolver
{
public static async Task<DiscountLookup> LoadAsync(ApiClient client, string loginHint)
{
var lookup = new DiscountLookup();
if (client == null)
{
return lookup;
}
var regionId = (Settings.Default.RegionId ?? string.Empty).Trim();
lookup.RegionId = regionId;
string buyerId = null;
try
{
var buyers = await client.GetBuyersAsync();
buyerId = ResolveBuyerId(buyers, loginHint);
lookup.BuyerId = buyerId;
AppDebugLog.Info(
"Discount",
string.IsNullOrWhiteSpace(buyerId)
? "buyer_id не найден — клиентская скидка будет 0"
: $"buyer_id={buyerId}");
}
catch (Exception ex)
{
AppDebugLog.Warning("Discount", $"Не удалось загрузить покупателей: {ex.Message}");
}
List<BuyerPriceListAssignmentDto> assignments = null;
if (!string.IsNullOrWhiteSpace(buyerId))
{
try
{
assignments = await client.GetBuyerPriceListsAsync(buyerId);
AppDebugLog.Info("Discount", $"Назначений прайсов покупателя: {assignments?.Count ?? 0}");
}
catch (Exception ex)
{
AppDebugLog.Warning("Discount", $"Не удалось загрузить buyer price-lists: {ex.Message}");
}
}
List<PriceListDto> priceLists = null;
try
{
priceLists = await client.GetPriceListsAsync();
AppDebugLog.Info("Discount", $"Прайс-листов: {priceLists?.Count ?? 0}");
}
catch (Exception ex)
{
AppDebugLog.Warning("Discount", $"Не удалось загрузить price-lists: {ex.Message}");
}
EnrichAssignmentsFromPriceLists(assignments, priceLists);
if (assignments != null)
{
foreach (var a in assignments)
{
if (string.IsNullOrWhiteSpace(a?.PriceListId))
{
continue;
}
var plId = a.PriceListId.Trim();
lookup.ByPriceListId[plId] = new ItemDiscountParts
{
PriceListId = plId,
ClientDiscountPct = PositiveOrZero(a.MarkupPct),
PriceListDiscountPct = PositiveOrZero(a.DefaultMarkupPct),
RegionDiscountPct = 0m
};
IndexSupplierKeys(lookup, a.SupplierId, a.SupplierName, plId);
}
}
if (priceLists != null)
{
foreach (var pl in priceLists)
{
var plId = pl?.ResolvedId;
if (string.IsNullOrWhiteSpace(plId))
{
continue;
}
if (!lookup.ByPriceListId.TryGetValue(plId, out var parts))
{
parts = new ItemDiscountParts { PriceListId = plId };
lookup.ByPriceListId[plId] = parts;
}
if (parts.PriceListDiscountPct <= 0m && pl.DefaultMarkupPct.HasValue && pl.DefaultMarkupPct.Value > 0m)
{
parts.PriceListDiscountPct = pl.DefaultMarkupPct.Value;
}
IndexSupplierKeys(lookup, pl.SupplierId, pl.SupplierName, plId);
}
}
foreach (var plId in lookup.ByPriceListId.Keys.ToList())
{
try
{
var regions = await client.GetPriceListRegionsAsync(plId);
var regionPct = ResolveRegionDiscount(regions, regionId);
lookup.ByPriceListId[plId].RegionDiscountPct = regionPct;
}
catch (Exception ex)
{
AppDebugLog.Warning("Discount", $"Регионы для прайса {plId}: {ex.Message}");
}
}
AppDebugLog.Info(
"Discount",
$"Готово: прайсов={lookup.ByPriceListId.Count}, привязок по поставщику={lookup.BySupplierKey.Count}, region={regionId}");
return lookup;
}
public static ItemDiscountParts ResolveForItem(DiscountLookup lookup, PriceSummaryItem item)
{
if (lookup == null || item == null)
{
return new ItemDiscountParts();
}
var priceListId = (item.PriceListId ?? string.Empty).Trim();
if (!string.IsNullOrWhiteSpace(priceListId) &&
lookup.ByPriceListId.TryGetValue(priceListId, out var byId))
{
return Clone(byId);
}
var supplierId = (item.SupplierId ?? string.Empty).Trim();
if (!string.IsNullOrWhiteSpace(supplierId) &&
lookup.BySupplierKey.TryGetValue(NormalizeKey(supplierId), out var plFromSupplier) &&
lookup.ByPriceListId.TryGetValue(plFromSupplier, out var bySupplier))
{
return Clone(bySupplier);
}
var supplierName = (item.SupplierName ?? string.Empty).Trim();
if (!string.IsNullOrWhiteSpace(supplierName) &&
lookup.BySupplierKey.TryGetValue(NormalizeKey(supplierName), out var plFromName) &&
lookup.ByPriceListId.TryGetValue(plFromName, out var byName))
{
return Clone(byName);
}
return new ItemDiscountParts();
}
private static string ResolveBuyerId(List<BuyerDto> buyers, string loginHint)
{
if (buyers == null || buyers.Count == 0)
{
return null;
}
var login = (loginHint ?? string.Empty).Trim();
if (!string.IsNullOrWhiteSpace(login))
{
var matched = buyers.FirstOrDefault(b =>
string.Equals((b.Username ?? string.Empty).Trim(), login, StringComparison.OrdinalIgnoreCase) ||
string.Equals((b.Login ?? string.Empty).Trim(), login, StringComparison.OrdinalIgnoreCase) ||
string.Equals((b.Name ?? string.Empty).Trim(), login, StringComparison.OrdinalIgnoreCase));
if (!string.IsNullOrWhiteSpace(matched?.ResolvedId))
{
return matched.ResolvedId;
}
}
if (buyers.Count == 1 && !string.IsNullOrWhiteSpace(buyers[0].ResolvedId))
{
return buyers[0].ResolvedId;
}
return buyers
.Select(b => b.ResolvedId)
.FirstOrDefault(id => !string.IsNullOrWhiteSpace(id));
}
private static void EnrichAssignmentsFromPriceLists(
List<BuyerPriceListAssignmentDto> assignments,
List<PriceListDto> priceLists)
{
if (assignments == null || priceLists == null || priceLists.Count == 0)
{
return;
}
var byId = priceLists
.Where(p => !string.IsNullOrWhiteSpace(p?.ResolvedId))
.GroupBy(p => p.ResolvedId, StringComparer.OrdinalIgnoreCase)
.ToDictionary(g => g.Key, g => g.First(), StringComparer.OrdinalIgnoreCase);
foreach (var a in assignments)
{
if (a == null || string.IsNullOrWhiteSpace(a.PriceListId))
{
continue;
}
if (!byId.TryGetValue(a.PriceListId.Trim(), out var pl))
{
continue;
}
if (string.IsNullOrWhiteSpace(a.SupplierId))
{
a.SupplierId = pl.SupplierId;
}
if (string.IsNullOrWhiteSpace(a.SupplierName))
{
a.SupplierName = pl.SupplierName;
}
if ((!a.DefaultMarkupPct.HasValue || a.DefaultMarkupPct.Value <= 0m) &&
pl.DefaultMarkupPct.HasValue)
{
a.DefaultMarkupPct = pl.DefaultMarkupPct;
}
}
}
private static decimal ResolveRegionDiscount(List<PriceListRegionDto> regions, string regionId)
{
if (regions == null || regions.Count == 0)
{
return 0m;
}
PriceListRegionDto match = null;
if (!string.IsNullOrWhiteSpace(regionId))
{
match = regions.FirstOrDefault(r =>
string.Equals((r.RegionId ?? string.Empty).Trim(), regionId, StringComparison.OrdinalIgnoreCase) &&
(r.InWork == null || r.InWork.Value));
}
if (match == null)
{
match = regions.FirstOrDefault(r => r.InWork == true) ?? regions.FirstOrDefault();
}
return PositiveOrZero(match?.MarkupPct);
}
private static void IndexSupplierKeys(DiscountLookup lookup, string supplierId, string supplierName, string priceListId)
{
if (lookup == null || string.IsNullOrWhiteSpace(priceListId))
{
return;
}
if (!string.IsNullOrWhiteSpace(supplierId))
{
lookup.BySupplierKey[NormalizeKey(supplierId)] = priceListId;
}
if (!string.IsNullOrWhiteSpace(supplierName))
{
lookup.BySupplierKey[NormalizeKey(supplierName)] = priceListId;
}
}
private static ItemDiscountParts Clone(ItemDiscountParts source)
{
if (source == null)
{
return new ItemDiscountParts();
}
return new ItemDiscountParts
{
PriceListId = source.PriceListId,
PriceListDiscountPct = source.PriceListDiscountPct,
RegionDiscountPct = source.RegionDiscountPct,
ClientDiscountPct = source.ClientDiscountPct
};
}
private static decimal PositiveOrZero(decimal? value)
{
return value.HasValue && value.Value > 0m ? value.Value : 0m;
}
private static string NormalizeKey(string value)
{
return (value ?? string.Empty).Trim().ToLowerInvariant();
}
}
public sealed class DiscountLookup
{
public string BuyerId { get; set; }
public string RegionId { get; set; }
public Dictionary<string, ItemDiscountParts> ByPriceListId { get; } =
new Dictionary<string, ItemDiscountParts>(StringComparer.OrdinalIgnoreCase);
public Dictionary<string, string> BySupplierKey { get; } =
new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
}
}

View File

@ -1,29 +1,57 @@
using System;
using System;
namespace Электроннаяармация.Classes
{
public static class MarkupHelper
{
public const decimal FallbackMarkupPercent = 30m;
public const decimal FallbackMarkupPercent = 0m;
/// <summary>
/// Сумма доступных скидок. null / отрицательные значения считаются 0.
/// </summary>
public static decimal SumDiscountPercents(params decimal?[] parts)
{
decimal total = 0m;
if (parts == null)
{
return total;
}
foreach (var part in parts)
{
if (part.HasValue && part.Value > 0m)
{
total += part.Value;
}
}
return total;
}
public static decimal ResolveMarkupPercent(decimal? itemMarkup, decimal? clientMarkup, decimal fallbackMarkup)
{
if (itemMarkup.HasValue && itemMarkup.Value >= 0)
var total = SumDiscountPercents(itemMarkup, clientMarkup);
if (total > 0m)
{
return itemMarkup.Value;
return total;
}
if (clientMarkup.HasValue && clientMarkup.Value >= 0)
{
return clientMarkup.Value;
}
return fallbackMarkup > 0 ? fallbackMarkup : FallbackMarkupPercent;
return fallbackMarkup > 0m ? fallbackMarkup : FallbackMarkupPercent;
}
public static decimal ApplyMarkup(decimal basePrice, decimal markupPercent)
{
return basePrice + (basePrice * markupPercent / 100m);
return ApplyDiscount(basePrice, markupPercent);
}
/// <summary>
/// итоговая = base * (1 - discount/100), минимум 0. Без округления.
/// </summary>
public static decimal ApplyDiscount(decimal basePrice, decimal discountPercent)
{
var effectiveDiscount = Math.Max(0m, discountPercent);
var result = basePrice * (1m - effectiveDiscount / 100m);
return result < 0m ? 0m : result;
}
public static bool TryParseMarkup(object value, out decimal markupPercent)
@ -59,7 +87,12 @@ namespace Электронная_Фармация.Classes
}
var text = value.ToString()?.Trim().Replace(',', '.');
return !string.IsNullOrWhiteSpace(text) && decimal.TryParse(text, System.Globalization.NumberStyles.Any, System.Globalization.CultureInfo.InvariantCulture, out markupPercent);
return !string.IsNullOrWhiteSpace(text) &&
decimal.TryParse(
text,
System.Globalization.NumberStyles.Any,
System.Globalization.CultureInfo.InvariantCulture,
out markupPercent);
}
}
}

View File

@ -16,6 +16,9 @@ namespace Электронная_Фармация.Classes
[JsonPropertyName("supplier_price_id")]
public string SupplierPriceID { get; set; }
[JsonPropertyName("price_list_id")]
public string PriceListId { get; set; }
[JsonPropertyName("guid_es")]
public string GuidEs { get; set; }

View File

@ -114,6 +114,8 @@
<Compile Include="Classes\LoginRequest.cs" />
<Compile Include="Classes\LoginResponse.cs" />
<Compile Include="Classes\MarkupHelper.cs" />
<Compile Include="Classes\BuyerDiscountDtos.cs" />
<Compile Include="Classes\DiscountResolver.cs" />
<Compile Include="Classes\AppServices.cs" />
<Compile Include="Classes\UiDialogs.cs" />
<Compile Include="Classes\UiThemeHelper.cs" />

View File

@ -108,6 +108,11 @@ namespace Электронная_Фармация
_statusFooter.Controls.Add(_lblCurrentUser);
_statusFooter.Controls.Add(_btnSettings);
// Клик по строке "аптека" открывает выбор грузополучателя без перезапуска приложения.
// Т.к. метка у нас одна (Dock=Fill), обработчик навешиваем на весь Label.
_lblCurrentUser.Cursor = Cursors.Hand;
_lblCurrentUser.Click += (_, __) => SwitchConsignee();
Controls.Add(_loadingOverlay);
Controls.Add(_documentHost);
Controls.Add(_statusFooter);
@ -206,7 +211,8 @@ namespace Электронная_Фармация
var login = Settings.Default.stringLogin?.Trim();
var hasToken = !string.IsNullOrWhiteSpace(Settings.Default.stringToken);
var pharmacy = ConsigneeName?.Trim();
if (string.IsNullOrWhiteSpace(pharmacy) || pharmacy == "Прайс-Лист")
if (hasToken &&
(string.IsNullOrWhiteSpace(pharmacy) || pharmacy == "Прайс-Лист"))
{
pharmacy = ConsigneeHelper.GetDefaultConsigneeName();
if (!string.IsNullOrWhiteSpace(pharmacy) &&
@ -230,7 +236,7 @@ namespace Электронная_Фармация
userPart = "Пользователь не авторизован";
}
if (!string.IsNullOrWhiteSpace(pharmacy) && pharmacy != "Прайс-Лист")
if (hasToken && !string.IsNullOrWhiteSpace(pharmacy) && pharmacy != "Прайс-Лист")
{
_lblCurrentUser.Text = $"{userPart} | Аптека: {pharmacy}";
}

View File

@ -14,6 +14,7 @@ using System.Threading.Tasks;
using System.Windows.Forms;
using Elfisa.UI.Theming;
using Электроннаяармация.Classes;
using Электроннаяармация.Properties;
using Электроннаяармация.Forms;
using Электроннаяармация.HelpForms;
using Электроннаяармация.UserControls;
@ -70,9 +71,26 @@ namespace Электронная_Фармация
UiThemeHelper.ApplyToControlTree(fCheckDatabaseIntegrary);
fCheckDatabaseIntegrary.ShowDialog();
var consigneeDialog = new HFConsignees { ParentForm = this };
UiThemeHelper.ApplyToControlTree(consigneeDialog);
consigneeDialog.ShowDialog();
var hasToken = !string.IsNullOrWhiteSpace(Settings.Default.stringToken);
// Окно выбора грузополучателя открываем только после авторизации.
// Если аптек одна — не открываем окно вообще.
int consigneeCount = 0;
using (var con = new SQLiteConnection(AppConfig.SqliteConnectionString))
{
con.Open();
using (var cmd = new SQLiteCommand("SELECT COUNT(*) FROM Consignees", con))
{
consigneeCount = Convert.ToInt32(cmd.ExecuteScalar());
}
}
if (hasToken && consigneeCount > 1)
{
var consigneeDialog = new HFConsignees { ParentForm = this };
UiThemeHelper.ApplyToControlTree(consigneeDialog);
consigneeDialog.ShowDialog();
}
if (string.IsNullOrWhiteSpace(ConsigneeName))
{
@ -109,8 +127,17 @@ namespace Электронная_Фармация
var title = consignee == "Прайс-Лист"
? "Прайс-лист"
: $"Прайс-лист ({consignee})";
// На смене аптеки не должно появляться много вкладок прайса.
// Поэтому перед открытием закрываем все вкладки с navKey="price".
ConsigneeHelper.SyncCartToPriceList(consignee);
foreach (var tab in _documentTabStrip.Tabs.Where(t => t.NavKey == "price").ToList())
{
CloseDocumentTab(tab.Id);
}
var ucPriceList = new UCPriceList(consignee);
OpenDocumentTab(title, "price", ucPriceList, allowDuplicate: true);
OpenDocumentTab(title, "price", ucPriceList, allowDuplicate: false);
ucPriceList.dgvPriceList.Focus();
RefreshCurrentUserStatus();
}
@ -167,9 +194,21 @@ namespace Электронная_Фармация
private void tsConsignee_Click(object sender, EventArgs e)
{
var hasToken = !string.IsNullOrWhiteSpace(Settings.Default.stringToken);
if (!hasToken)
{
UiDialogs.ShowInfo("Сначала выполните авторизацию, чтобы выбрать грузополучателя.", "Авторизация", this);
return;
}
var fConsignees = new FConsignees();
UiThemeHelper.ApplyToControlTree(fConsignees);
fConsignees.ShowDialog();
var dr = fConsignees.ShowDialog(this);
if (dr == DialogResult.OK &&
!string.IsNullOrWhiteSpace(fConsignees.SelectedConsigneeNameAfterSave))
{
showMePriceList(fConsignees.SelectedConsigneeNameAfterSave);
}
}
private void tsItemInvoices_Click(object sender, EventArgs e)
@ -197,14 +236,29 @@ namespace Электронная_Фармация
ConsigneeHelper.SetActiveConsignee(ConsigneeName);
}
// Перед открытием прайс-листа синхронизируем Zakaz/SummaZakaza с корзиной активной аптеки.
ConsigneeHelper.SyncCartToPriceList(ConsigneeName);
var ucPriceList = new UCPriceList(consignee);
OpenDocumentTab($"Прайс-лист ({consignee})", "price", ucPriceList, allowDuplicate: true);
foreach (var tab in _documentTabStrip.Tabs.Where(t => t.NavKey == "price").ToList())
{
CloseDocumentTab(tab.Id);
}
OpenDocumentTab($"Прайс-лист ({consignee})", "price", ucPriceList, allowDuplicate: false);
ucPriceList.dgvPriceList.Focus();
RefreshCurrentUserStatus();
}
private void SwitchConsignee()
{
var hasToken = !string.IsNullOrWhiteSpace(Settings.Default.stringToken);
if (!hasToken)
{
UiDialogs.ShowInfo("Сначала выполните авторизацию, чтобы сменить грузополучателя.", "Авторизация", this);
return;
}
var consigneeDialog = new HFConsignees { ParentForm = this };
UiThemeHelper.ApplyToControlTree(consigneeDialog);
consigneeDialog.ShowDialog(this);
@ -226,7 +280,9 @@ namespace Электронная_Фармация
private void tsExchangeLoad_Click(object sender, EventArgs e)
{
string connectionStringToLocalDB = AppConfig.SqliteConnectionString;
string qShowMeCoountDataFromTempOrder = "select count(*) from TempOrderItems";
var consigneeSafe = (string.IsNullOrWhiteSpace(ConsigneeName) ? ConsigneeHelper.GetDefaultConsigneeName() : ConsigneeName)
?.Replace("'", "''");
string qShowMeCoountDataFromTempOrder = $"select count(*) from TempOrderItems where ConsigneeName = '{consigneeSafe}'";
int countDataTempOrder = 0;
using (SQLiteConnection sqlCon = new SQLiteConnection(connectionStringToLocalDB))
@ -250,6 +306,8 @@ namespace Электронная_Фармация
ClearDocumentTabs();
HF_DownloadDataFromServer hF_DownloadDataFromServer = new HF_DownloadDataFromServer();
hF_DownloadDataFromServer.ActiveConsigneeName =
string.IsNullOrWhiteSpace(ConsigneeName) ? ConsigneeHelper.GetDefaultConsigneeName() : ConsigneeName;
UiThemeHelper.ApplyToControlTree(hF_DownloadDataFromServer);
DialogResult downloadResult = DialogResult.Cancel;
if (countDataTempOrder > 0)
@ -627,6 +685,75 @@ namespace Электронная_Фармация
UiThemeHelper.ApplyToControlTree(hF_Registration);
if (hF_Registration.ShowDialog() == DialogResult.OK)
{
// После успешной авторизации сначала выбираем грузополучателя,
// и только потом (если нужно) запрашиваем Location ID.
int consigneeCount;
using (var con = new SQLiteConnection(AppConfig.SqliteConnectionString))
{
con.Open();
using (var cmd = new SQLiteCommand("SELECT COUNT(*) FROM Consignees", con))
{
consigneeCount = Convert.ToInt32(cmd.ExecuteScalar());
}
}
if (consigneeCount <= 1)
{
var defaultName = ConsigneeHelper.GetDefaultConsigneeName();
if (!string.IsNullOrWhiteSpace(defaultName))
{
ConsigneeName = defaultName;
ConsigneeHelper.SetActiveConsignee(defaultName);
}
}
else
{
var consigneeDialog = new HFConsignees { ParentForm = this };
UiThemeHelper.ApplyToControlTree(consigneeDialog);
if (consigneeDialog.ShowDialog(this) != DialogResult.OK)
{
// Без выбора грузополучателя дальше не идём.
return;
}
}
if (string.IsNullOrWhiteSpace(AppConfig.LocationId))
{
var consigneeName = ConsigneeName ?? string.Empty;
var consigneeAddress = string.Empty;
if (!string.IsNullOrWhiteSpace(consigneeName))
{
using (var con = new SQLiteConnection(AppConfig.SqliteConnectionString))
{
con.Open();
using (var cmd = new SQLiteCommand(
@"SELECT ifnull(ConsigneesAddress, '')
FROM Consignees
WHERE ConsigneesName = @name
LIMIT 1",
con))
{
cmd.Parameters.AddWithValue("@name", consigneeName.Trim());
consigneeAddress = cmd.ExecuteScalar()?.ToString() ?? string.Empty;
}
}
}
if (!HF_LocationId.PromptAndSave(this, consigneeName, consigneeAddress))
{
ToastNotification.ShowCustom(
"Location ID не указан. Укажите его перед отправкой заказов.",
Color.DarkOrange,
Color.White);
return;
}
}
if (!string.IsNullOrWhiteSpace(AppConfig.LocationId))
{
ConsigneeHelper.ApplyLocationIdToEmptyConsignees(AppConfig.LocationId);
}
RefreshCurrentUserStatus();
}
}
@ -643,10 +770,38 @@ namespace Электронная_Фармация
private async void tsBtnSentData_Click(object sender, EventArgs e)
{
var hasToken = !string.IsNullOrWhiteSpace(Settings.Default.stringToken);
if (!hasToken)
{
UiDialogs.ShowInfo("Сначала выполните авторизацию, чтобы отправить заказы.", "Авторизация", this);
return;
}
DialogResult drUpdateData = UiDialogs.ConfirmYesNoCancel("Будут отправлены все сохранённые заказы. Вы уверены?", "Отправка заказов", this);
if (drUpdateData == DialogResult.Yes)
{
var nowUtc = DateTime.UtcNow;
var lastPriceUpdateUtc = Settings.Default.LastPriceUpdateUtc;
var lastPriceReminderUtc = Settings.Default.LastPriceReminderUtc;
var priceIsStale = lastPriceUpdateUtc == DateTime.MinValue ||
(nowUtc - lastPriceUpdateUtc) > TimeSpan.FromHours(24);
var canRemind = lastPriceReminderUtc == DateTime.MinValue ||
(nowUtc - lastPriceReminderUtc) > TimeSpan.FromHours(24);
if (priceIsStale && canRemind)
{
// Напоминание не блокирует отправку.
UiDialogs.ShowInfo(
"Прайс-лист не обновлялся более 24 часов. Рекомендуем обновить прайс перед отправкой заказов.",
"Прайс-лист",
this);
Settings.Default.LastPriceReminderUtc = nowUtc;
Settings.Default.Save();
}
ShowShellLoading("Отправка заказов...");
ToastNotification.ShowCustom("Отправляем данные", Color.DarkOrange, Color.White);
@ -670,7 +825,9 @@ namespace Электронная_Фармация
private void tsBtnDownloadData_Click(object sender, EventArgs e)
{
string connectionStringToLocalDB = AppConfig.SqliteConnectionString;
string qShowMeCoountDataFromTempOrder = "select count(*) from TempOrderItems";
var consigneeSafe = (string.IsNullOrWhiteSpace(ConsigneeName) ? ConsigneeHelper.GetDefaultConsigneeName() : ConsigneeName)
?.Replace("'", "''");
string qShowMeCoountDataFromTempOrder = $"select count(*) from TempOrderItems where ConsigneeName = '{consigneeSafe}'";
int countDataTempOrder = 0;
using (SQLiteConnection sqlCon = new SQLiteConnection(connectionStringToLocalDB))
@ -694,6 +851,8 @@ namespace Электронная_Фармация
ClearDocumentTabs();
HF_DownloadDataFromServer hF_DownloadDataFromServer = new HF_DownloadDataFromServer();
hF_DownloadDataFromServer.ActiveConsigneeName =
string.IsNullOrWhiteSpace(ConsigneeName) ? ConsigneeHelper.GetDefaultConsigneeName() : ConsigneeName;
UiThemeHelper.ApplyToControlTree(hF_DownloadDataFromServer);
DialogResult downloadResult = DialogResult.Cancel;
if (countDataTempOrder > 0)

View File

@ -309,6 +309,7 @@ create table if not exists [OrderItems] (
string strCheckExistsTableTempOrderItems = @"
create table if not exists [TempOrderItems] (
[ConsigneeName] nvarchar(128) not null,
[guid_es] nvarchar(36) not null,
[es_code] integer not null,
[supplier_id] nvarchar(36) not null,
@ -320,8 +321,9 @@ create table if not exists [OrderItems] (
[Description] text,
[Zakaz] real,
[SummaZakaza] nvarchar(64),
[id_PriceList_Item] integer primary key not null,
[supplier_price_id] nvarchar(36)
[id_PriceList_Item] integer not null,
[supplier_price_id] nvarchar(36),
primary key ([ConsigneeName], [id_PriceList_Item])
)
";
@ -394,6 +396,191 @@ create table if not exists [OrderItems] (
cmdCreateNotExistingsTables.CommandText = strCheckExistsTableTempOrderItems;
cmdCreateNotExistingsTables.ExecuteNonQuery();
// Миграция TempOrderItems под раздельную корзину по аптекам.
// Проверяем наличие ConsigneeName и то, что id_PriceList_Item не является одинарным PK.
bool hasConsigneeName = false;
bool idPriceListItemIsSinglePk = false;
using (var infoCmd = new SQLiteCommand("PRAGMA table_info([TempOrderItems]);", conForCheckTables))
using (var infoReader = infoCmd.ExecuteReader())
{
while (infoReader.Read())
{
var colName = infoReader["name"]?.ToString();
var pk = 0;
try { pk = Convert.ToInt32(infoReader["pk"]); } catch { pk = 0; }
if (string.Equals(colName, "ConsigneeName", StringComparison.OrdinalIgnoreCase))
{
hasConsigneeName = true;
}
if (string.Equals(colName, "id_PriceList_Item", StringComparison.OrdinalIgnoreCase) && pk == 1)
{
idPriceListItemIsSinglePk = true;
}
}
}
if (!hasConsigneeName || idPriceListItemIsSinglePk)
{
string defaultConsignee;
using (var defCmd = new SQLiteCommand(
@"SELECT ifnull((SELECT ConsigneesName
FROM Consignees
WHERE ConsigneesUseAsDefault = 1
LIMIT 1), '')",
conForCheckTables))
{
defaultConsignee = defCmd.ExecuteScalar()?.ToString() ?? string.Empty;
}
using (var tx = conForCheckTables.BeginTransaction())
{
using (var cmd = new SQLiteCommand(conForCheckTables))
{
cmd.Transaction = tx;
cmd.CommandText = @"
CREATE TABLE IF NOT EXISTS [TempOrderItems_new] (
[ConsigneeName] nvarchar(128) not null,
[guid_es] nvarchar(36) not null,
[es_code] integer not null,
[supplier_id] nvarchar(36) not null,
[DrugName] nvarchar(256) not null COLLATE NOCASE,
[SupplierName] nvarchar(256) not null,
[Price] nvarchar(32),
[Quantity] integer,
[ExpiryPeriod] nvarchar(64),
[Description] text,
[Zakaz] real,
[SummaZakaza] nvarchar(64),
[id_PriceList_Item] integer not null,
[supplier_price_id] nvarchar(36),
primary key ([ConsigneeName], [id_PriceList_Item])
);";
cmd.ExecuteNonQuery();
cmd.Parameters.Clear();
cmd.Parameters.AddWithValue("@defaultConsignee", defaultConsignee.Trim());
cmd.CommandText = @"
INSERT INTO [TempOrderItems_new] (
[ConsigneeName],
[guid_es],
[es_code],
[supplier_id],
[DrugName],
[SupplierName],
[Price],
[Quantity],
[ExpiryPeriod],
[Description],
[Zakaz],
[SummaZakaza],
[id_PriceList_Item],
[supplier_price_id]
)
SELECT
@defaultConsignee,
[guid_es],
[es_code],
[supplier_id],
[DrugName],
[SupplierName],
[Price],
[Quantity],
[ExpiryPeriod],
[Description],
[Zakaz],
[SummaZakaza],
[id_PriceList_Item],
[supplier_price_id]
FROM [TempOrderItems];";
cmd.ExecuteNonQuery();
cmd.Parameters.Clear();
cmd.CommandText = @"DROP TABLE [TempOrderItems];";
cmd.ExecuteNonQuery();
cmd.CommandText = @"ALTER TABLE [TempOrderItems_new] RENAME TO [TempOrderItems];";
cmd.ExecuteNonQuery();
}
tx.Commit();
}
}
// Перед seed-вставкой грузополучателей нужно гарантировать наличие LocationId,
// иначе INSERT в Consignees (LocationId) упадёт на старых версиях БД.
TryAddColumn(conForCheckTables, "Consignees", "LocationId", "nvarchar(64)");
// Seed-тестовые грузополучатели (аптеки) для удобства ручного тестирования.
// Вставляем только если таблица пустая.
using (var countConsigneesCmd = new SQLiteCommand(
@"SELECT COUNT(*) FROM Consignees;",
conForCheckTables))
{
var countObj = countConsigneesCmd.ExecuteScalar();
var count = 0;
try { count = Convert.ToInt32(countObj); } catch { count = 0; }
if (count == 0)
{
var testLocationIds = new[]
{
"87368f6d-488b-41a9-b170-edc629522844",
"c4cd845f-820d-45ac-a473-3df1a0b96feb",
"d6187013-00f5-4c1b-b258-c670cf71d115"
};
// Тестовые имена/адреса — можно менять под вашу тестовую среду.
var testNames = new[]
{
"Тестовая аптека 1",
"Тестовая аптека 2",
"Тестовая аптека 3"
};
var testAddresses = new[]
{
"Тестовый адрес 1",
"Тестовый адрес 2",
"Тестовый адрес 3"
};
using (var tx = conForCheckTables.BeginTransaction())
{
int nextCode;
using (var nextCodeCmd = new SQLiteCommand(
@"SELECT ifnull(MAX(codeConsignees), 0) + 1 FROM Consignees;",
conForCheckTables,
tx))
{
nextCode = Convert.ToInt32(nextCodeCmd.ExecuteScalar());
}
for (var i = 0; i < 3; i++)
{
using (var insertCmd = new SQLiteCommand(
@"INSERT INTO Consignees
(codeConsignees, ConsigneesName, ConsigneesAddress, ConsigneesUseAsDefault, LocationId)
VALUES (@code, @name, @address, @isDefault, @loc);",
conForCheckTables,
tx))
{
insertCmd.Parameters.AddWithValue("@code", nextCode + i);
insertCmd.Parameters.AddWithValue("@name", testNames[i]);
insertCmd.Parameters.AddWithValue("@address", testAddresses[i]);
insertCmd.Parameters.AddWithValue("@isDefault", i == 0 ? 1 : 0);
insertCmd.Parameters.AddWithValue("@loc", testLocationIds[i]);
insertCmd.ExecuteNonQuery();
}
}
tx.Commit();
}
}
}
TryAddColumn(conForCheckTables, "Orders", "ConsigneeName", "nvarchar(128)");
TryAddColumn(conForCheckTables, "Invoice", "SupplierName", "nvarchar(256)");
TryAddColumn(conForCheckTables, "Invoice", "ConsigneesName", "nvarchar(128)");
@ -406,11 +593,28 @@ create table if not exists [OrderItems] (
TryAddColumn(conForCheckTables, "PriceList", "TradeName", "nvarchar(256)");
TryAddColumn(conForCheckTables, "PriceList", "Dosage", "nvarchar(128)");
TryAddColumn(conForCheckTables, "PriceList", "MarkupPercent", "real");
TryAddColumn(conForCheckTables, "Consignees", "LocationId", "nvarchar(64)");
ConsigneeHelper.MigrateGlobalLocationIdIfNeeded(conForCheckTables);
}
catch (Exception ex)
{
try
{
var logDir = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Logs");
Directory.CreateDirectory(logDir);
var logPath = Path.Combine(
logDir,
$"db-integrity-err-{DateTime.Now:yyyy-MM-dd}.log");
File.AppendAllText(
logPath,
$"{DateTime.Now:yyyy-MM-dd HH:mm:ss.fff} {ex}{Environment.NewLine}{Environment.NewLine}",
Encoding.UTF8);
}
catch
{
// Логирование не должно скрывать исходную ошибку.
}
UiDialogs.ShowError($"Возникла ошибка при проверке локальной базы на целостность.\n{ex}", "Ошибка целостности", this);
}
finally

View File

@ -8,6 +8,8 @@ namespace Электронная_Фармация.Forms
{
public partial class FConsignees : Form
{
public string SelectedConsigneeNameAfterSave { get; private set; } = string.Empty;
public FConsignees()
{
InitializeComponent();
@ -86,6 +88,12 @@ order by [ConsigneesUseAsDefault] desc, [ConsigneesName]";
dgvConsignees.EndEdit();
// Кто должен стать активной аптекой после сохранения.
// По вашему ТЗ: "аптека выбранная в этом окне" => текущая выделенная строка.
var selectedName = dgvConsignees.CurrentRow?.Cells["Наименование"]?.Value?.ToString()?.Trim()
?? string.Empty;
SelectedConsigneeNameAfterSave = selectedName;
using (var connection = new SQLiteConnection(AppConfig.SqliteConnectionString))
{
connection.Open();
@ -119,7 +127,14 @@ order by [ConsigneesUseAsDefault] desc, [ConsigneesName]";
table.AcceptChanges();
ToastNotification.ShowSuccess("Грузополучатели сохранены");
showMeContentFromConsigneesTable();
if (!string.IsNullOrWhiteSpace(selectedName))
{
ConsigneeHelper.SetActiveConsignee(selectedName);
}
DialogResult = DialogResult.OK;
Close();
}
private void btnAdd_Click(object sender, EventArgs e)
@ -129,23 +144,62 @@ order by [ConsigneesUseAsDefault] desc, [ConsigneesName]";
dialog.Text = "Новая аптека";
dialog.FormBorderStyle = FormBorderStyle.FixedDialog;
dialog.StartPosition = FormStartPosition.CenterParent;
dialog.ClientSize = new System.Drawing.Size(420, 220);
// Немного увеличиваем высоту/ширину на случай DPI/шрифтов.
dialog.ClientSize = new System.Drawing.Size(480, 310);
dialog.MaximizeBox = false;
dialog.MinimizeBox = false;
dialog.ShowInTaskbar = false;
var lblName = new Label { Text = "Наименование", Left = 16, Top = 16, Width = 380 };
var txtName = new TextBox { Left = 16, Top = 40, Width = 380 };
var lblAddress = new Label { Text = "Адрес", Left = 16, Top = 76, Width = 380 };
var txtAddress = new TextBox { Left = 16, Top = 100, Width = 380 };
var lblLocation = new Label { Text = "Location ID", Left = 16, Top = 136, Width = 380 };
var txtLocation = new TextBox { Left = 16, Top = 160, Width = 380 };
var btnOk = new Button { Text = "Добавить", DialogResult = DialogResult.OK, Left = 220, Top = 188, Width = 80 };
var btnCancel = new Button { Text = "Отмена", DialogResult = DialogResult.Cancel, Left = 310, Top = 188, Width = 80 };
dialog.Controls.AddRange(new Control[] { lblName, txtName, lblAddress, txtAddress, lblLocation, txtLocation, btnOk, btnCancel });
var lblName = new Label { Text = "Наименование", Left = 16, Top = 16, AutoSize = true };
var txtName = new TextBox { Left = 16, Top = 42, Width = 440, Height = 28 };
var lblAddress = new Label { Text = "Адрес", Left = 16, Top = 82, AutoSize = true };
var txtAddress = new TextBox { Left = 16, Top = 108, Width = 440, Height = 28 };
var lblLocation = new Label { Text = "Location ID", Left = 16, Top = 148, AutoSize = true };
var txtLocation = new TextBox { Left = 16, Top = 174, Width = 440, Height = 28 };
var btnOk = new Elfisa.UI.Controls.ModernButton
{
Text = "Добавить",
DialogResult = DialogResult.OK
};
var btnCancel = new Elfisa.UI.Controls.ModernButton
{
Text = "Отмена",
DialogResult = DialogResult.Cancel
};
dialog.Controls.AddRange(new Control[]
{
lblName, txtName, lblAddress, txtAddress, lblLocation, txtLocation, btnOk, btnCancel
});
dialog.AcceptButton = btnOk;
dialog.CancelButton = btnCancel;
UiThemeHelper.ApplyToControlTree(dialog);
btnOk.Padding = new Padding(12, 4, 12, 4);
btnCancel.Padding = new Padding(12, 4, 12, 4);
Elfisa.UI.Controls.ModernButtonStyles.FitToText(btnOk, 36);
Elfisa.UI.Controls.ModernButtonStyles.FitToText(btnCancel, 36);
btnOk.Height = 36;
btnCancel.Height = 36;
const int padding = 16;
const int gap = 10;
// Минимально гарантируем ширину, чтобы обе кнопки поместились.
var requiredWidth = btnOk.Width + btnCancel.Width + gap + padding * 2;
if (dialog.ClientSize.Width < requiredWidth)
{
dialog.ClientSize = new System.Drawing.Size(requiredWidth, dialog.ClientSize.Height);
}
// Всегда зажимаем в границы формы после вычисления ширины ModernButton'ов.
btnCancel.Left = Math.Max(padding, dialog.ClientSize.Width - btnCancel.Width - padding);
btnCancel.Top = dialog.ClientSize.Height - btnCancel.Height - padding;
btnOk.Left = Math.Max(padding, btnCancel.Left - btnOk.Width - gap);
btnOk.Top = btnCancel.Top;
if (dialog.ShowDialog(this) != DialogResult.OK)
{
return;

View File

@ -24,6 +24,10 @@ namespace Электронная_Фармация.HelpForms
InitializeComponent();
}
// Чтобы при обновлении прайса не терять корзины других аптек,
// чистим TempOrderItems только для активного грузополучателя.
public string ActiveConsigneeName { get; set; } = string.Empty;
private readonly string _login = Settings.Default.stringLogin ?? string.Empty;
private readonly string _password = Settings.Default.stringPassword ?? string.Empty;
private readonly string _connectionStringToLocalDb = AppConfig.SqliteConnectionString;
@ -54,10 +58,23 @@ namespace Электронная_Фармация.HelpForms
AppendStatus("Загружаю сводный прайс...");
var allSuppliersSummary = await GetFullPriceSummaryPagedAsync(client);
int totalItems = allSuppliersSummary.Summary?.Count ?? 0;
AppendStatus("Загружаю скидки покупателя / прайса / региона...");
var discountLookup = await DiscountResolver.LoadAsync(client, _login);
if (!string.IsNullOrWhiteSpace(discountLookup.BuyerId))
{
AppendStatus($"buyer_id: {discountLookup.BuyerId}");
}
else
{
AppendStatus("buyer_id не найден — клиентская скидка будет 0, если не пришла иначе");
}
AppendStatus($"Прайс-листов со скидками: {discountLookup.ByPriceListId.Count}");
ApplyClientMarkupSettings(allSuppliersSummary);
if (allSuppliersSummary.MarkupPercent.HasValue)
{
AppendStatus($"Наценка клиента: {allSuppliersSummary.MarkupPercent.Value:0.##}%");
AppendStatus($"Скидка из summary: {allSuppliersSummary.MarkupPercent.Value:0.##}%");
}
AppendStatus($"Получено {totalItems} позиций");
@ -71,7 +88,7 @@ namespace Электронная_Фармация.HelpForms
phasePercent,
$"Обработка: {p.Current} / {p.Total} ({PercentOf(p.Current, p.Total)}%)");
});
var tablePrice = await Task.Run(() => BuildPriceTable(allSuppliersSummary, buildProgress));
var tablePrice = await Task.Run(() => BuildPriceTable(allSuppliersSummary, discountLookup, buildProgress));
SetOverallProgress(BuildPhaseEnd, "Сохранение в локальную базу...");
var saveProgress = new Progress<CountProgress>(p =>
@ -87,6 +104,10 @@ namespace Электронная_Фармация.HelpForms
DialogResult = DialogResult.OK;
AppendStatus($"Готово. Сохранено {tablePrice.Rows.Count} позиций в локальную базу.");
ToastNotification.ShowSuccess($"Прайс обновлён: {tablePrice.Rows.Count} позиций");
Settings.Default.LastPriceUpdateUtc = DateTime.UtcNow;
Settings.Default.LastPriceReminderUtc = DateTime.MinValue;
Settings.Default.Save();
}
catch (UnauthorizedAccessException ex)
{
@ -377,6 +398,7 @@ namespace Электронная_Фармация.HelpForms
private static DataTable BuildPriceTable(
PriceSummaryResponse allSuppliersSummary,
DiscountLookup discountLookup,
IProgress<CountProgress> progress = null)
{
var tablePrice = new DataTable();
@ -426,7 +448,7 @@ namespace Электронная_Фармация.HelpForms
return tablePrice;
}
var clientMarkup = allSuppliersSummary.MarkupPercent;
var summaryClientDiscount = allSuppliersSummary.MarkupPercent;
int total = allSuppliersSummary.Summary.Count;
int current = 0;
foreach (var item in allSuppliersSummary.Summary)
@ -438,7 +460,20 @@ namespace Электронная_Фармация.HelpForms
row["supplier_id"] = item.SupplierId ?? string.Empty;
row["DrugName"] = item.DrugName ?? string.Empty;
row["SupplierName"] = item.SupplierName ?? string.Empty;
row["Price"] = item.Price?.ToString() ?? string.Empty;
var parts = DiscountResolver.ResolveForItem(discountLookup, item);
// Вешаем только то, что пришло. Нет значения = 0.
// summary.markup_percent / item.markup_percent — доп. источник, если API ещё отдаёт.
var totalDiscount = MarkupHelper.SumDiscountPercents(
parts.PriceListDiscountPct,
parts.RegionDiscountPct,
parts.ClientDiscountPct > 0m ? parts.ClientDiscountPct : (decimal?)null,
parts.ClientDiscountPct <= 0m ? summaryClientDiscount : null,
item.MarkupPercent);
var basePrice = item.Price ?? 0m;
var finalPrice = MarkupHelper.ApplyDiscount(basePrice, totalDiscount);
row["Price"] = finalPrice.ToString(System.Globalization.CultureInfo.InvariantCulture);
row["Quantity"] = item.Quantity?.ToString() ?? string.Empty;
row["ExpiryPeriod"] = item.ExpiryPeriod == null ? (object)DBNull.Value : item.ExpiryPeriod;
row["Description"] = item.Description == null ? (object)DBNull.Value : item.Description;
@ -446,8 +481,7 @@ namespace Электронная_Фармация.HelpForms
row["SummaZakaza"] = string.Empty;
row["TradeName"] = string.IsNullOrWhiteSpace(item.TradeName) ? (object)DBNull.Value : item.TradeName.Trim();
row["Dosage"] = string.IsNullOrWhiteSpace(item.Dosage) ? (object)DBNull.Value : item.Dosage.Trim();
var markupPercent = MarkupHelper.ResolveMarkupPercent(item.MarkupPercent, clientMarkup, Settings.Default.ClientMarkupPercent);
row["MarkupPercent"] = markupPercent;
row["MarkupPercent"] = totalDiscount;
tablePrice.Rows.Add(row);
current++;
@ -463,7 +497,9 @@ namespace Электронная_Фармация.HelpForms
private void SavePriceTable(DataTable tablePrice, IProgress<CountProgress> progress = null)
{
const string commandToDelete = "delete from [PriceList]";
const string commandToDeleteTempOrder = "delete from [TempOrderItems]";
var commandToDeleteTempOrder = string.IsNullOrWhiteSpace(ActiveConsigneeName)
? "delete from [TempOrderItems]"
: "delete from [TempOrderItems] where [ConsigneeName] = @consignee";
const string commandToInsert = @"INSERT INTO [PriceList] (
[guid_es],
[es_code],
@ -509,6 +545,13 @@ VALUES
using (var cmdDeleteTempOrder = new SQLiteCommand(commandToDeleteTempOrder, sqliteCon))
{
cmdDeleteBeforeInsert.ExecuteNonQuery();
if (!string.IsNullOrWhiteSpace(ActiveConsigneeName) &&
commandToDeleteTempOrder.Contains("@consignee"))
{
cmdDeleteTempOrder.Parameters.AddWithValue("@consignee", ActiveConsigneeName.Trim());
}
cmdDeleteTempOrder.ExecuteNonQuery();
}
@ -735,3 +778,5 @@ VALUES
}
}
}

View File

@ -55,40 +55,58 @@ namespace Электронная_Фармация.HelpForms
this.lblHint.Size = new System.Drawing.Size(420, 48);
this.lblHint.TabIndex = 1;
this.lblHint.Text = "Укажите UUID торговой точки. Он будет использоваться при отправке заказов на сервер.";
// lblConsigneeName
this.lblConsigneeName.AutoSize = true;
this.lblConsigneeName.Font = new System.Drawing.Font("Segoe UI Semibold", 10F, System.Drawing.FontStyle.Bold);
this.lblConsigneeName.Location = new System.Drawing.Point(20, 128);
this.lblConsigneeName.Name = "lblConsigneeName";
this.lblConsigneeName.Size = new System.Drawing.Size(0, 0);
this.lblConsigneeName.TabIndex = 2;
this.lblConsigneeName.Text = "";
// lblConsigneeAddress
this.lblConsigneeAddress.AutoSize = true;
this.lblConsigneeAddress.Font = new System.Drawing.Font("Segoe UI", 10F);
this.lblConsigneeAddress.Location = new System.Drawing.Point(20, 150);
this.lblConsigneeAddress.Name = "lblConsigneeAddress";
this.lblConsigneeAddress.Size = new System.Drawing.Size(0, 0);
this.lblConsigneeAddress.TabIndex = 3;
this.lblConsigneeAddress.Text = "";
//
// lblLocationId
//
this.lblLocationId.AutoSize = true;
this.lblLocationId.Font = new System.Drawing.Font("Segoe UI", 12F);
this.lblLocationId.Location = new System.Drawing.Point(20, 128);
this.lblLocationId.Location = new System.Drawing.Point(20, 182);
this.lblLocationId.Name = "lblLocationId";
this.lblLocationId.Size = new System.Drawing.Size(108, 28);
this.lblLocationId.TabIndex = 2;
this.lblLocationId.TabIndex = 4;
this.lblLocationId.Text = "Location ID";
//
// txtLocationId
//
this.txtLocationId.Font = new System.Drawing.Font("Segoe UI", 12F);
this.txtLocationId.Location = new System.Drawing.Point(24, 160);
this.txtLocationId.Location = new System.Drawing.Point(24, 214);
this.txtLocationId.Name = "txtLocationId";
this.txtLocationId.Size = new System.Drawing.Size(412, 34);
this.txtLocationId.TabIndex = 3;
this.txtLocationId.TabIndex = 5;
//
// btnCancel
//
this.btnCancel.Location = new System.Drawing.Point(24, 220);
this.btnCancel.Location = new System.Drawing.Point(24, 275);
this.btnCancel.Name = "btnCancel";
this.btnCancel.Size = new System.Drawing.Size(125, 40);
this.btnCancel.TabIndex = 4;
this.btnCancel.TabIndex = 6;
this.btnCancel.Text = "Отмена";
this.btnCancel.Click += new System.EventHandler(this.btnCancel_Click);
//
// btnSave
//
this.btnSave.Location = new System.Drawing.Point(221, 220);
this.btnSave.Location = new System.Drawing.Point(221, 275);
this.btnSave.Name = "btnSave";
this.btnSave.Size = new System.Drawing.Size(215, 40);
this.btnSave.TabIndex = 5;
this.btnSave.TabIndex = 7;
this.btnSave.Text = "Сохранить";
this.btnSave.Click += new System.EventHandler(this.btnSave_Click);
//
@ -98,10 +116,12 @@ namespace Электронная_Фармация.HelpForms
this.AutoScaleDimensions = new System.Drawing.SizeF(8F, 16F);
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
this.CancelButton = this.btnCancel;
this.ClientSize = new System.Drawing.Size(460, 280);
this.ClientSize = new System.Drawing.Size(460, 330);
this.Controls.Add(this.btnSave);
this.Controls.Add(this.btnCancel);
this.Controls.Add(this.txtLocationId);
this.Controls.Add(this.lblConsigneeAddress);
this.Controls.Add(this.lblConsigneeName);
this.Controls.Add(this.lblLocationId);
this.Controls.Add(this.lblHint);
this.Controls.Add(this.panelHeader);
@ -123,6 +143,8 @@ namespace Электронная_Фармация.HelpForms
private System.Windows.Forms.Panel panelHeader;
private System.Windows.Forms.Label lblTitle;
private System.Windows.Forms.Label lblHint;
private System.Windows.Forms.Label lblConsigneeName;
private System.Windows.Forms.Label lblConsigneeAddress;
private System.Windows.Forms.Label lblLocationId;
private Elfisa.UI.Controls.ModernTextBox txtLocationId;
private Elfisa.UI.Controls.ModernButton btnCancel;

View File

@ -10,6 +10,9 @@ namespace Электронная_Фармация.HelpForms
{
public partial class HF_LocationId : Form
{
private string _consigneeName = string.Empty;
private string _consigneeAddress = string.Empty;
public HF_LocationId()
{
InitializeComponent();
@ -28,6 +31,20 @@ namespace Электронная_Фармация.HelpForms
}
}
/// <summary>
/// Показывает Location ID с контекстом: выбранная аптека (имя/адрес).
/// </summary>
public static bool PromptAndSave(IWin32Window owner, string consigneeName, string consigneeAddress)
{
using (var form = new HF_LocationId())
{
form._consigneeName = consigneeName ?? string.Empty;
form._consigneeAddress = consigneeAddress ?? string.Empty;
UiThemeHelper.ApplyToControlTree(form);
return form.ShowDialog(owner) == DialogResult.OK;
}
}
private void HF_LocationId_Load(object sender, EventArgs e)
{
UiThemeHelper.ApplyToControlTree(this);
@ -35,6 +52,18 @@ namespace Электронная_Фармация.HelpForms
txtLocationId.Text = Settings.Default.LocationId ?? string.Empty;
txtLocationId.SelectAll();
txtLocationId.Focus();
if (lblConsigneeName != null)
{
lblConsigneeName.Text = _consigneeName;
lblConsigneeName.Visible = !string.IsNullOrWhiteSpace(_consigneeName);
}
if (lblConsigneeAddress != null)
{
lblConsigneeAddress.Text = _consigneeAddress;
lblConsigneeAddress.Visible = !string.IsNullOrWhiteSpace(_consigneeAddress);
}
}
private void btnSave_Click(object sender, EventArgs e)

View File

@ -56,26 +56,6 @@ namespace Электронная_Фармация.HelpForms
AppDebugLog.Info("Auth", $"Авторизация успешна. token={AppDebugLog.MaskToken(token)}");
ToastNotification.ShowSuccess("Авторизация успешно выполнена");
// Fallback Location ID (на аптеку задаётся у грузополучателя).
if (string.IsNullOrWhiteSpace(AppConfig.LocationId))
{
if (HF_LocationId.PromptAndSave(this))
{
ConsigneeHelper.ApplyLocationIdToEmptyConsignees(AppConfig.LocationId);
}
else
{
ToastNotification.ShowCustom(
"Location ID не указан. Задайте его у грузополучателя или в настройках.",
System.Drawing.Color.DarkOrange,
System.Drawing.Color.White);
}
}
else
{
ConsigneeHelper.ApplyLocationIdToEmptyConsignees(AppConfig.LocationId);
}
DialogResult = DialogResult.OK;
Close();
return;

View File

@ -143,5 +143,29 @@ namespace Электронная_Фармация.Properties {
}
}
[global::System.Configuration.UserScopedSettingAttribute()]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Configuration.DefaultSettingValueAttribute("01.01.0001 00:00:00")]
public global::System.DateTime LastPriceUpdateUtc {
get {
return ((global::System.DateTime)(this["LastPriceUpdateUtc"]));
}
set {
this["LastPriceUpdateUtc"] = value;
}
}
[global::System.Configuration.UserScopedSettingAttribute()]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Configuration.DefaultSettingValueAttribute("01.01.0001 00:00:00")]
public global::System.DateTime LastPriceReminderUtc {
get {
return ((global::System.DateTime)(this["LastPriceReminderUtc"]));
}
set {
this["LastPriceReminderUtc"] = value;
}
}
}
}

View File

@ -32,5 +32,11 @@
<Setting Name="InvoiceExportPath" Type="System.String" Scope="User">
<Value Profile="(Default)" />
</Setting>
<Setting Name="LastPriceUpdateUtc" Type="System.DateTime" Scope="User">
<Value Profile="(Default)">01.01.0001 00:00:00</Value>
</Setting>
<Setting Name="LastPriceReminderUtc" Type="System.DateTime" Scope="User">
<Value Profile="(Default)">01.01.0001 00:00:00</Value>
</Setting>
</Settings>
</SettingsFile>

View File

@ -11,6 +11,7 @@ using System.Data.SQLite;
using Электроннаяармация.UserControls;
using System.Data.Common;
using Электроннаяармация.Classes;
using Электроннаяармация.Properties;
using Elfisa.UI.Theming;
namespace Электроннаяармация.UserControls
@ -39,7 +40,24 @@ namespace Электронная_Фармация.UserControls
public void loadDataAboutOrderV2(string Supplier)
{
string qShowMeOrderItems = "select * from TempOrderItems";
var consigneeSafe = (_consigneeName ?? string.Empty).Replace("'", "''");
string qShowMeOrderItems = $@"
SELECT
[guid_es],
[es_code],
[supplier_id],
[DrugName],
[SupplierName],
[Price],
[Quantity],
[ExpiryPeriod],
[Description],
[Zakaz],
[SummaZakaza],
[id_PriceList_Item],
[supplier_price_id]
FROM [TempOrderItems]
WHERE [ConsigneeName] = '{consigneeSafe}'";
//string qShowMeSumOfOrder = "select cast(sum(Price * Zakaz) as real) from TempOrderItems";
@ -53,7 +71,8 @@ namespace Электронная_Фармация.UserControls
if (Supplier != string.Empty && Supplier != "Корзина")
{
qShowMeOrderItems += $" where [SupplierName] = '{Supplier}'";
var supplierSafe = Supplier.Replace("'", "''");
qShowMeOrderItems += $" and [SupplierName] = '{supplierSafe}'";
}
SQLiteCommand cmdShowMeTempOrderItems = new SQLiteCommand(qShowMeOrderItems, conTempOrder);
@ -286,8 +305,9 @@ and ConsigneesName = '{consigneeName}'
private void DeleteCartItem(string idPriceListItem, string drugName)
{
var safeId = idPriceListItem.Replace("'", "''");
var consigneeSafe = (_consigneeName ?? string.Empty).Replace("'", "''");
string qClearPriceList = $"update [PriceList] set [Zakaz] = null, [SummaZakaza] = null where id_PriceList_Item = '{safeId}'";
string qDeleteFromTempOrder = $"delete from [TempOrderItems] where [id_PriceList_Item] = '{safeId}'";
string qDeleteFromTempOrder = $"delete from [TempOrderItems] where [id_PriceList_Item] = '{safeId}' and [ConsigneeName] = '{consigneeSafe}'";
using (var sqlCon = new SQLiteConnection(connectionStringToLocalDB))
{
@ -324,18 +344,26 @@ and ConsigneesName = '{consigneeName}'
{
using(SQLiteConnection sqlConForDeleteTempOrder = new SQLiteConnection(connectionStringToLocalDB))
{
var supplierSafe = (Supplier ?? string.Empty).Replace("'", "''");
var consigneeSafe = (_consigneeName ?? string.Empty).Replace("'", "''");
string qDeletePriceListItemForASupplier;
string qDeleteTempOrderForASupplier;
if (Supplier != string.Empty)
if (supplierSafe != string.Empty)
{
qDeletePriceListItemForASupplier = $"update [PriceList] set [Zakaz] = null, [SummaZakaza] = null where [id_PriceList_Item] in (select [id_PriceList_Item] from [TempOrderItems] where [SupplierName] = '{Supplier}')";
qDeleteTempOrderForASupplier = $"delete from [TempOrderItems] where [SupplierName] = '{Supplier}'";
qDeletePriceListItemForASupplier =
$"update [PriceList] set [Zakaz] = null, [SummaZakaza] = null where [id_PriceList_Item] in " +
$"(select [id_PriceList_Item] from [TempOrderItems] where [SupplierName] = '{supplierSafe}' and [ConsigneeName] = '{consigneeSafe}')";
qDeleteTempOrderForASupplier =
$"delete from [TempOrderItems] where [SupplierName] = '{supplierSafe}' and [ConsigneeName] = '{consigneeSafe}'";
}
else
{
qDeletePriceListItemForASupplier = $"update [PriceList] set [Zakaz] = null, [SummaZakaza] = null";
qDeleteTempOrderForASupplier = $"delete from [TempOrderItems]";
qDeletePriceListItemForASupplier =
$"update [PriceList] set [Zakaz] = null, [SummaZakaza] = null " +
$"where [id_PriceList_Item] in (select [id_PriceList_Item] from [TempOrderItems] where [ConsigneeName] = '{consigneeSafe}')";
qDeleteTempOrderForASupplier =
$"delete from [TempOrderItems] where [ConsigneeName] = '{consigneeSafe}'";
}
@ -423,10 +451,18 @@ ConsigneesName = '{consigneeName}'
private void btnSendOrder_Click(object sender, EventArgs e)
{
var hasToken = !string.IsNullOrWhiteSpace(Settings.Default.stringToken);
if (!hasToken)
{
UiDialogs.ShowInfo("Сначала выполните авторизацию, чтобы создать заказ.", "Авторизация", FindForm());
return;
}
string connectionStringToLocalDB = AppConfig.SqliteConnectionString;
string SupplierName = _supplierName;
string consigneeName = _consigneeName;
var consigneeSafe = (consigneeName ?? string.Empty).Replace("'", "''");
@ -465,6 +501,7 @@ select
SupplierName
from TempOrderItems
where zakaz > 0
and [ConsigneeName] = '{consigneeSafe}'
{filterAddon}
group by SupplierName
";
@ -520,6 +557,8 @@ group by SupplierName
string OrderNumber = $"EP-{DateTime.Now:yyyyMMdd-HHmmss}-{Guid.NewGuid().ToString("N").Substring(0, 6)}";
string OrderDate = DateTime.Now.ToString("dd.MM.yyyy");
_consigneeName = ParentForm?.ConsigneeName ?? _consigneeName ?? string.Empty;
var consigneeSafe = (_consigneeName ?? string.Empty).Replace("'", "''");
var supNameSafe = (SupName ?? string.Empty).Replace("'", "''");
if (HasStockIssues(SupName, out var stockIssue))
{
@ -532,14 +571,16 @@ select
count(*)
from TempOrderItems
where Zakaz > 0
and SupplierName = '{SupName.Replace("'", "''")}'
and SupplierName = '{supNameSafe}'
and [ConsigneeName] = '{consigneeSafe}'
";
string qShowMeSumOrderedItems = $@"
select
ifnull(sum(cast(replace(SummaZakaza, ',', '.') as real)), 0)
from TempOrderItems
where Zakaz > 0
and SupplierName = '{SupName.Replace("'", "''")}'
and SupplierName = '{supNameSafe}'
and [ConsigneeName] = '{consigneeSafe}'
";
string qShowMeMinimalSumOfOrder = $@"
select
@ -549,7 +590,7 @@ where SuppliersName = '{SupName.Replace("'", "''")}'
";
string qDisposeTempOrder = $@"
delete from TempOrderItems where SupplierName = '{SupName}'
delete from TempOrderItems where SupplierName = '{supNameSafe}' and [ConsigneeName] = '{consigneeSafe}'
";
string connectionStringToLocalDB = AppConfig.SqliteConnectionString;
@ -594,12 +635,13 @@ select
'{OrderDate}',
supplier_id,
SupplierName,
(select count(*) from TempOrderItems where SupplierName = '{SupName.Replace("'", "''")}'),
(select count(*) from TempOrderItems where SupplierName = '{supNameSafe}' and [ConsigneeName] = '{consigneeSafe}'),
'{sumOrder.ToString(System.Globalization.CultureInfo.InvariantCulture)}',
'НОВЫЙ',
'{_consigneeName.Replace("'", "''")}'
from TempOrderItems
where SupplierName = '{SupName.Replace("'", "''")}'
where SupplierName = '{supNameSafe}'
and [ConsigneeName] = '{consigneeSafe}'
group by supplier_id, SupplierName
";
@ -616,7 +658,7 @@ select
[ExpiryPeriod],
[supplier_price_id]
from TempOrderItems
where SupplierName = '{SupName}'
where SupplierName = '{supNameSafe}' and [ConsigneeName] = '{consigneeSafe}'
";
#region старыеЗапросыСозданияЗаказа

View File

@ -1072,10 +1072,12 @@ namespace Электронная_Фармация.UserControls
List<String> SuppliersFromOrder = new List<String>();
var consigneeSafe = (_WorkWithConsignee ?? string.Empty).Replace("'", "''");
string qShowMeSuppliersFromOrder = $@"
select
[SupplierName]
from [TempOrderItems]
where [ConsigneeName] = '{consigneeSafe}'
group by [SupplierName]
";
@ -1759,9 +1761,10 @@ and tempOrder.GoodName = PriceList.GoodName
void creatingTempOrder(string idPriceListItem, string zakaz, string summaZakaza)
{
var consigneeSafe = (_WorkWithConsignee ?? string.Empty).Replace("'", "''");
string qUpdatePriceList = $"update [PriceList] set [Zakaz] = '{zakaz}', [SummaZakaza] = '{summaZakaza}' where [id_PriceList_Item] = '{idPriceListItem}'";
string qDeleteFromTempOrder = $"delete from [TempOrderItems] where [id_PriceList_Item] = '{idPriceListItem}'";
string qCreateNewTempOrder = $"insert into TempOrderItems([guid_es],[es_code],[supplier_id],[DrugName],[SupplierName],[Price],[Quantity],[ExpiryPeriod],[Description],[Zakaz],[SummaZakaza],[id_PriceList_Item],[supplier_price_id]) select [guid_es],[es_code],[supplier_id],[DrugName],[SupplierName],[Price],[Quantity],[ExpiryPeriod],[Description],[Zakaz],[SummaZakaza],[id_PriceList_Item],[supplier_price_id] from [PriceList] where [id_PriceList_Item] = '{idPriceListItem}'";
string qDeleteFromTempOrder = $"delete from [TempOrderItems] where [id_PriceList_Item] = '{idPriceListItem}' and [ConsigneeName] = '{consigneeSafe}'";
string qCreateNewTempOrder = $"insert into TempOrderItems([ConsigneeName],[guid_es],[es_code],[supplier_id],[DrugName],[SupplierName],[Price],[Quantity],[ExpiryPeriod],[Description],[Zakaz],[SummaZakaza],[id_PriceList_Item],[supplier_price_id]) select '{consigneeSafe}',[guid_es],[es_code],[supplier_id],[DrugName],[SupplierName],[Price],[Quantity],[ExpiryPeriod],[Description],[Zakaz],[SummaZakaza],[id_PriceList_Item],[supplier_price_id] from [PriceList] where [id_PriceList_Item] = '{idPriceListItem}'";
using(SQLiteConnection sqlCon = new SQLiteConnection(connectionStringToLocalDB))
{
@ -1795,13 +1798,14 @@ and tempOrder.GoodName = PriceList.GoodName
void deleteGood(int index, string IdPriceListItem)
{
var consigneeSafe = (_WorkWithConsignee ?? string.Empty).Replace("'", "''");
dgvPriceList.Rows[index].Cells[9].Value = string.Empty;
dgvPriceList.Rows[index].Cells[10].Value = string.Empty;
string IdPLI = IdPriceListItem;
string qClearPriceList = $"update [PriceList] set [Zakaz] = null, [SummaZakaza] = null where id_PriceList_Item = '{IdPLI}'";
string qDeleteFromTempOrder = $"delete from [TempOrderItems] where [id_PriceList_Item] = '{IdPLI}'";
string qDeleteFromTempOrder = $"delete from [TempOrderItems] where [id_PriceList_Item] = '{IdPLI}' and [ConsigneeName] = '{consigneeSafe}'";
using(SQLiteConnection sqlCon = new SQLiteConnection(connectionStringToLocalDB))
{
@ -2025,8 +2029,7 @@ and SumOrderedItems = '{SumOrder}'";
var markupPercentage = GetMarkupPercentForRow(currentRow);
numericPercent.Value = Math.Max(numericPercent.Minimum, Math.Min(numericPercent.Maximum, markupPercentage));
var priceWithPercentage = MarkupHelper.ApplyMarkup((decimal)priceForGood, markupPercentage);
lblPriceForGood.Text = $"= {priceWithPercentage:0.##} руб";
lblPriceForGood.Text = $"= {priceForGood:0.##} руб";
}
catch
{
@ -2493,3 +2496,5 @@ and SumOrderedItems = '{SumOrder}'";
//}
}
}