feat: накидка по строке в таблице «Цена продажи» + минимальный CSV

Sale-таблица показывает LOT/Описание/Кол-во/Накидка,%/Цена вместо Estimate/
Склад/Конкуренты/Ручная цена; итоговая цена строки = база (raw estimate или
доля от общей «Ручная цена») × (Аплифт к estimate + своя Накидка%). Экспорт
CSV этой таблицы теперь выводит только LOT;Описание;Кол-во;Цена. Buy-таблица
и массовый экспорт по проекту не затронуты — новые поля запроса опциональны
и включаются только кнопкой «Экспорт CSV» у Sale-таблицы.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Mikhail Chusavitin
2026-09-15 16:39:54 +03:00
co-authored by Claude Sonnet 5
parent fb412a4227
commit 7263dd4572
5 changed files with 385 additions and 100 deletions
+31 -1
View File
@@ -62,13 +62,43 @@ Rules:
## Pricing tab layout
The Pricing tab (Ценообразование) has two tables: Buy (Цена покупки) and Sale (Цена продажи).
Their column sets differ (see below); the shared per-LOT row expansion/grouping rules apply to both.
Column order (both tables):
Buy table column order:
```
PN вендора | Описание | LOT | Кол-во | Estimate | Склад | Конкуренты | Ручная цена
```
Sale table column order:
```
PN вендора | Описание | LOT | Кол-во | Накидка, % | Цена
```
Sale per-row pricing (`_saleRowPrice`/`recomputeSalePrices` in `index.html`,
`applySaleRowPricing` in `internal/services/export.go`):
- each row's base is its raw Estimate unit price × qty, unless the table-wide "Ручная
цена" input is set — then the base is that row's proportional share of the manual
total (same distribution as before this existed, last row absorbs the rounding
remainder), overwriting the row's own base rather than filling a separate column;
- `Цена = base × (Аплифт к estimate + row's own Накидка% / 100)` — Накидка is a
per-row percentage, additive to the table-wide uplift, entered directly in the
row's own input; unset rows default to 0% (no change to older configs' totals);
Склад/Конкуренты are not shown or exported for the Sale table;
- per-row Накидка is keyed by `"<vendorPN>::<lot>"` (uppercased, "NONE" when there's
no vendor PN) — plain `lot_name` isn't a safe key since the same LOT can appear
under more than one vendor-PN group (see row expansion rules below) — and persisted
as an additive key, `Notes.pricing_ui.sale_row_markups`, alongside the existing
`sale_uplift`/`sale_custom_price` (see
[decisions/2026-09-15-sale-row-markup-and-minimal-csv.md](decisions/2026-09-15-sale-row-markup-and-minimal-csv.md));
- the Sale table's own "Экспорт CSV" button additionally sends
`minimal_sale_columns: true` + `sale_row_markups`, which collapses
`POST /api/configs/:uuid/export/pricing` output to exactly `LOT;Описание;Кол-во;Цена`
(`ProjectPricingExportOptions.MinimalSaleColumns` in `internal/services/export.go`).
This is scoped to that one request: Buy export and the project-level bulk export
never set it and are unaffected.
Per-LOT row expansion rules:
- each `lot_mappings` entry in a BOM row becomes its own table row with its own quantity and prices;
- `baseLot` (resolved LOT without an explicit mapping) is treated as the first sub-row with `quantity_per_pn` from `_getRowLotQtyPerPN`;
@@ -0,0 +1,58 @@
# Decision: per-row "Накидка" on the Sale table + minimal Sale CSV columns
**Date:** 2026-09-15
**Status:** active
## Context
The Sale (Цена продажи) table only had one table-wide "Аплифт к estimate" multiplier
and one table-wide "Ручная цена" override. There was no way to push margin up on
individual rows (e.g. scarce GPUs) without changing the uplift for every row, and the
table showed Estimate/Склад/Конкуренты/Ручная-цена columns that a sales rep handing a
quote to a customer never needed — the actual deliverable is just LOT, Описание, Кол-во,
and a final price.
## Decision
- The Sale table gains a per-row "Накидка, %" input. Each row's final "Цена" is
`base × (Аплифт к estimate + Накидка% / 100)`, where `base` is the row's raw Estimate
unit price × qty, or — when the table-wide "Ручная цена" is set — that row's
proportional share of the manual total (same last-row-absorbs-remainder distribution
the table-wide manual price already used, now overwriting the row's base instead of
filling a separate column).
- The Sale table's visible/exported columns collapse to
`PN вендора | Описание | LOT | Кол-во | Накидка, % | Цена` on screen, and exactly
`LOT;Описание;Кол-во;Цена` in its own CSV export. Estimate, Склад, Конкуренты, and
the old Ручная-цена column are no longer shown or exported for this table. The Buy
table (Цена покупки) is untouched.
- Per-row markup is keyed by `"<vendorPN>::<lot>"` (uppercased, `NONE` when there is no
vendor PN) — not by `lot_name` alone, since the same LOT can appear under more than
one vendor-PN BOM group (see "Per-LOT row expansion rules" in `02-architecture.md`).
- Persistence is additive: `Configuration.Notes.pricing_ui` gains a new
`sale_row_markups` map alongside the existing `sale_uplift`/`sale_custom_price`, the
same JSON-blob-in-`Notes` mechanism established by
[2026-08-12-project-export-per-config-sale-uplift.md](2026-08-12-project-export-per-config-sale-uplift.md).
Zero/unset rows are omitted from the map entirely.
- CSV export: `POST /api/configs/:uuid/export/pricing` gained `minimal_sale_columns`
(bool) and `sale_row_markups` (map) request fields, read into
`ProjectPricingExportOptions.MinimalSaleColumns`/`SaleRowMarkups`. These are wired
**only** in `ExportConfigPricingCSV`, gated on `req.MinimalSaleColumns` being true —
the Sale table's own "Экспорт CSV" button is the only caller that sets it. Buy export
(basis=fob) and the project-level bulk export (`ExportProjectPricingCSV`) never send
it and keep their full existing column sets/output unchanged.
- `ProjectPricingExportOptions.ManualPrice`, previously parsed from the request but
never actually read into the options struct (a pre-existing dead field), is now wired
— but only inside the same `MinimalSaleColumns` gate, so this bug fix does not change
Buy-table CSV output for anyone already relying on the old (broken) behavior.
## Consequences
- Old clients / already-saved configurations are unaffected: `sale_row_markups` absent
from `Notes` means every row's markup defaults to 0%, reproducing the exact totals
the table produced before this change (uplift-only). Old clients that don't know the
new `Notes` key simply ignore it, exactly like `sale_uplift` is ignored by even older
code.
- If a future caller needs the full-column DDP CSV format with per-row markup applied,
it must do so explicitly — `MinimalSaleColumns` intentionally couples "reduced
columns" and "per-row markup applied" as one request-level switch rather than two
independent options, since the only current caller needs both together.