122 lines
6.6 KiB
Markdown
122 lines
6.6 KiB
Markdown
# Vendor Mapping (Сопоставление partnumber → LOT)
|
||
|
||
> Решение зафиксировано: 2026-02-18
|
||
|
||
## Концепция
|
||
|
||
`qt_partnumber_book_items` — канонический контракт сопоставления для внешнего конфигуратора.
|
||
|
||
Одна строка на `partnumber`; состав хранится в `lots_json` как список `{lot_name, qty}`.
|
||
|
||
---
|
||
|
||
## Правила
|
||
|
||
### 1. Единственная запись на PN
|
||
|
||
Запрещено создавать несколько строк для одного `partnumber` в `qt_partnumber_book_items`.
|
||
|
||
### 2. Резолвинг
|
||
|
||
- Сначала точное совпадение по `partnumber` в `qt_partnumber_book_items`.
|
||
- Если состав PN содержит несколько LOT, резолвер возвращает весь список `{lot_name, qty}`.
|
||
- Vendor хранится как metadata в `qt_vendor_partnumber_seen`; канонический vendor-aware mapping в БД отсутствует.
|
||
|
||
### 4. Ignore-логика
|
||
|
||
- **Не использовать** `stock_ignore_rules` для новой логики.
|
||
- Использовать `qt_vendor_partnumber_seen.is_ignored`.
|
||
- `qt_vendor_partnumber_seen` хранится в формате **1 строка на partnumber** (vendor/source не участвуют в уникальности).
|
||
- Ignore применяется по `partnumber` (одинаково для записей с vendor и без vendor).
|
||
- Если внешняя система ошибочно записала в `qt_vendor_partnumber_seen.partnumber` значение, равное `lot.lot_name`,
|
||
такая строка считается мусором и не должна попадать в Global Vendor Mappings UI.
|
||
Такие seen-строки подлежат очистке, если для этого значения нет явной строки в `qt_partnumber_book_items`.
|
||
|
||
---
|
||
|
||
## Таблицы БД
|
||
|
||
| Таблица | Назначение |
|
||
|---------|------------|
|
||
| `qt_vendor_partnumber_seen` | Реестр seen-записей (уникально по `partnumber`) + флаг `is_ignored` |
|
||
| `qt_partnumber_books` | Версионированные книги PN; каждая книга хранит `partnumbers_json` — список PN, входящих в книгу |
|
||
| `qt_partnumber_book_items` | Глобальный source-of-truth каталог `partnumber -> lots_json`; без дубликатов по `partnumber`; `lots_json` хранит список `{lot_name, qty}` |
|
||
|
||
Миграции:
|
||
- `migrations/023_vendor_partnumber_global_mapping.sql` (historical)
|
||
- `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`
|
||
- `migrations/031_drop_is_primary_pn.sql`
|
||
- `migrations/032_drop_legacy_vendor_mapping_tables.sql`
|
||
|
||
---
|
||
|
||
## Partnumber Book — инварианты снимка
|
||
|
||
При формировании partnumber book обязательно:
|
||
|
||
1. **`qt_partnumber_book_items` содержит одну строку на `partnumber`** — дубликаты по PN не допускаются.
|
||
2. **`lots_json`** содержит полный состав PN как JSON-массив объектов `{lot_name, qty}`.
|
||
3. **Ignored PN исключаются** — партномера с `qt_vendor_partnumber_seen.is_ignored = true` не попадают ни в каталог, ни в `partnumbers_json` книги.
|
||
4. **Многокомпонентные PN не разворачиваются в отдельные строки** — PN сохраняется одной записью, а состав компонентов уходит в `lots_json`.
|
||
5. **`description`** берётся из `qt_partnumber_book_items.description`.
|
||
6. **`qt_partnumber_books.partnumbers_json`** хранит отсортированный список PN, входящих в конкретную книгу.
|
||
7. **Конфликт разных составов для одного PN недопустим** — если при построении книги один и тот же PN даёт разные `lots_json`, создание snapshot должно завершиться ошибкой.
|
||
|
||
## QuoteForge Read Contract
|
||
|
||
QuoteForge must read the active book header first:
|
||
|
||
```sql
|
||
SELECT id, version, created_at, created_by, partnumbers_json
|
||
FROM qt_partnumber_books
|
||
WHERE is_active = 1
|
||
ORDER BY created_at DESC, id DESC
|
||
LIMIT 1;
|
||
```
|
||
|
||
Then load item payloads from the catalog by PN list:
|
||
|
||
```sql
|
||
SELECT partnumber, lots_json, description
|
||
FROM qt_partnumber_book_items
|
||
WHERE partnumber IN (...PNs from partnumbers_json...);
|
||
```
|
||
|
||
Contract notes:
|
||
- `partnumbers_json` is the membership snapshot of the selected book.
|
||
- `qt_partnumber_book_items` is the current canonical catalog of PN compositions.
|
||
- `lots_json` format is JSON array: `[{"lot_name":"CPU_X","qty":2},{"lot_name":"RAM_X","qty":4}]`
|
||
- QuoteForge must not expect `lot_name` or `is_primary_pn` columns in `qt_partnumber_book_items`.
|
||
|
||
---
|
||
|
||
## Связанные модули
|
||
|
||
| Роль | Файл |
|
||
|------|------|
|
||
| Миграция | `migrations/025_dedup_vendor_seen_by_partnumber.sql` |
|
||
| Миграции снимков | `migrations/026–032` |
|
||
| Резолвер | `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`
|