diff --git a/docs/API.md b/docs/API.md index ba27529..63c5771 100644 --- a/docs/API.md +++ b/docs/API.md @@ -103,6 +103,7 @@ GET /api/supplier-prices/summary?supplier_id={uuid} ```json { + "markup_percent": 15.5, "summary": [ { "supplier_price_id": "uuid", @@ -114,6 +115,7 @@ GET /api/supplier-prices/summary?supplier_id={uuid} "cure_form": "таб.", "barcode": "...", "price": 123.45, + "markup_percent": 15.5, "quantity": 10, "region_id": "...", "region_name": "...", diff --git a/src/ElectronicPharmacy/App.config b/src/ElectronicPharmacy/App.config index 761ce75..262f9c5 100644 --- a/src/ElectronicPharmacy/App.config +++ b/src/ElectronicPharmacy/App.config @@ -27,6 +27,9 @@ 0 + + 0 + diff --git a/src/ElectronicPharmacy/Classes/MarkupHelper.cs b/src/ElectronicPharmacy/Classes/MarkupHelper.cs new file mode 100644 index 0000000..73e5c68 --- /dev/null +++ b/src/ElectronicPharmacy/Classes/MarkupHelper.cs @@ -0,0 +1,65 @@ +using System; + +namespace Электронная_Фармация.Classes +{ + public static class MarkupHelper + { + public const decimal FallbackMarkupPercent = 30m; + + public static decimal ResolveMarkupPercent(decimal? itemMarkup, decimal? clientMarkup, decimal fallbackMarkup) + { + if (itemMarkup.HasValue && itemMarkup.Value >= 0) + { + return itemMarkup.Value; + } + + if (clientMarkup.HasValue && clientMarkup.Value >= 0) + { + return clientMarkup.Value; + } + + return fallbackMarkup > 0 ? fallbackMarkup : FallbackMarkupPercent; + } + + public static decimal ApplyMarkup(decimal basePrice, decimal markupPercent) + { + return basePrice + (basePrice * markupPercent / 100m); + } + + public static bool TryParseMarkup(object value, out decimal markupPercent) + { + markupPercent = 0m; + if (value == null || value == DBNull.Value) + { + return false; + } + + if (value is decimal decimalValue) + { + markupPercent = decimalValue; + return true; + } + + if (value is double doubleValue) + { + markupPercent = (decimal)doubleValue; + return true; + } + + if (value is float floatValue) + { + markupPercent = (decimal)floatValue; + return true; + } + + if (value is int intValue) + { + markupPercent = intValue; + return true; + } + + var text = value.ToString()?.Trim().Replace(',', '.'); + 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 cf713c0..c24ae7c 100644 --- a/src/ElectronicPharmacy/Classes/PriceSummaryItem.cs +++ b/src/ElectronicPharmacy/Classes/PriceSummaryItem.cs @@ -56,11 +56,17 @@ namespace Электронная_Фармация.Classes public string Barcode { get; set; } /// - /// Финальная цена с учетом наценки (рассчитана на сервере) + /// Базовая цена поставщика (без розничной наценки клиента). /// [JsonPropertyName("price")] public decimal? Price { get; set; } + /// + /// Процент наценки для позиции. Если не задан — используется markup_percent из ответа прайса. + /// + [JsonPropertyName("markup_percent")] + public decimal? MarkupPercent { get; set; } + /// /// Количество товара на складе /// diff --git a/src/ElectronicPharmacy/Classes/PriceSummaryResponse.cs b/src/ElectronicPharmacy/Classes/PriceSummaryResponse.cs index f1e2c59..1d83522 100644 --- a/src/ElectronicPharmacy/Classes/PriceSummaryResponse.cs +++ b/src/ElectronicPharmacy/Classes/PriceSummaryResponse.cs @@ -9,6 +9,12 @@ namespace Электронная_Фармация.Classes { public class PriceSummaryResponse { + /// + /// Процент наценки для текущего клиента (покупателя), приходит вместе с прайсом. + /// + [JsonPropertyName("markup_percent")] + public decimal? MarkupPercent { get; set; } + [JsonPropertyName("summary")] public List Summary { get; set; } } diff --git a/src/ElectronicPharmacy/ElectronicPharmacy.csproj b/src/ElectronicPharmacy/ElectronicPharmacy.csproj index fd4daf9..f961d0c 100644 --- a/src/ElectronicPharmacy/ElectronicPharmacy.csproj +++ b/src/ElectronicPharmacy/ElectronicPharmacy.csproj @@ -111,6 +111,7 @@ + diff --git a/src/ElectronicPharmacy/Forms/FCheckDatabaseIntegrary.cs b/src/ElectronicPharmacy/Forms/FCheckDatabaseIntegrary.cs index 4001551..2ff4df3 100644 --- a/src/ElectronicPharmacy/Forms/FCheckDatabaseIntegrary.cs +++ b/src/ElectronicPharmacy/Forms/FCheckDatabaseIntegrary.cs @@ -402,6 +402,7 @@ create table if not exists [OrderItems] ( TryAddColumn(conForCheckTables, "Invoice", "IsRequestedInvoice", "int(1)"); TryAddColumn(conForCheckTables, "PriceList", "TradeName", "nvarchar(256)"); TryAddColumn(conForCheckTables, "PriceList", "Dosage", "nvarchar(128)"); + TryAddColumn(conForCheckTables, "PriceList", "MarkupPercent", "real"); } catch (Exception ex) { diff --git a/src/ElectronicPharmacy/HelpForms/HF_DownloadDataFromServer.Designer.cs b/src/ElectronicPharmacy/HelpForms/HF_DownloadDataFromServer.Designer.cs index e9c9758..fdcc440 100644 --- a/src/ElectronicPharmacy/HelpForms/HF_DownloadDataFromServer.Designer.cs +++ b/src/ElectronicPharmacy/HelpForms/HF_DownloadDataFromServer.Designer.cs @@ -51,6 +51,8 @@ // // progressDownload // + this.progressDownload.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left) + | System.Windows.Forms.AnchorStyles.Right))); this.progressDownload.Location = new System.Drawing.Point(25, 52); this.progressDownload.Name = "progressDownload"; this.progressDownload.Size = new System.Drawing.Size(630, 22); @@ -60,23 +62,34 @@ // // lblProgress // - this.lblProgress.AutoEllipsis = true; - this.lblProgress.Font = new System.Drawing.Font("Segoe UI", 10F); + this.lblProgress.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left) + | System.Windows.Forms.AnchorStyles.Right))); + this.lblProgress.Font = new System.Drawing.Font("Segoe UI", 9.75F); this.lblProgress.Location = new System.Drawing.Point(25, 82); this.lblProgress.Name = "lblProgress"; - this.lblProgress.Size = new System.Drawing.Size(630, 24); + this.lblProgress.Size = new System.Drawing.Size(630, 40); this.lblProgress.TabIndex = 7; this.lblProgress.Text = "Подготовка..."; + this.lblProgress.UseMnemonic = false; // // rtxtDebug // - this.rtxtDebug.BackColor = System.Drawing.SystemColors.Control; - this.rtxtDebug.BorderStyle = System.Windows.Forms.BorderStyle.None; - this.rtxtDebug.Location = new System.Drawing.Point(25, 116); + this.rtxtDebug.Anchor = ((System.Windows.Forms.AnchorStyles)((((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom) + | System.Windows.Forms.AnchorStyles.Left) + | System.Windows.Forms.AnchorStyles.Right))); + this.rtxtDebug.BackColor = System.Drawing.SystemColors.Window; + this.rtxtDebug.BorderStyle = System.Windows.Forms.BorderStyle.FixedSingle; + this.rtxtDebug.DetectUrls = false; + this.rtxtDebug.Font = new System.Drawing.Font("Consolas", 9F); + this.rtxtDebug.HideSelection = false; + this.rtxtDebug.Location = new System.Drawing.Point(25, 128); this.rtxtDebug.Name = "rtxtDebug"; - this.rtxtDebug.Size = new System.Drawing.Size(630, 215); + this.rtxtDebug.ReadOnly = true; + this.rtxtDebug.ScrollBars = System.Windows.Forms.RichTextBoxScrollBars.Vertical; + this.rtxtDebug.Size = new System.Drawing.Size(630, 243); this.rtxtDebug.TabIndex = 0; this.rtxtDebug.Text = ""; + this.rtxtDebug.WordWrap = true; // // dataGridView1 // @@ -95,8 +108,9 @@ // // btnCloseThis // + this.btnCloseThis.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right))); this.btnCloseThis.Font = new System.Drawing.Font("Segoe UI", 11.25F); - this.btnCloseThis.Location = new System.Drawing.Point(555, 337); + this.btnCloseThis.Location = new System.Drawing.Point(555, 381); this.btnCloseThis.Margin = new System.Windows.Forms.Padding(4); this.btnCloseThis.Name = "btnCloseThis"; this.btnCloseThis.Size = new System.Drawing.Size(100, 41); @@ -110,15 +124,16 @@ this.AutoScaleDimensions = new System.Drawing.SizeF(8F, 16F); this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; this.BackColor = System.Drawing.SystemColors.Control; - this.ClientSize = new System.Drawing.Size(684, 396); + this.ClientSize = new System.Drawing.Size(684, 440); this.Controls.Add(this.lblProgress); this.Controls.Add(this.progressDownload); this.Controls.Add(this.lblTitle); this.Controls.Add(this.btnCloseThis); this.Controls.Add(this.dataGridView1); this.Controls.Add(this.rtxtDebug); - this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedSingle; - this.MaximizeBox = false; + this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.Sizable; + this.MaximizeBox = true; + this.MinimumSize = new System.Drawing.Size(640, 420); this.MinimizeBox = false; this.Name = "HF_DownloadDataFromServer"; this.StartPosition = System.Windows.Forms.FormStartPosition.CenterParent; diff --git a/src/ElectronicPharmacy/HelpForms/HF_DownloadDataFromServer.cs b/src/ElectronicPharmacy/HelpForms/HF_DownloadDataFromServer.cs index 3d8b23b..521aa05 100644 --- a/src/ElectronicPharmacy/HelpForms/HF_DownloadDataFromServer.cs +++ b/src/ElectronicPharmacy/HelpForms/HF_DownloadDataFromServer.cs @@ -2,7 +2,9 @@ using System.Collections.Generic; using System.Data; using System.Data.SQLite; +using System.Drawing; using System.Net.Http; +using System.Text; using System.Threading.Tasks; using System.Windows.Forms; using Электронная_Фармация.Classes; @@ -30,6 +32,7 @@ namespace Электронная_Фармация.HelpForms private async void HF_DownloadDataFromServer_Load(object sender, EventArgs e) { UiThemeHelper.ApplyToControlTree(this); + ConfigureLogView(); btnCloseThis.Enabled = false; dataGridView1.Visible = false; rtxtDebug.Visible = true; @@ -51,6 +54,12 @@ namespace Электронная_Фармация.HelpForms AppendStatus("Загружаю сводный прайс..."); var allSuppliersSummary = await GetFullPriceSummaryPagedAsync(client); int totalItems = allSuppliersSummary.Summary?.Count ?? 0; + ApplyClientMarkupSettings(allSuppliersSummary); + if (allSuppliersSummary.MarkupPercent.HasValue) + { + AppendStatus($"Наценка клиента: {allSuppliersSummary.MarkupPercent.Value:0.##}%"); + } + AppendStatus($"Получено {totalItems} позиций"); SetOverallProgress(DownloadPhaseEnd, $"Получено {totalItems} / {totalItems} позиций (100%)"); @@ -182,6 +191,11 @@ namespace Электронная_Фармация.HelpForms } int got = chunk?.Summary?.Count ?? 0; + if (page == 0 && chunk?.MarkupPercent.HasValue == true) + { + combined.MarkupPercent = chunk.MarkupPercent; + } + if (got > 0) { combined.Summary.AddRange(chunk.Summary); @@ -197,6 +211,19 @@ namespace Электронная_Фармация.HelpForms return combined; } + private static void ApplyClientMarkupSettings(PriceSummaryResponse response) + { + if (response?.MarkupPercent == null) + { + return; + } + + var markup = response.MarkupPercent.Value; + Settings.Default.ClientMarkupPercent = markup; + Settings.Default.IntDefaultPercentMarkup = (int)Math.Round(markup, MidpointRounding.AwayFromZero); + Settings.Default.Save(); + } + private void ReportTransferProgress(HttpTransferProgress transfer) { if (transfer.TotalBytes.HasValue && transfer.TotalBytes.Value > 0) @@ -253,12 +280,19 @@ namespace Электронная_Фармация.HelpForms DataType = typeof(string), AllowDBNull = true }); + tablePrice.Columns.Add(new DataColumn + { + ColumnName = "MarkupPercent", + DataType = typeof(decimal), + AllowDBNull = true + }); if (allSuppliersSummary?.Summary == null) { return tablePrice; } + var clientMarkup = allSuppliersSummary.MarkupPercent; int total = allSuppliersSummary.Summary.Count; int current = 0; foreach (var item in allSuppliersSummary.Summary) @@ -278,6 +312,8 @@ 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; tablePrice.Rows.Add(row); current++; @@ -308,7 +344,8 @@ namespace Электронная_Фармация.HelpForms [SummaZakaza], [supplier_price_id], [TradeName], -[Dosage] +[Dosage], +[MarkupPercent] ) VALUES ( @@ -325,7 +362,8 @@ VALUES @SummaZakaza, @supplier_price_id, @TradeName, -@Dosage +@Dosage, +@MarkupPercent )"; int total = tablePrice.Rows.Count; @@ -361,6 +399,7 @@ VALUES cmd.Parameters.AddWithValue("@supplier_price_id", row["supplier_price_id"]); cmd.Parameters.AddWithValue("@TradeName", row.Table.Columns.Contains("TradeName") ? row["TradeName"] : DBNull.Value); cmd.Parameters.AddWithValue("@Dosage", row.Table.Columns.Contains("Dosage") ? row["Dosage"] : DBNull.Value); + cmd.Parameters.AddWithValue("@MarkupPercent", row.Table.Columns.Contains("MarkupPercent") ? row["MarkupPercent"] : DBNull.Value); cmd.ExecuteNonQuery(); } @@ -433,11 +472,74 @@ VALUES progressDownload.Minimum = 0; progressDownload.Maximum = 100; progressDownload.Value = Math.Max(0, Math.Min(100, percent)); - lblProgress.Text = $"{Math.Max(0, Math.Min(100, percent))}% — {text}"; + lblProgress.Text = FormatProgressText(percent, text); lblProgress.Refresh(); progressDownload.Refresh(); } + private void ConfigureLogView() + { + rtxtDebug.ReadOnly = true; + rtxtDebug.WordWrap = true; + rtxtDebug.ScrollBars = RichTextBoxScrollBars.Vertical; + rtxtDebug.HideSelection = false; + rtxtDebug.DetectUrls = false; + rtxtDebug.BorderStyle = BorderStyle.FixedSingle; + if (rtxtDebug.Font.Name != "Consolas") + { + rtxtDebug.Font = new Font("Consolas", 9f); + } + + lblProgress.AutoEllipsis = false; + lblProgress.UseMnemonic = false; + } + + private static string FormatProgressText(int percent, string text) + { + return WrapLongText($"{Math.Max(0, Math.Min(100, percent))}% — {text}", 78, 2); + } + + private static string WrapLongText(string text, int maxLineLength, int maxLines) + { + if (string.IsNullOrEmpty(text) || text.Length <= maxLineLength) + { + return text ?? string.Empty; + } + + var lines = new List(); + var remaining = text.Trim(); + while (!string.IsNullOrEmpty(remaining) && lines.Count < maxLines) + { + if (remaining.Length <= maxLineLength) + { + lines.Add(remaining); + break; + } + + var breakIndex = remaining.LastIndexOf(' ', Math.Min(maxLineLength, remaining.Length - 1)); + if (breakIndex <= 0) + { + breakIndex = maxLineLength; + } + + lines.Add(remaining.Substring(0, breakIndex).TrimEnd()); + remaining = remaining.Substring(breakIndex).TrimStart(); + } + + if (!string.IsNullOrEmpty(remaining) && lines.Count >= maxLines) + { + var lastLine = lines[lines.Count - 1]; + if (lastLine.Length > maxLineLength - 3) + { + lastLine = lastLine.Substring(0, Math.Max(0, maxLineLength - 3)).TrimEnd(); + } + + lines[lines.Count - 1] = lastLine + "..."; + } + + return string.Join(Environment.NewLine, lines); + } + private void AppendStatus(string message) { if (InvokeRequired) @@ -446,8 +548,10 @@ VALUES return; } - rtxtDebug.AppendText(message + Environment.NewLine); + var wrapped = WrapLongText(message ?? string.Empty, 96, int.MaxValue); + rtxtDebug.AppendText(wrapped + Environment.NewLine); rtxtDebug.SelectionStart = rtxtDebug.TextLength; + rtxtDebug.SelectionLength = 0; rtxtDebug.ScrollToCaret(); rtxtDebug.Refresh(); AppDebugLog.Info("Download", message); diff --git a/src/ElectronicPharmacy/Properties/Settings.Designer.cs b/src/ElectronicPharmacy/Properties/Settings.Designer.cs index 61c34aa..a1aa19b 100644 --- a/src/ElectronicPharmacy/Properties/Settings.Designer.cs +++ b/src/ElectronicPharmacy/Properties/Settings.Designer.cs @@ -34,6 +34,18 @@ namespace Электронная_Фармация.Properties { this["IntDefaultPercentMarkup"] = value; } } + + [global::System.Configuration.UserScopedSettingAttribute()] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Configuration.DefaultSettingValueAttribute("0")] + public decimal ClientMarkupPercent { + get { + return ((decimal)(this["ClientMarkupPercent"])); + } + set { + this["ClientMarkupPercent"] = value; + } + } [global::System.Configuration.UserScopedSettingAttribute()] [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] diff --git a/src/ElectronicPharmacy/Properties/Settings.settings b/src/ElectronicPharmacy/Properties/Settings.settings index b4863ca..b597362 100644 --- a/src/ElectronicPharmacy/Properties/Settings.settings +++ b/src/ElectronicPharmacy/Properties/Settings.settings @@ -5,6 +5,9 @@ 0 + + 0 + diff --git a/src/ElectronicPharmacy/UserControls/UCPriceList.cs b/src/ElectronicPharmacy/UserControls/UCPriceList.cs index 5da0f28..f833035 100644 --- a/src/ElectronicPharmacy/UserControls/UCPriceList.cs +++ b/src/ElectronicPharmacy/UserControls/UCPriceList.cs @@ -399,6 +399,11 @@ namespace Электронная_Фармация.UserControls tablePrice.Columns["Dosage"].ColumnName = "Дозировка"; } + if (tablePrice.Columns.Contains("MarkupPercent")) + { + tablePrice.Columns["MarkupPercent"].ColumnName = "Наценка %"; + } + EnsureDerivedBrowseColumns(tablePrice); _fullPriceTable = tablePrice; _selectedBaseName = string.Empty; @@ -797,6 +802,7 @@ namespace Электронная_Фармация.UserControls HideOptionalColumn("Торговое название"); HideOptionalColumn("Дозировка"); + HideOptionalColumn("Наценка %"); HideOptionalColumn("Базовое наименование"); HideOptionalColumn("МГ"); @@ -927,16 +933,37 @@ namespace Электронная_Фармация.UserControls void loadInfoAboutDefaultMarkup() { - var percentageMarkupDefault = Properties.Settings.Default.IntDefaultPercentMarkup; + decimal percentageMarkupDefault = Properties.Settings.Default.ClientMarkupPercent; if (percentageMarkupDefault <= 0) { - percentageMarkupDefault = 30; + percentageMarkupDefault = Properties.Settings.Default.IntDefaultPercentMarkup; + } + + if (percentageMarkupDefault <= 0) + { + percentageMarkupDefault = MarkupHelper.FallbackMarkupPercent; } _percentageAsDefault = percentageMarkupDefault; + numericPercent.DecimalPlaces = 2; + numericPercent.Increment = 0.1m; numericPercent.Value = Math.Max(numericPercent.Minimum, Math.Min(numericPercent.Maximum, percentageMarkupDefault)); } + private decimal GetMarkupPercentForRow(DataGridViewRow row) + { + if (row?.DataGridView?.Columns.Contains("Наценка %") == true) + { + var cellValue = row.Cells["Наценка %"]?.Value; + if (MarkupHelper.TryParseMarkup(cellValue, out var itemMarkup) && itemMarkup >= 0) + { + return itemMarkup; + } + } + + return _percentageAsDefault; + } + public void loadSupplierTabs(string SupplierName) { string tabName; @@ -1911,9 +1938,10 @@ and SumOrderedItems = '{SumOrder}'"; return; } - var markupPercentage = (double)numericPercent.Value; - var priceWithPercentage = priceForGood + (priceForGood / 100 * markupPercentage); - lblPriceForGood.Text = $"= {priceWithPercentage} руб"; + 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.##} руб"; } catch { @@ -2313,13 +2341,14 @@ and SumOrderedItems = '{SumOrder}'"; private void checkUsePercentageAsDefault_CheckedChanged(object sender, EventArgs e) { - int newDefaultPercent; if (_percentageAsDefault != numericPercent.Value && checkUsePercentageAsDefault.Checked == true) { - newDefaultPercent = Convert.ToInt32(numericPercent.Value); - Properties.Settings.Default.IntDefaultPercentMarkup = Convert.ToInt32(newDefaultPercent); + var newDefaultPercent = numericPercent.Value; + Properties.Settings.Default.ClientMarkupPercent = newDefaultPercent; + Properties.Settings.Default.IntDefaultPercentMarkup = Convert.ToInt32(Math.Round(newDefaultPercent, MidpointRounding.AwayFromZero)); Properties.Settings.Default.Save(); - } + _percentageAsDefault = newDefaultPercent; + } } private void timerForResetSearch_Tick(object sender, EventArgs e)