Go refactoring: - Split handlers/pricing.go (2446→291 lines) into 5 focused files - Split services/stock_import.go (1334→~400 lines) into stock_mappings.go + stock_parse.go - Split services/sync/service.go (1290→~250 lines) into 3 files JS extraction: - Move all inline <script> blocks to web/static/js/ (6 files) - Templates reduced: admin_pricing 2873→521, lot 1531→304, vendor_mappings 1063→169, etc. Competitor pricelists (migrations 033-039): - qt_competitors + partnumber_log_competitors tables - Excel import with column mapping, dedup, bulk insert - p/n→lot resolution via weighted_median, discount applied - Unmapped p/ns written to qt_vendor_partnumber_seen - Quote counts (unique/total) shown on /admin/competitors - price_method="weighted_median", price_period_days=0 stored explicitly Fix price_method/price_period_days for warehouse items: - warehouse: weighted_avg, period=0 - competitor: weighted_median, period=0 - Removes misleading DB defaults (was: median/90) Update bible: architecture.md, pricelist.md, history.md Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
132 lines
4.5 KiB
Markdown
132 lines
4.5 KiB
Markdown
# 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:<code>"`).
|
||
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. **Discount**: effective price = `weighted_median × (1 − expected_discount_pct / 100)`.
|
||
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, 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
|
||
|
||
```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"`
|
||
}
|
||
```
|
||
|
||
> `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`
|