Persist per-pharmacy markup percent and simplify consignee picker UI.

Store client markup on each Consignee so it survives restarts and price refresh; show only name and address in the selection dialog with a wider window.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Magomed 2026-07-30 14:53:28 +03:00
parent 3d253a2f54
commit d68155b8ef
6 changed files with 213 additions and 35 deletions

View File

@ -215,6 +215,113 @@ namespace Электронная_Фармация.Classes
return result; return result;
} }
/// <summary>
/// Процент наценки, который клиент задал вручную для аптеки.
/// null = ещё не сохраняли, брать fallback.
/// </summary>
public static decimal? GetClientMarkupPercent(string consigneeName)
{
if (string.IsNullOrWhiteSpace(consigneeName))
{
return null;
}
try
{
using (var connection = new SQLiteConnection(AppConfig.SqliteConnectionString))
{
connection.Open();
using (var command = new SQLiteCommand(
@"SELECT ClientMarkupPercent
FROM Consignees
WHERE ConsigneesName = @name
LIMIT 1",
connection))
{
command.Parameters.AddWithValue("@name", consigneeName.Trim());
var result = command.ExecuteScalar();
if (result == null || result == DBNull.Value)
{
return null;
}
if (result is decimal d)
{
return d;
}
if (decimal.TryParse(
Convert.ToString(result, System.Globalization.CultureInfo.InvariantCulture),
System.Globalization.NumberStyles.Any,
System.Globalization.CultureInfo.InvariantCulture,
out var parsed))
{
return parsed;
}
return null;
}
}
}
catch
{
return null;
}
}
/// <summary>
/// Сохраняет процент наценки клиента для конкретной аптеки.
/// Не сбрасывается при обновлении прайса.
/// </summary>
public static void SetClientMarkupPercent(string consigneeName, decimal markupPercent)
{
if (string.IsNullOrWhiteSpace(consigneeName))
{
return;
}
using (var connection = new SQLiteConnection(AppConfig.SqliteConnectionString))
{
connection.Open();
using (var command = new SQLiteCommand(
@"UPDATE Consignees
SET ClientMarkupPercent = @pct
WHERE ConsigneesName = @name",
connection))
{
command.Parameters.AddWithValue("@pct", markupPercent);
command.Parameters.AddWithValue("@name", consigneeName.Trim());
command.ExecuteNonQuery();
}
}
}
/// <summary>
/// Если у аптеки ещё нет сохранённого процента — записать стартовое значение с сервера.
/// Уже введённые клиентом значения не трогаем.
/// </summary>
public static void SeedClientMarkupPercentIfEmpty(decimal? serverMarkupPercent)
{
if (!serverMarkupPercent.HasValue)
{
return;
}
using (var connection = new SQLiteConnection(AppConfig.SqliteConnectionString))
{
connection.Open();
using (var command = new SQLiteCommand(
@"UPDATE Consignees
SET ClientMarkupPercent = @pct
WHERE ClientMarkupPercent IS NULL",
connection))
{
command.Parameters.AddWithValue("@pct", serverMarkupPercent.Value);
command.ExecuteNonQuery();
}
}
}
/// <summary> /// <summary>
/// Синхронизирует поля заказа в `PriceList` (Zakaz/SummaZakaza) с корзиной выбранной аптеки. /// Синхронизирует поля заказа в `PriceList` (Zakaz/SummaZakaza) с корзиной выбранной аптеки.
/// Данные корзины хранятся в TempOrderItems с фильтром по ConsigneeName, а PriceList обновляется /// Данные корзины хранятся в TempOrderItems с фильтром по ConsigneeName, а PriceList обновляется

View File

@ -593,6 +593,7 @@ FROM [TempOrderItems];";
TryAddColumn(conForCheckTables, "PriceList", "TradeName", "nvarchar(256)"); TryAddColumn(conForCheckTables, "PriceList", "TradeName", "nvarchar(256)");
TryAddColumn(conForCheckTables, "PriceList", "Dosage", "nvarchar(128)"); TryAddColumn(conForCheckTables, "PriceList", "Dosage", "nvarchar(128)");
TryAddColumn(conForCheckTables, "PriceList", "MarkupPercent", "real"); TryAddColumn(conForCheckTables, "PriceList", "MarkupPercent", "real");
TryAddColumn(conForCheckTables, "Consignees", "ClientMarkupPercent", "real");
ConsigneeHelper.MigrateGlobalLocationIdIfNeeded(conForCheckTables); ConsigneeHelper.MigrateGlobalLocationIdIfNeeded(conForCheckTables);
} }
catch (Exception ex) catch (Exception ex)

