Files
QuoteForge/bible-local/decisions/2026-09-15-sale-row-markup-and-minimal-csv.md
T
Mikhail ChusavitinandClaude Sonnet 5 7263dd4572 feat: накидка по строке в таблице «Цена продажи» + минимальный CSV
Sale-таблица показывает LOT/Описание/Кол-во/Накидка,%/Цена вместо Estimate/
Склад/Конкуренты/Ручная цена; итоговая цена строки = база (raw estimate или
доля от общей «Ручная цена») × (Аплифт к estimate + своя Накидка%). Экспорт
CSV этой таблицы теперь выводит только LOT;Описание;Кол-во;Цена. Buy-таблица
и массовый экспорт по проекту не затронуты — новые поля запроса опциональны
и включаются только кнопкой «Экспорт CSV» у Sale-таблицы.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-15 16:39:54 +03:00

3.7 KiB
Raw Blame History

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. 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.