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:
2026-02-21 22:16:16 +03:00
parent 225e1beda9
commit a4457a0a28
13 changed files with 751 additions and 32 deletions

View File

@@ -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) |
---

View File

@@ -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 |
---

View File

@@ -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

View File

@@ -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/026028` |
| Резолвер | `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` |