5.3 KiB
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 windowprice_coefficient— markup coefficientmanual_price— manually set pricemeta_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):
- Only mapped partnumbers — only positions with a record in
qt_partnumber_book_itemsare included. Unmapped partnumbers must not appear in any form. - No pricing settings — do not load from
lot_metadata: noprice_period_days,price_coefficient,manual_price,meta_prices. - Price method: always
weighted_avg(quantity-weighted average). - Field
price_methodalways contains"weighted_avg". - 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):
- Only mapped partnumbers — only positions with a record in
qt_partnumber_book_itemsare included. Unmapped p/ns are counted and written toqt_vendor_partnumber_seen(source_type ="competitor:<code>"). - No pricing settings — do not load from
lot_metadata: noprice_period_days,price_coefficient,manual_price,meta_prices. - Price method: always
weighted_median(quantity-weighted median across all matched p/ns for a lot). - Field
price_methodalways contains"weighted_median",price_period_daysalways0. - Discount: effective price =
weighted_median × (1 − expected_discount_pct / 100). - 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, discount %, 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
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"`
}
gorm:"-:migration"— no DB column created, but mapped on SELECT.gorm:"-"— fully ignored in all DB operations.
Categories (lot_category)
- Source:
lot.lot_categorycolumn in tablelot. - NOT from
qt_lot_metadata. - NOT derived from LOT name.
- Persisted into
qt_pricelist_items.lot_categorywhen pricelist is created. - JSON field name:
"category". - JOIN with
lotis only needed forlot_description; category is already inqt_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'sexpected_discount_pct→weighted_medianper lot → insert intoqt_pricelist_items - price_method:
weighted_median, price_period_days:0 - Quotes stored in
partnumber_log_competitors; unmapped p/ns recorded inqt_vendor_partnumber_seenwithsource_type = 'competitor:<code>'