# Pricelists ## Types PriceForge supports three pricelist types (`source` field): | Type | Data source | Status | |------|-------------|--------| | `estimate` | `qt_lot_metadata` — snapshot of current prices | Active | | `warehouse` | `stock_log` — stock import data | Active | | `competitor` | Excel exports from competitor B2B portals | Active | --- ## Estimate **Source**: snapshot of current component prices from `qt_lot_metadata`. **Filter**: - Include only rows with `current_price > 0`. - Exclude hidden rows (`is_hidden = 1`). **Stores**: - `price_period_days` — price calculation window - `price_coefficient` — markup coefficient - `manual_price` — manually set price - `meta_prices` — historical prices **Purpose**: primary pricelist for calculations and estimates. **Categories**: loaded from `lot.lot_category` for each component. --- ## Warehouse **Source**: `stock_log` (stock import records). **Rules (CRITICAL)**: 1. **Only mapped partnumbers** — only positions with a record in `qt_partnumber_book_items` are included. Unmapped partnumbers must not appear in any form. 2. **No pricing settings** — do not load from `lot_metadata`: no `price_period_days`, `price_coefficient`, `manual_price`, `meta_prices`. 3. **Price method**: always `weighted_avg` (quantity-weighted average). 4. Field `price_method` always contains `"weighted_avg"`. 5. These are raw stock prices without additional processing. **Categories**: loaded from `lot.lot_category`. **Implementation**: `internal/warehouse/snapshot.go` --- ## Competitor **Source**: Excel exports from competitor B2B portals, uploaded manually via `/admin/competitors`. **Rules (CRITICAL)**: 1. **Only mapped partnumbers** — only positions with a record in `qt_partnumber_book_items` are included. Unmapped p/ns are counted and written to `qt_vendor_partnumber_seen` (source_type = `"competitor:"`). 2. **No pricing settings** — do not load from `lot_metadata`: no `price_period_days`, `price_coefficient`, `manual_price`, `meta_prices`. 3. **Price method**: always `weighted_median` (quantity-weighted median across all matched p/ns for a lot). 4. Field `price_method` always contains `"weighted_median"`, `price_period_days` always `0`. 5. **Uplift**: effective price = `price / price_uplift`. `price_uplift` is a divisor > 1 stored per competitor; default `1.3`. Replaces the former `expected_discount_pct` field. 6. **Deduplication**: if price and qty for a p/n are unchanged since last import — skip insert into `partnumber_log_competitors`. **Key tables**: - `qt_competitors` — competitor profiles (name, code, `price_uplift DECIMAL(8,4) DEFAULT 1.3`, column mapping JSON, currency) - `partnumber_log_competitors` — historical quote log (competitor_id, partnumber, price USD, price local, qty, date) **Implementation**: `internal/services/competitor_import.go`, `internal/repository/competitor.go` --- ## Data Model ```go type Pricelist struct { ID uint `gorm:"primaryKey"` Version string Source string // "estimate" | "warehouse" | "competitor" // ... } type PricelistItem struct { ID uint `gorm:"primaryKey"` PricelistID uint LotName string `gorm:"size:255"` Price float64 `gorm:"type:decimal(12,2)"` LotCategory *string `gorm:"column:lot_category;size:50" json:"category,omitempty"` // Virtual fields (via JOIN or programmatically) LotDescription string `gorm:"-:migration" json:"lot_description,omitempty"` AvailableQty *float64 `gorm:"-" json:"available_qty,omitempty"` Partnumbers []string `gorm:"-" json:"partnumbers,omitempty"` // Enriched per-source virtual fields (warehouse / competitor only) CompetitorNames []string `gorm:"-" json:"competitor_names,omitempty"` PriceSpreadPct *float64 `gorm:"-" json:"price_spread_pct,omitempty"` PartnumberQtys map[string]float64 `gorm:"-" json:"partnumber_qtys,omitempty"` } ``` > `gorm:"-:migration"` — no DB column created, but mapped on SELECT. > `gorm:"-"` — fully ignored in all DB operations. --- ## Categories (lot_category) - **Source**: `lot.lot_category` column in table `lot`. - **NOT** from `qt_lot_metadata`. - **NOT** derived from LOT name. - Persisted into `qt_pricelist_items.lot_category` when pricelist is created. - JSON field name: `"category"`. - JOIN with `lot` is only needed for `lot_description`; category is already in `qt_pricelist_items`. - Default value when category is missing: `PART_`. --- ## Pricelist Creation (background task) Creation runs via Task Manager. Handler returns `task_id`; frontend polls. ``` POST /api/pricelists/create → { "task_id": "uuid" } → polling GET /api/tasks/:id → { "status": "completed", "result": { "pricelist_id": 42 } } ``` Task type: `TaskTypePricelistCreate`. **Implementation**: - Service: `internal/services/pricelist/service.go` - Warehouse calc: `internal/warehouse/snapshot.go` - Handler: `internal/handlers/pricelist.go` --- ## Pricelist Deletion Guard `CountUsage(id)` checks `qt_configurations` for references across **all three** pricelist columns: `pricelist_id`, `warehouse_pricelist_id`, `competitor_pricelist_id`. A pricelist is only deletable when all three counts are zero. --- ## Competitor Pricelist - **Source**: `competitor` - **Rebuilt via**: `POST /api/competitors/pricelist` (task type: `competitor_import`) - **Logic**: Latest quote per `(competitor_id, partnumber)` across ALL active competitors → apply each competitor's `price_uplift` (divide price by uplift) → `weighted_median` per lot → insert into `qt_pricelist_items` - **price_method**: `weighted_median`, **price_period_days**: `0` - Quotes stored in `partnumber_log_competitors`; unmapped p/ns recorded in `qt_vendor_partnumber_seen` with `source_type = 'competitor:'` --- ## Pricelist Detail Page — Display Layouts Page: `/pricelists/:id` — `web/templates/pricelist_detail.html` + `web/static/js/pricelist_detail.js` Two display layouts exist based on `source`: ### Estimate layout (`source = "estimate"`) Columns: Артикул | Категория | Описание | Цена, $ | Настройки - Description truncated to 60 chars. - "Настройки" column shows per-row price adjustment controls. - No partnumber or supplier column. ### Stock layout (`source = "warehouse"` or `"competitor"`) Columns: Артикул | Категория | Описание | Partnumbers | Поставщик | Цена, $ - "Настройки" column is hidden. - "Доступно" (available qty) column is hidden. - Артикул column has word-break enabled (long lot names wrap). - Description truncated to 30 chars. **Partnumbers column**: shows only PNs with `partnumber_qtys[pn] > 0`, formatted as `PN (qty шт.)`. Up to 4 shown, then `+N ещё`. **Поставщик column**: - Warehouse: grey text "склад". - Competitor: blue badge(s) with competitor name(s) from `competitor_names`. **Spread badge** (competitor only): `±N%` in amber next to the price, computed from `price_spread_pct`. ### Per-source enrichment (`internal/repository/pricelist.go`) - `enrichWarehouseItems`: calls `warehouse.LoadLotMetrics` → fills `Partnumbers` and `PartnumberQtys`. - `enrichCompetitorItems`: loads latest competitor quotes (qty > 0 only) → resolves to lots via `qt_partnumber_book_items` → fills `CompetitorNames`, `Partnumbers`, `PartnumberQtys`, `PriceSpreadPct`. `LoadLotMetrics` signature: `(db, lotNames, latestOnly) → (qtyByLot, partnumbersByLot, pnQtysByLot, error)`.