Files
PriceForge/bible-local/pricelist.md
Mikhail Chusavitin f48615e8a9 Modularize Go files, extract JS to static, implement competitor pricelists
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>
2026-03-13 07:44:10 +03:00

132 lines
4.5 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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`