View File

@ -45,14 +45,14 @@
this.dgvConsignees.BackgroundColor = System.Drawing.SystemColors.ButtonFace; this.dgvConsignees.BackgroundColor = System.Drawing.SystemColors.ButtonFace;
this.dgvConsignees.BorderStyle = System.Windows.Forms.BorderStyle.None; this.dgvConsignees.BorderStyle = System.Windows.Forms.BorderStyle.None;
this.dgvConsignees.ColumnHeadersHeightSizeMode = System.Windows.Forms.DataGridViewColumnHeadersHeightSizeMode.AutoSize; this.dgvConsignees.ColumnHeadersHeightSizeMode = System.Windows.Forms.DataGridViewColumnHeadersHeightSizeMode.AutoSize;
this.dgvConsignees.Location = new System.Drawing.Point(12, 56); this.dgvConsignees.Location = new System.Drawing.Point(16, 56);
this.dgvConsignees.MultiSelect = false; this.dgvConsignees.MultiSelect = false;
this.dgvConsignees.Name = "dgvConsignees"; this.dgvConsignees.Name = "dgvConsignees";
this.dgvConsignees.ReadOnly = true; this.dgvConsignees.ReadOnly = true;
this.dgvConsignees.RowHeadersVisible = false; this.dgvConsignees.RowHeadersVisible = false;
this.dgvConsignees.RowHeadersWidth = 51; this.dgvConsignees.RowHeadersWidth = 51;
this.dgvConsignees.RowTemplate.Height = 24; this.dgvConsignees.RowTemplate.Height = 24;
this.dgvConsignees.Size = new System.Drawing.Size(409, 295); this.dgvConsignees.Size = new System.Drawing.Size(640, 295);
this.dgvConsignees.TabIndex = 0; this.dgvConsignees.TabIndex = 0;
// //
// label1 // label1
@ -69,7 +69,7 @@
// //
this.btnConfirmConsignee.FlatStyle = System.Windows.Forms.FlatStyle.Popup; this.btnConfirmConsignee.FlatStyle = System.Windows.Forms.FlatStyle.Popup;
this.btnConfirmConsignee.Font = new System.Drawing.Font("Segoe UI Semibold", 10.8F, System.Drawing.FontStyle.Bold, System.Drawing.GraphicsUnit.Point, ((byte)(204))); this.btnConfirmConsignee.Font = new System.Drawing.Font("Segoe UI Semibold", 10.8F, System.Drawing.FontStyle.Bold, System.Drawing.GraphicsUnit.Point, ((byte)(204)));
this.btnConfirmConsignee.Location = new System.Drawing.Point(336, 361); this.btnConfirmConsignee.Location = new System.Drawing.Point(567, 361);
this.btnConfirmConsignee.Name = "btnConfirmConsignee"; this.btnConfirmConsignee.Name = "btnConfirmConsignee";
this.btnConfirmConsignee.Size = new System.Drawing.Size(89, 43); this.btnConfirmConsignee.Size = new System.Drawing.Size(89, 43);
this.btnConfirmConsignee.TabIndex = 2; this.btnConfirmConsignee.TabIndex = 2;
@ -81,7 +81,7 @@
// //
this.btnCloseThis.FlatStyle = System.Windows.Forms.FlatStyle.Popup; this.btnCloseThis.FlatStyle = System.Windows.Forms.FlatStyle.Popup;
this.btnCloseThis.Font = new System.Drawing.Font("Segoe UI Semibold", 10.8F, System.Drawing.FontStyle.Bold, System.Drawing.GraphicsUnit.Point, ((byte)(204))); this.btnCloseThis.Font = new System.Drawing.Font("Segoe UI Semibold", 10.8F, System.Drawing.FontStyle.Bold, System.Drawing.GraphicsUnit.Point, ((byte)(204)));
this.btnCloseThis.Location = new System.Drawing.Point(174, 361); this.btnCloseThis.Location = new System.Drawing.Point(420, 361);
this.btnCloseThis.Name = "btnCloseThis"; this.btnCloseThis.Name = "btnCloseThis";
this.btnCloseThis.Size = new System.Drawing.Size(125, 43); this.btnCloseThis.Size = new System.Drawing.Size(125, 43);
this.btnCloseThis.TabIndex = 3; this.btnCloseThis.TabIndex = 3;
@ -93,7 +93,7 @@
// //
this.AutoScaleDimensions = new System.Drawing.SizeF(8F, 16F); this.AutoScaleDimensions = new System.Drawing.SizeF(8F, 16F);
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
this.ClientSize = new System.Drawing.Size(434, 412); this.ClientSize = new System.Drawing.Size(672, 412);
this.Controls.Add(this.btnCloseThis); this.Controls.Add(this.btnCloseThis);
this.Controls.Add(this.btnConfirmConsignee); this.Controls.Add(this.btnConfirmConsignee);
this.Controls.Add(this.label1); this.Controls.Add(this.label1);

