Restore previous bible/ content as bible-local/

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-01 22:26:50 +03:00
parent f0c5aa8da3
commit 27e33db446
115 changed files with 19743 additions and 0 deletions

View File

@@ -0,0 +1,121 @@
# Vendor Mapping (Сопоставление partnumber → LOT)
> Решение зафиксировано: 2026-02-18
## Концепция
`lot_partnumbers` — канонический контракт сопоставления для внешнего конфигуратора.
Маппинг строго 1:1 по ключу `(vendor, partnumber)``lot_name`.
---
## Правила
### 1. Единственная запись на ключ
Запрещено создавать несколько строк для одного ключа `(vendor, partnumber)`.
### 2. Порядок резолвинга (фиксированный)
```
1. Точное совпадение: vendor + partnumber → lot_name
2. Fallback: vendor='' + partnumber → lot_name
```
Резолвер: `internal/lotmatch/matcher.go`
### 3. Составные маппинги — через бандлы
Если один внешний partnumber соответствует нескольким LOT — использовать внутренние бандл-таблицы:
```
lot_partnumbers: (vendor, partnumber) → bundle_lot_name
qt_lot_bundles: bundle_lot_name → [item1, item2, ...]
qt_lot_bundle_items: bundle_lot_name, lot_name, qty
```
- Bundle LOT — внутренний, скрыт в обычном UI LOT по умолчанию.
- Bundle expansion происходит в PriceForge: при расчёте warehouse-прайслиста и при создании partnumber book snapshot.
### 4. Ignore-логика
- **Не использовать** `stock_ignore_rules` для новой логики.
- Использовать `qt_vendor_partnumber_seen.is_ignored`.
- `qt_vendor_partnumber_seen` хранится в формате **1 строка на partnumber** (vendor/source не участвуют в уникальности).
- Ignore применяется по `partnumber` (одинаково для записей с vendor и без vendor).
### 5. Клиентская совместимость
- Клиент потребляет LOT-based прайслисты (как обычно).
- Bundle expansion/allocation происходит только внутри PriceForge.
---
## Allocation при отсутствии estimate
Если у bundle-компонента нет estimate-цены:
1. Fallback: взять из предыдущего активного warehouse-прайслиста.
2. Если нет предыдущего — цена `0`.
---
## Таблицы БД
| Таблица | Назначение |
|---------|------------|
| `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.
---
## Связанные модули
| Роль | Файл |
|------|------|
| Миграция | `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/partnumber_book.go` |
| API | `internal/handlers/pricing.go` |
| Warehouse calc | `internal/warehouse/snapshot.go` |
| Stock import seen/ignore | `internal/services/stock_import.go` |
| Модели | `internal/models/lot.go`, `internal/models/configuration.go` |
| Роутинг | `cmd/pfs/main.go` |
---
## CSV Import (Global Vendor Mappings UI)
- Формат CSV для Excel (RU locale): разделитель `;`
- Кодировка: `UTF-8` (BOM допускается и поддерживается)
- Рекомендуемые колонки: `vendor;partnumber;lot_name;description;ignore`
- Допустим импорт как с заголовком, так и без заголовка (в фиксированном порядке колонок выше)
- Пустые строки пропускаются
- Для строки обязательны `partnumber` и (`lot_name` или заполненный `ignore`)
- Если `ignore` заполнен и `lot_name` пустой, строка помечается как ignored в `qt_vendor_partnumber_seen`