Add partnumber book snapshots for QuoteForge integration
- Migrations 026-028: qt_partnumber_books + qt_partnumber_book_items tables; is_primary_pn on lot_partnumbers; version VARCHAR(30); description VARCHAR(10000) on items (required by QuoteForge sync) - Service: CreateSnapshot expands bundles, filters empty lot_name and ignored PNs, copies description, activates new book atomically, applies GFS retention (7d/5w/12m/10y) with explicit item deletion - Task type TaskTypePartnumberBookCreate; handlers ListPartnumberBooks and CreatePartnumberBook; routes GET/POST /api/admin/pricing/partnumber-books - UI: snapshot list + "Создать снапшот сопоставлений" button with progress polling on /vendor-mappings page - Bible: history, api, background-tasks, vendor-mapping updated Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -63,6 +63,8 @@
|
||||
| GET/POST | `/api/admin/pricing/stock-mappings` | Stock partnumber mappings |
|
||||
| GET/POST | `/api/admin/pricing/vendor-mappings` | Vendor partnumber mappings |
|
||||
| GET | `/api/admin/pricing/alerts` | Alerts list |
|
||||
| GET | `/api/admin/pricing/partnumber-books` | List all partnumber book snapshots with item counts |
|
||||
| POST | `/api/admin/pricing/partnumber-books` | Create partnumber book snapshot (returns task_id) |
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -33,6 +33,7 @@
|
||||
| `TaskTypeRecalculate` | Recalculate all component prices |
|
||||
| `TaskTypeStockImport` | Import stock file (.mxl/.xlsx) |
|
||||
| `TaskTypePricelistCreate` | Create pricelist (estimate/warehouse/competitor) |
|
||||
| `TaskTypePartnumberBookCreate` | Create partnumber book snapshot for QuoteForge |
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -5,6 +5,56 @@
|
||||
|
||||
---
|
||||
|
||||
## 2026-02-21: Partnumber Book Snapshots for QuoteForge
|
||||
|
||||
### Decision
|
||||
|
||||
Implemented versioned snapshots of the `lot_partnumbers` → LOT mapping in `qt_partnumber_books` / `qt_partnumber_book_items`. PriceForge writes; QuoteForge reads (SELECT only).
|
||||
|
||||
### What changed
|
||||
|
||||
- Migration `026`: added `is_primary_pn TINYINT(1) DEFAULT 1` to `lot_partnumbers`; created `qt_partnumber_books` and `qt_partnumber_book_items` tables (version `VARCHAR(20)`, later corrected).
|
||||
- Migration `027`: corrected `version VARCHAR(20) → VARCHAR(30)` — `PNBOOK-YYYY-MM-DD-NNN` is 21 chars and overflowed the original column.
|
||||
- Migration `028`: added `description VARCHAR(10000) NULL` to `qt_partnumber_book_items` — required by QuoteForge sync (`SELECT partnumber, lot_name, is_primary_pn, description`).
|
||||
- Models `PartnumberBook`, `PartnumberBookItem` (with `Description *string`) added to `internal/models/lot.go`; `IsPrimaryPN bool` added to `LotPartnumber`.
|
||||
- Service `internal/services/partnumber_book.go`:
|
||||
- `CreateSnapshot`: expands bundles (QuoteForge is bundle-unaware), copies `description` from `lot_partnumbers` to every expanded row, generates version `PNBOOK-YYYY-MM-DD-NNN`, deactivates previous books and activates new one atomically, then runs GFS retention cleanup.
|
||||
- `expandMappings`: filters out rows where `lot_name` is empty/whitespace; filters out partnumbers marked `is_ignored = true` in `qt_vendor_partnumber_seen`. Only valid PN→LOT pairs enter the snapshot.
|
||||
- `applyRetention`: deletes items explicitly (`DELETE … WHERE book_id IN (…)`) before deleting books — does not rely on FK CASCADE which GORM does not trigger on batch deletes.
|
||||
- `ListBooks`: returns all snapshots ordered newest-first with item counts.
|
||||
- GFS retention policy: 7 daily · 5 weekly · 12 monthly · 10 yearly; applied automatically after each snapshot; active book is never deleted.
|
||||
- Task type `TaskTypePartnumberBookCreate` added to `internal/tasks/task.go`.
|
||||
- Handlers `ListPartnumberBooks` and `CreatePartnumberBook` added to `internal/handlers/pricing.go`; `PartnumberBookService` injected via constructor.
|
||||
- Routes `GET /api/admin/pricing/partnumber-books` and `POST /api/admin/pricing/partnumber-books` registered in `cmd/pfs/main.go`.
|
||||
- UI: "Снимки сопоставлений (Partnumber Books)" section with snapshot table, progress bar, and "Создать снапшот сопоставлений" button added to `web/templates/vendor_mappings.html`.
|
||||
|
||||
### Rationale
|
||||
|
||||
QuoteForge needs a stable, versioned copy of the PN→LOT mapping to resolve BOM line items without live dependency on PriceForge. Snapshots decouple the two systems.
|
||||
|
||||
### Constraints
|
||||
|
||||
- Bundles MUST be expanded: QuoteForge does not know about `qt_lot_bundles`.
|
||||
- Snapshot rows with empty `lot_name` or `is_ignored = true` partnumbers MUST be excluded.
|
||||
- `description` in book items comes from `lot_partnumbers.description`; for expanded bundle rows the description of the parent partnumber mapping is used.
|
||||
- `is_primary_pn` is copied from `lot_partnumbers` into each book item; drives qty logic in QuoteForge.
|
||||
- New snapshot is immediately `is_active=1`; all previous books are deactivated in the same transaction.
|
||||
- Version format: `PNBOOK-YYYY-MM-DD-NNN` (`VARCHAR(30)`), sequential within the same day.
|
||||
- Item deletion during retention MUST be explicit (items first, then books) — FK CASCADE is unreliable with GORM batch deletes.
|
||||
- QuoteForge has `SELECT` permission only on `qt_partnumber_books` and `qt_partnumber_book_items`.
|
||||
|
||||
### Files
|
||||
|
||||
- Migrations: `026_add_partnumber_books.sql`, `027_fix_partnumber_books_version_length.sql`, `028_add_description_to_partnumber_book_items.sql`
|
||||
- Models: `internal/models/lot.go`
|
||||
- Service: `internal/services/partnumber_book.go`
|
||||
- Handler: `internal/handlers/pricing.go`
|
||||
- Tasks: `internal/tasks/task.go`
|
||||
- Routes: `cmd/pfs/main.go`
|
||||
- UI: `web/templates/vendor_mappings.html`
|
||||
|
||||
---
|
||||
|
||||
## 2026-02-20: Pricelist Formation Hardening (Estimate/Warehouse/Meta)
|
||||
|
||||
### Decision
|
||||
|
||||
@@ -36,7 +36,7 @@ qt_lot_bundle_items: bundle_lot_name, lot_name, qty
|
||||
```
|
||||
|
||||
- Bundle LOT — внутренний, скрыт в обычном UI LOT по умолчанию.
|
||||
- Bundle expansion (разворачивание) происходит только внутри PriceForge при расчёте warehouse-прайслиста.
|
||||
- Bundle expansion происходит в PriceForge: при расчёте warehouse-прайслиста и при создании partnumber book snapshot.
|
||||
|
||||
### 4. Ignore-логика
|
||||
|
||||
@@ -64,14 +64,31 @@ qt_lot_bundle_items: bundle_lot_name, lot_name, qty
|
||||
|
||||
| Таблица | Назначение |
|
||||
|---------|------------|
|
||||
| `lot_partnumbers` | Канонические маппинги `(vendor, partnumber)` → `lot_name` |
|
||||
| `lot_partnumbers` | Канонические маппинги `(vendor, partnumber)` → `lot_name`; колонка `is_primary_pn` — ведущий PN для qty в BOM |
|
||||
| `qt_lot_bundles` | Определения бандлов (bundle LOT → описание) |
|
||||
| `qt_lot_bundle_items` | Состав бандла: `(bundle_lot_name, lot_name, qty)` |
|
||||
| `qt_vendor_partnumber_seen` | Реестр seen-записей (уникально по `partnumber`) + флаг `is_ignored` |
|
||||
| `qt_partnumber_books` | Версионированные снимки маппинга для QuoteForge (пишет PriceForge) |
|
||||
| `qt_partnumber_book_items` | Строки снимка: `(book_id, partnumber, lot_name, is_primary_pn, description)`; бандлы развёрнуты; пустые lot_name и ignored PN исключены |
|
||||
|
||||
Миграции:
|
||||
- `migrations/023_vendor_partnumber_global_mapping.sql`
|
||||
- `migrations/025_dedup_vendor_seen_by_partnumber.sql`
|
||||
- `migrations/026_add_partnumber_books.sql`
|
||||
- `migrations/027_fix_partnumber_books_version_length.sql`
|
||||
- `migrations/028_add_description_to_partnumber_book_items.sql`
|
||||
|
||||
---
|
||||
|
||||
## Partnumber Book — инварианты снимка
|
||||
|
||||
При формировании `qt_partnumber_book_items` обязательно:
|
||||
|
||||
1. **Пустой `lot_name` исключается** — `TRIM(lot_name) = ''` или NULL не попадают в снимок.
|
||||
2. **Ignored PN исключаются** — партномера с `qt_vendor_partnumber_seen.is_ignored = true` не попадают в снимок.
|
||||
3. **Бандлы разворачиваются** — каждая строка содержит конечный `lot_name` компонента, не имя бандла.
|
||||
4. **`description`** берётся из `lot_partnumbers.description`; для развёрнутых бандлов — из родительской записи partnumber.
|
||||
5. **Удаление items при retention** — явное (`DELETE WHERE book_id IN (...)`), не через FK CASCADE.
|
||||
|
||||
---
|
||||
|
||||
@@ -81,8 +98,10 @@ qt_lot_bundle_items: bundle_lot_name, lot_name, qty
|
||||
|------|------|
|
||||
| Миграция | `migrations/023_vendor_partnumber_global_mapping.sql` |
|
||||
| Миграция | `migrations/025_dedup_vendor_seen_by_partnumber.sql` |
|
||||
| Миграции снимков | `migrations/026–028` |
|
||||
| Резолвер | `internal/lotmatch/matcher.go` |
|
||||
| Сервис | `internal/services/vendor_mapping.go` |
|
||||
| Сервис маппингов | `internal/services/vendor_mapping.go` |
|
||||
| Сервис снимков | `internal/services/partnumber_book.go` |
|
||||
| API | `internal/handlers/pricing.go` |
|
||||
| Warehouse calc | `internal/warehouse/snapshot.go` |
|
||||
| Stock import seen/ignore | `internal/services/stock_import.go` |
|
||||
|
||||
Reference in New Issue
Block a user