View File

@ -28,7 +28,6 @@ namespace Электронная_Фармация.HelpForms
string qShowMeConsigneesList = @" string qShowMeConsigneesList = @"
select ConsigneesName as [Грузополучатель], select ConsigneesName as [Грузополучатель],
ifnull(LocationId, '') as [Location ID],
ConsigneesAddress as [Адрес] ConsigneesAddress as [Адрес]
from Consignees from Consignees
order by ConsigneesUseAsDefault desc, ConsigneesName"; order by ConsigneesUseAsDefault desc, ConsigneesName";
@ -38,6 +37,16 @@ order by ConsigneesUseAsDefault desc, ConsigneesName";
DataTable tableConsignees = new DataTable(); DataTable tableConsignees = new DataTable();
daConsigneesList.Fill(tableConsignees); daConsigneesList.Fill(tableConsignees);
dgvConsignees.DataSource = tableConsignees; dgvConsignees.DataSource = tableConsignees;
if (dgvConsignees.Columns.Contains("Грузополучатель"))
{
dgvConsignees.Columns["Грузополучатель"].FillWeight = 45;
}
if (dgvConsignees.Columns.Contains("Адрес"))
{
dgvConsignees.Columns["Адрес"].FillWeight = 55;
}
} }
catch (Exception ex) catch (Exception ex)
{ {

View File

@ -368,15 +368,14 @@ namespace Электронная_Фармация.HelpForms
private static void ApplyClientMarkupSettings(PriceSummaryResponse response) private static void ApplyClientMarkupSettings(PriceSummaryResponse response)
{ {
// Серверный % используем только как стартовое значение для аптек,
// у которых клиент ещё ничего не сохранил. Уже введённые проценты не трогаем.
if (response?.MarkupPercent == null) if (response?.MarkupPercent == null)
{ {
return; return;
} }
var markup = response.MarkupPercent.Value; ConsigneeHelper.SeedClientMarkupPercentIfEmpty(response.MarkupPercent);
Settings.Default.ClientMarkupPercent = markup;
Settings.Default.IntDefaultPercentMarkup = (int)Math.Round(markup, MidpointRounding.AwayFromZero);
Settings.Default.Save();
} }
private void ReportTransferProgress(HttpTransferProgress transfer) private void ReportTransferProgress(HttpTransferProgress transfer)

View File

@ -35,6 +35,7 @@ namespace Электронная_Фармация.UserControls
DataTable tablePriceList; DataTable tablePriceList;
string _WorkWithConsignee; string _WorkWithConsignee;
decimal _percentageAsDefault; decimal _percentageAsDefault;
bool _suppressMarkupPersist;
string _selectedSupplierFilter = string.Empty; string _selectedSupplierFilter = string.Empty;
bool _suppressSupplierFilterEvents; bool _suppressSupplierFilterEvents;
bool _suppressPriceListSelectionEvents; bool _suppressPriceListSelectionEvents;
@ -1006,34 +1007,59 @@ namespace Электронная_Фармация.UserControls
void loadInfoAboutDefaultMarkup() void loadInfoAboutDefaultMarkup()
{ {
decimal percentageMarkupDefault = Properties.Settings.Default.ClientMarkupPercent; decimal percentageMarkupDefault = 0m;
var fromConsignee = ConsigneeHelper.GetClientMarkupPercent(_WorkWithConsignee);
if (fromConsignee.HasValue)
{
percentageMarkupDefault = fromConsignee.Value;
}
else
{
percentageMarkupDefault = Properties.Settings.Default.ClientMarkupPercent;
if (percentageMarkupDefault <= 0) if (percentageMarkupDefault <= 0)
{ {
percentageMarkupDefault = Properties.Settings.Default.IntDefaultPercentMarkup; percentageMarkupDefault = Properties.Settings.Default.IntDefaultPercentMarkup;
} }
if (percentageMarkupDefault <= 0) if (percentageMarkupDefault < 0)
{ {
percentageMarkupDefault = MarkupHelper.FallbackMarkupPercent; percentageMarkupDefault = MarkupHelper.FallbackMarkupPercent;
} }
// Первый запуск для этой аптеки — сразу закрепим текущее значение за аптекой.
if (!string.IsNullOrWhiteSpace(_WorkWithConsignee))
{
try
{
ConsigneeHelper.SetClientMarkupPercent(_WorkWithConsignee, percentageMarkupDefault);
}
catch
{
// колонка может ещё не быть — при следующем старте миграция добавит
}
}
}
_percentageAsDefault = percentageMarkupDefault; _percentageAsDefault = percentageMarkupDefault;
_suppressMarkupPersist = true;
try
{
numericPercent.DecimalPlaces = 2; numericPercent.DecimalPlaces = 2;
numericPercent.Increment = 0.1m; numericPercent.Increment = 0.1m;
numericPercent.Value = Math.Max(numericPercent.Minimum, Math.Min(numericPercent.Maximum, percentageMarkupDefault)); numericPercent.Value = Math.Max(
numericPercent.Minimum,
Math.Min(numericPercent.Maximum, percentageMarkupDefault));
}
finally
{
_suppressMarkupPersist = false;
}
} }
private decimal GetMarkupPercentForRow(DataGridViewRow row) 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; return _percentageAsDefault;
} }
@ -2028,7 +2054,18 @@ and SumOrderedItems = '{SumOrder}'";
} }
var markupPercentage = GetMarkupPercentForRow(currentRow); var markupPercentage = GetMarkupPercentForRow(currentRow);
numericPercent.Value = Math.Max(numericPercent.Minimum, Math.Min(numericPercent.Maximum, markupPercentage)); _suppressMarkupPersist = true;
try
{
numericPercent.Value = Math.Max(
numericPercent.Minimum,
Math.Min(numericPercent.Maximum, markupPercentage));
}
finally
{
_suppressMarkupPersist = false;
}
lblPriceForGood.Text = $"= {priceForGood:0.##} руб"; lblPriceForGood.Text = $"= {priceForGood:0.##} руб";
} }
catch catch
@ -2405,6 +2442,11 @@ and SumOrderedItems = '{SumOrder}'";
private void numericPercent_ValueChanged(object sender, EventArgs e) private void numericPercent_ValueChanged(object sender, EventArgs e)
{ {
if (!_suppressMarkupPersist)
{
PersistClientMarkupPercent();
}
if (numericPercent.Value != _percentageAsDefault) if (numericPercent.Value != _percentageAsDefault)
{ {
checkUsePercentageAsDefault.Checked = false; checkUsePercentageAsDefault.Checked = false;
@ -2413,6 +2455,30 @@ and SumOrderedItems = '{SumOrder}'";
ShowMePriceWithMarkupPercentForSelectedGood(); ShowMePriceWithMarkupPercentForSelectedGood();
} }
private void PersistClientMarkupPercent()
{
var value = numericPercent.Value;
_percentageAsDefault = value;
if (!string.IsNullOrWhiteSpace(_WorkWithConsignee))
{
try
{
ConsigneeHelper.SetClientMarkupPercent(_WorkWithConsignee, value);
}
catch
{
// ignore transient DB errors; value stays in UI
}
}
// Глобальный fallback на случай новой аптеки без своего значения.
Properties.Settings.Default.ClientMarkupPercent = value;
Properties.Settings.Default.IntDefaultPercentMarkup =
Convert.ToInt32(Math.Round(value, MidpointRounding.AwayFromZero));
Properties.Settings.Default.Save();
}
private void UCPriceList_Paint(object sender, PaintEventArgs e) private void UCPriceList_Paint(object sender, PaintEventArgs e)
{ {
if (comboSupplierFilter.DroppedDown) if (comboSupplierFilter.DroppedDown)
@ -2435,13 +2501,9 @@ and SumOrderedItems = '{SumOrder}'";
private void checkUsePercentageAsDefault_CheckedChanged(object sender, EventArgs e) private void checkUsePercentageAsDefault_CheckedChanged(object sender, EventArgs e)
{ {
if (_percentageAsDefault != numericPercent.Value && checkUsePercentageAsDefault.Checked == true) if (checkUsePercentageAsDefault.Checked)
{ {
var newDefaultPercent = numericPercent.Value; PersistClientMarkupPercent();
Properties.Settings.Default.ClientMarkupPercent = newDefaultPercent;
Properties.Settings.Default.IntDefaultPercentMarkup = Convert.ToInt32(Math.Round(newDefaultPercent, MidpointRounding.AwayFromZero));
Properties.Settings.Default.Save();
_percentageAsDefault = newDefaultPercent;
} }
} }