feat: PN code в импорте BOM, объединённый список LOT (world ∪ estimate)
Колонка «PN code» в таблице импорта BOM: HPE-спецификации хранят партномер в двух столбцах, канонический вид — P52534-B21#B19. Склейка попадает в vendor_partnumber и сравнивается с книгой партномеров как есть; при загрузке разбирается обратно на две колонки. Схему БД менять не пришлось. Список допустимых LOT собирается из world ∪ estimate (componentUniverse), а не только из estimate. Прежняя область видимости молча теряла lot_mappings: резолвер сопоставлял PN по книге, которая про прайслисты не знает, а фронт отбрасывал LOT, которого нет в estimate — при этом в корзину он всё равно попадал. World-only позиции отдают price_quality 0. Кнопка «Пересопоставить» — перерешать BOM по актуальной книге, так как при открытии конфигурации сопоставления остаются замороженными. Итоги на вкладке «Ценообразование»: звёздочки убраны (переносились на новую строку), все три суммы красные, при наведении — попап с долей цен из прайслиста WORLD. Колонка «PN вендора» больше не переносится. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
0fa7b0b1b6
commit
905f9a4952
@@ -261,7 +261,7 @@ PK: lot_name
|
||||
| price | decimal(12,2) NOT NULL | |
|
||||
| price_quality | tinyint unsigned, nullable | added by migration 033. Set by the external pricelist-building tool, 0-9, based on quote recency/count per its own per-lot pricing-period settings — QF only reads and displays it, never computes it. |
|
||||
|
||||
`price_quality` is synced through `LocalPricelistItem` (pricelist detail page) and, separately, through `LocalComponent`/`services.ComponentView` (`/api/components`, used by the configurator's search dropdown and item table) — both read paths go through `pricelistItemRow`/`toLocalComponent()` in `internal/localdb/components.go`, still scoped to the currently selected pricelist. The color scale (red 0 → yellow 5 → green 9, gradient) lives in **one shared JS module**, `web/static/price-quality.js` (loaded by `base.html` for every page) — `priceQualityColor`/`qualityDotHtml`/`qualityBadgeHtml`/`qualityRowStyle`. Do not reimplement the color scale locally in a template; add a new helper to that module instead. Used in: pricelist detail "Качество" column, configurator search dropdown (colored dot), configurator table (leftmost quality-dot column), pricing tab (row background tint).
|
||||
`price_quality` is synced through `LocalPricelistItem` (pricelist detail page) and, separately, through `LocalComponent`/`services.ComponentView` (`/api/components`, used by the configurator's search dropdown and item table) — both read paths go through `pricelistItemRow`/`toLocalComponent()` in `internal/localdb/components.go`. The `/api/components` path reads the component universe (`world` ∪ `estimate`, see [decisions/2026-07-24-component-universe-world-union.md](decisions/2026-07-24-component-universe-world-union.md)), where a world-only LOT is forced to `price_quality = 0` — the only place QF writes this field rather than reading it. The color scale (red 0 → yellow 5 → green 9, gradient) lives in **one shared JS module**, `web/static/price-quality.js` (loaded by `base.html` for every page) — `priceQualityColor`/`qualityDotHtml`/`qualityBadgeHtml`/`qualityRowStyle`. Do not reimplement the color scale locally in a template; add a new helper to that module instead. Used in: pricelist detail "Качество" column, configurator search dropdown (colored dot), configurator table (leftmost quality-dot column), pricing tab (row background tint).
|
||||
|
||||
The real table also has `price_method`, `price_period_days`, `price_coefficient`, `manual_price`, `meta_prices` and `lead_time_weeks` columns, owned and written by the external pricing engine that maintains `qt_pricelist_items` — **keep them in the table for backward compatibility with that system; QF must never drop, rename, or write to them.** QF itself does not model, sync, or display any of them (tried once, removed as unused/dead in the client). Do not re-add them to `models.PricelistItem`/`LocalPricelistItem` without a concrete UI need.
|
||||
|
||||
|
||||
@@ -25,6 +25,33 @@ Rules:
|
||||
- QuoteForge does not use legacy BOM tables;
|
||||
- apply flow rebuilds cart rows from `lot_mappings[]`.
|
||||
|
||||
## Split partnumbers (HPE option codes)
|
||||
|
||||
HPE specs carry the partnumber in two columns: base PN plus an option code
|
||||
(`P52534-B21` + `B19`). The canonical partnumber is the concatenation
|
||||
`P52534-B21#B19`, and it is what gets compared against the partnumber book.
|
||||
|
||||
Rules:
|
||||
- the import grid has a `PN code` column type in addition to `P/N`; it is optional
|
||||
and at most one column may carry it;
|
||||
- an empty code cell keeps the bare PN — no trailing `#`;
|
||||
- the composed value is stored in `vendor_partnumber`. There is **no separate
|
||||
`pn_code` field**: no DDL and no new JSON key, and the stored string is exactly
|
||||
the string the resolver looks up;
|
||||
- the column layout of the import grid is not persisted. On load the grid is
|
||||
re-derived from `vendor_spec`, and the `PN code` column reappears only when at
|
||||
least one stored partnumber contains `#`, split on the first `#`.
|
||||
|
||||
## Re-resolving an existing BOM
|
||||
|
||||
Opening a configuration does **not** re-resolve its BOM: `loadVendorSpec` renders the
|
||||
`lot_mappings[]` frozen at save time, so a configuration keeps the mapping it was
|
||||
saved with even after the partnumber book changes.
|
||||
|
||||
Book entries added later are picked up only through the `Пересопоставить` button,
|
||||
which calls the resolve endpoint for every row. Book matches win over the stored
|
||||
mapping (resolver step 1 beats step 2); rows the book does not know keep theirs.
|
||||
|
||||
## Partnumber books
|
||||
|
||||
Partnumber books are pull-only snapshots from PriceForge.
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
# Decision: the component universe is `world` ∪ `estimate`, not `estimate` alone
|
||||
|
||||
**Date:** 2026-07-24
|
||||
**Status:** active
|
||||
|
||||
## Context
|
||||
|
||||
Every component read path in `internal/localdb/components.go` was scoped to the latest
|
||||
active `estimate` pricelist. That pricelist is therefore what defined the set of LOTs the
|
||||
configurator considers to exist, and the frontend used it as its validation list
|
||||
(`_bomLotValid` in `index.html`, fed by `/api/components`).
|
||||
|
||||
That silently broke BOM mappings. The resolver matches a vendor partnumber against the
|
||||
partnumber book, which knows nothing about pricelists, so it legitimately returns LOTs the
|
||||
estimate pricelist does not carry. The frontend then displayed such a row as mapped —
|
||||
`_getRowBaseLot` does not validate `resolved_lot` — and `applyBOMToEstimate` pushed the LOT
|
||||
into the cart, but `_getRowCanonicalLotMappings` filtered it out through `_bomLotValid`, so
|
||||
the mapping vanished on save. Result: the LOT sat in the cart while its BOM row showed
|
||||
"н/д", and the pricing tab listed it at the bottom as an orphan with an empty vendor PN.
|
||||
|
||||
Prices for such LOTs were already available — see
|
||||
[2026-07-10-world-pricelist-fallback.md](2026-07-10-world-pricelist-fallback.md) — only
|
||||
membership was missing.
|
||||
|
||||
## Decision
|
||||
|
||||
`componentUniverse()` in `internal/localdb/components.go` is the single source of the LOT
|
||||
set, and every component read path goes through it: `ListComponents`,
|
||||
`SearchLocalComponents`, `SearchLocalComponentsByCategory`, `GetLocalComponent`,
|
||||
`GetLocalComponentCategories`, `GetLocalComponentCategoriesByLotNames`, `CountComponents`.
|
||||
|
||||
- The universe is the latest active `world` pricelist — the widest list available — plus
|
||||
the latest active `estimate` pricelist as a safety net.
|
||||
- Deduplication is on `UPPER(lot_name)`. The estimate row wins a collision, so its
|
||||
category, description and `price_quality` stay authoritative and nothing changes for
|
||||
LOTs that were already visible.
|
||||
- A world-only row reports `price_quality = 0`, rendering at the red end of the shared
|
||||
scale in `web/static/price-quality.js`. This is the **one** place QF sets that field
|
||||
instead of only reading it (contrast `bible-local/03-database.md`), and it deliberately
|
||||
overrides whatever the external pricing tool wrote on the world row.
|
||||
- The helper always returns a derived table (`(?) AS c`), so callers apply their own
|
||||
`Select`/`Count`/`Where` without knowing which pricelists exist.
|
||||
- Either pricelist may be absent; only both missing is an error.
|
||||
|
||||
On the frontend, `_bomLotPersistable` additionally lets a LOT with
|
||||
`resolution_source === 'book'` be persisted into `lot_mappings[]` even when it is in
|
||||
neither pricelist. Hand-typed LOTs stay validated against the universe.
|
||||
|
||||
## Consequences
|
||||
|
||||
- The configurator's pickers, category list and search now surface world-only LOTs. They
|
||||
are selectable and appear in the reddest quality colour.
|
||||
- `CountComponents` — the "is there data" check — counts the union.
|
||||
- A LOT present in neither pricelist (nothing to price it with) still resolves from the
|
||||
book and now survives a save, instead of being dropped without a message.
|
||||
- Covered by `internal/localdb/component_universe_test.go`: union and dedup, estimate
|
||||
winning a collision, forced quality 0, and each single-source / empty case.
|
||||
Reference in New Issue
Block a user