diff --git a/src/ElectronicPharmacy/Classes/ApiClient.cs b/src/ElectronicPharmacy/Classes/ApiClient.cs index e1e9894..33ae71c 100644 --- a/src/ElectronicPharmacy/Classes/ApiClient.cs +++ b/src/ElectronicPharmacy/Classes/ApiClient.cs @@ -157,6 +157,164 @@ namespace Электронная_Фармация.Classes } } + public async Task> GetBuyersAsync() + { + EnsureAuthenticated(); + var responseBody = await SendAuthorizedGetAsync("/api/buyers", "загрузку покупателей"); + return DeserializeBuyers(responseBody); + } + + public async Task 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(responseBody, JsonOptions); + } + catch (Exception ex) + { + AppDebugLog.Error("ApiClient", "Не удалось разобрать покупателя", ex); + throw new HttpRequestException($"Некорректный ответ сервера при загрузке покупателя: {ex.Message}", ex); + } + } + + public async Task> 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> GetPriceListsAsync() + { + EnsureAuthenticated(); + var responseBody = await SendAuthorizedGetAsync("/api/price-lists", "загрузку прайс-листов"); + return DeserializePriceLists(responseBody); + } + + public async Task> 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 DeserializeBuyers(string responseBody) + { + try + { + using (var doc = JsonDocument.Parse(responseBody)) + { + if (doc.RootElement.ValueKind == JsonValueKind.Array) + { + return JsonSerializer.Deserialize>(responseBody, JsonOptions) + ?? new List(); + } + + var wrapped = JsonSerializer.Deserialize(responseBody, JsonOptions); + return wrapped?.Buyers ?? wrapped?.Items ?? new List(); + } + } + catch (Exception ex) + { + AppDebugLog.Error("ApiClient", "Не удалось разобрать список покупателей", ex); + throw new HttpRequestException($"Некорректный ответ сервера при загрузке покупателей: {ex.Message}", ex); + } + } + + private static List DeserializeBuyerPriceLists(string responseBody) + { + try + { + using (var doc = JsonDocument.Parse(responseBody)) + { + if (doc.RootElement.ValueKind == JsonValueKind.Array) + { + return JsonSerializer.Deserialize>(responseBody, JsonOptions) + ?? new List(); + } + + var wrapped = JsonSerializer.Deserialize(responseBody, JsonOptions); + return wrapped?.Items ?? wrapped?.PriceLists ?? new List(); + } + } + catch (Exception ex) + { + AppDebugLog.Error("ApiClient", "Не удалось разобрать назначения прайсов покупателя", ex); + throw new HttpRequestException( + $"Некорректный ответ сервера при загрузке назначений прайсов: {ex.Message}", + ex); + } + } + + private static List DeserializePriceLists(string responseBody) + { + try + { + using (var doc = JsonDocument.Parse(responseBody)) + { + if (doc.RootElement.ValueKind == JsonValueKind.Array) + { + return JsonSerializer.Deserialize>(responseBody, JsonOptions) + ?? new List(); + } + + var wrapped = JsonSerializer.Deserialize(responseBody, JsonOptions); + return wrapped?.PriceLists ?? wrapped?.Items ?? new List(); + } + } + catch (Exception ex) + { + AppDebugLog.Error("ApiClient", "Не удалось разобрать прайс-листы", ex); + throw new HttpRequestException($"Некорректный ответ сервера при загрузке прайс-листов: {ex.Message}", ex); + } + } + + private static List DeserializePriceListRegions(string responseBody) + { + try + { + using (var doc = JsonDocument.Parse(responseBody)) + { + if (doc.RootElement.ValueKind == JsonValueKind.Array) + { + return JsonSerializer.Deserialize>(responseBody, JsonOptions) + ?? new List(); + } + } + + return new List(); + } + catch (Exception ex) + { + AppDebugLog.Error("ApiClient", "Не удалось разобрать региональные наценки прайса", ex); + throw new HttpRequestException( + $"Некорректный ответ сервера при загрузке региональных наценок: {ex.Message}", + ex); + } + } + public async Task> GetInvoicesAsync() { var orders = await GetBuyerOrdersAsync(status: "Placed", limit: 200); diff --git a/src/ElectronicPharmacy/Classes/AppConfig.cs b/src/ElectronicPharmacy/Classes/AppConfig.cs index 459c3dc..52affaa 100644 --- a/src/ElectronicPharmacy/Classes/AppConfig.cs +++ b/src/ElectronicPharmacy/Classes/AppConfig.cs @@ -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(); /// /// 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; + } + } } } diff --git a/src/ElectronicPharmacy/Classes/BuyerDiscountDtos.cs b/src/ElectronicPharmacy/Classes/BuyerDiscountDtos.cs new file mode 100644 index 0000000..a5c76f3 --- /dev/null +++ b/src/ElectronicPharmacy/Classes/BuyerDiscountDtos.cs @@ -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 Buyers { get; set; } + + [JsonPropertyName("items")] + public List 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; } + + /// Клиентская скидка на этот прайс. + [JsonPropertyName("markup_pct")] + public decimal? MarkupPct { get; set; } + + /// Базовая скидка самого прайса. + [JsonPropertyName("default_markup_pct")] + public decimal? DefaultMarkupPct { get; set; } + } + + public sealed class BuyerPriceListsResponse + { + [JsonPropertyName("items")] + public List Items { get; set; } + + [JsonPropertyName("price_lists")] + public List 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 PriceLists { get; set; } + + [JsonPropertyName("items")] + public List 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; } + } + + /// + /// Скидки, доступные для одной позиции прайса. + /// Отсутствующий источник = 0. + /// + 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; + } +} diff --git a/src/ElectronicPharmacy/Classes/ConsigneeHelper.cs b/src/ElectronicPharmacy/Classes/ConsigneeHelper.cs index 4c91df2..cd8dcda 100644 --- a/src/ElectronicPharmacy/Classes/ConsigneeHelper.cs +++ b/src/ElectronicPharmacy/Classes/ConsigneeHelper.cs @@ -214,5 +214,61 @@ namespace Электронная_Фармация.Classes return result; } + + /// + /// Синхронизирует поля заказа в `PriceList` (Zakaz/SummaZakaza) с корзиной выбранной аптеки. + /// Данные корзины хранятся в TempOrderItems с фильтром по ConsigneeName, а PriceList обновляется + /// только для текущего активного просмотра. + /// + 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(); + } + } + } } } diff --git a/src/ElectronicPharmacy/Classes/DiscountResolver.cs b/src/ElectronicPharmacy/Classes/DiscountResolver.cs new file mode 100644 index 0000000..076d4f6 --- /dev/null +++ b/src/ElectronicPharmacy/Classes/DiscountResolver.cs @@ -0,0 +1,321 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Электронная_Фармация.Properties; + +namespace Электронная_Фармация.Classes +{ + /// + /// Собирает доступные скидки покупателя/прайса/региона и вешает на позицию то, что пришло. + /// Отсутствующий источник = 0. + /// + public static class DiscountResolver + { + public static async Task 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 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 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 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 assignments, + List 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 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 ByPriceListId { get; } = + new Dictionary(StringComparer.OrdinalIgnoreCase); + public Dictionary BySupplierKey { get; } = + new Dictionary(StringComparer.OrdinalIgnoreCase); + } +} diff --git a/src/ElectronicPharmacy/Classes/MarkupHelper.cs b/src/ElectronicPharmacy/Classes/MarkupHelper.cs index 73e5c68..41d2b65 100644 --- a/src/ElectronicPharmacy/Classes/MarkupHelper.cs +++ b/src/ElectronicPharmacy/Classes/MarkupHelper.cs @@ -1,29 +1,57 @@ -using System; +using System; namespace Электронная_Фармация.Classes { public static class MarkupHelper { - public const decimal FallbackMarkupPercent = 30m; + public const decimal FallbackMarkupPercent = 0m; + + /// + /// Сумма доступных скидок. null / отрицательные значения считаются 0. + /// + 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); + } + + /// + /// итоговая = base * (1 - discount/100), минимум 0. Без округления. + /// + 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); } } } diff --git a/src/ElectronicPharmacy/Classes/PriceSummaryItem.cs b/src/ElectronicPharmacy/Classes/PriceSummaryItem.cs index c24ae7c..a48328f 100644 --- a/src/ElectronicPharmacy/Classes/PriceSummaryItem.cs +++ b/src/ElectronicPharmacy/Classes/PriceSummaryItem.cs @@ -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; } diff --git a/src/ElectronicPharmacy/ElectronicPharmacy.csproj b/src/ElectronicPharmacy/ElectronicPharmacy.csproj index e670c27..3ddadbc 100644 --- a/src/ElectronicPharmacy/ElectronicPharmacy.csproj +++ b/src/ElectronicPharmacy/ElectronicPharmacy.csproj @@ -114,6 +114,8 @@ + + diff --git a/src/ElectronicPharmacy/Form1.UiShell.cs b/src/ElectronicPharmacy/Form1.UiShell.cs index 4d06500..fb054bb 100644 --- a/src/ElectronicPharmacy/Form1.UiShell.cs +++ b/src/ElectronicPharmacy/Form1.UiShell.cs @@ -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}"; } diff --git a/src/ElectronicPharmacy/Form1.cs b/src/ElectronicPharmacy/Form1.cs index 9c49f08..1ce1d3e 100644 --- a/src/ElectronicPharmacy/Form1.cs +++ b/src/ElectronicPharmacy/Form1.cs @@ -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) diff --git a/src/ElectronicPharmacy/Forms/FCheckDatabaseIntegrary.cs b/src/ElectronicPharmacy/Forms/FCheckDatabaseIntegrary.cs index cff0da6..6c96beb 100644 --- a/src/ElectronicPharmacy/Forms/FCheckDatabaseIntegrary.cs +++ b/src/ElectronicPharmacy/Forms/FCheckDatabaseIntegrary.cs @@ -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 diff --git a/src/ElectronicPharmacy/Forms/FConsignees.cs b/src/ElectronicPharmacy/Forms/FConsignees.cs index 8142cf1..274064e 100644 --- a/src/ElectronicPharmacy/Forms/FConsignees.cs +++ b/src/ElectronicPharmacy/Forms/FConsignees.cs @@ -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; diff --git a/src/ElectronicPharmacy/HelpForms/HF_DownloadDataFromServer.cs b/src/ElectronicPharmacy/HelpForms/HF_DownloadDataFromServer.cs index a9f8ed2..7b33c64 100644 --- a/src/ElectronicPharmacy/HelpForms/HF_DownloadDataFromServer.cs +++ b/src/ElectronicPharmacy/HelpForms/HF_DownloadDataFromServer.cs @@ -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(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 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 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 } } } + + diff --git a/src/ElectronicPharmacy/HelpForms/HF_LocationId.Designer.cs b/src/ElectronicPharmacy/HelpForms/HF_LocationId.Designer.cs index 5215bbc..5c1554e 100644 --- a/src/ElectronicPharmacy/HelpForms/HF_LocationId.Designer.cs +++ b/src/ElectronicPharmacy/HelpForms/HF_LocationId.Designer.cs @@ -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; diff --git a/src/ElectronicPharmacy/HelpForms/HF_LocationId.cs b/src/ElectronicPharmacy/HelpForms/HF_LocationId.cs index 1ba4401..487c584 100644 --- a/src/ElectronicPharmacy/HelpForms/HF_LocationId.cs +++ b/src/ElectronicPharmacy/HelpForms/HF_LocationId.cs @@ -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 } } + /// + /// Показывает Location ID с контекстом: выбранная аптека (имя/адрес). + /// + 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) diff --git a/src/ElectronicPharmacy/HelpForms/HF_Registration.cs b/src/ElectronicPharmacy/HelpForms/HF_Registration.cs index 3185a7f..4b11d9d 100644 --- a/src/ElectronicPharmacy/HelpForms/HF_Registration.cs +++ b/src/ElectronicPharmacy/HelpForms/HF_Registration.cs @@ -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; diff --git a/src/ElectronicPharmacy/Properties/Settings.Designer.cs b/src/ElectronicPharmacy/Properties/Settings.Designer.cs index a1aa19b..bd579f2 100644 --- a/src/ElectronicPharmacy/Properties/Settings.Designer.cs +++ b/src/ElectronicPharmacy/Properties/Settings.Designer.cs @@ -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; + } + } + } } diff --git a/src/ElectronicPharmacy/Properties/Settings.settings b/src/ElectronicPharmacy/Properties/Settings.settings index b597362..83ffe4b 100644 --- a/src/ElectronicPharmacy/Properties/Settings.settings +++ b/src/ElectronicPharmacy/Properties/Settings.settings @@ -32,5 +32,11 @@ + + 01.01.0001 00:00:00 + + + 01.01.0001 00:00:00 + \ No newline at end of file diff --git a/src/ElectronicPharmacy/UserControls/UCOrderItems.cs b/src/ElectronicPharmacy/UserControls/UCOrderItems.cs index 4ec0789..ea444d1 100644 --- a/src/ElectronicPharmacy/UserControls/UCOrderItems.cs +++ b/src/ElectronicPharmacy/UserControls/UCOrderItems.cs @@ -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 старыеЗапросыСозданияЗаказа diff --git a/src/ElectronicPharmacy/UserControls/UCPriceList.cs b/src/ElectronicPharmacy/UserControls/UCPriceList.cs index 8bedd83..f0d61ae 100644 --- a/src/ElectronicPharmacy/UserControls/UCPriceList.cs +++ b/src/ElectronicPharmacy/UserControls/UCPriceList.cs @@ -1072,10 +1072,12 @@ namespace Электронная_Фармация.UserControls List SuppliersFromOrder = new List(); + 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}'"; //} } } + +