7.9 KiB
7.9 KiB
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). - Если внешняя система ошибочно записала в
qt_vendor_partnumber_seen.partnumberзначение, равноеlot.lot_name, такая строка считается мусором и не должна попадать в Global Vendor Mappings UI. Такие seen-строки подлежат очистке, если для этого значения нет явного PN→LOT маппинга вlot_partnumbers.
5. Клиентская совместимость
- Клиент потребляет LOT-based прайслисты (как обычно).
- Bundle expansion/allocation происходит только внутри PriceForge.
Allocation при отсутствии estimate
Если у bundle-компонента нет estimate-цены:
- Fallback: взять из предыдущего активного warehouse-прайслиста.
- Если нет предыдущего — цена
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 |
Версионированные книги 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.sqlmigrations/025_dedup_vendor_seen_by_partnumber.sqlmigrations/026_add_partnumber_books.sqlmigrations/027_fix_partnumber_books_version_length.sqlmigrations/028_add_description_to_partnumber_book_items.sqlmigrations/029_change_partnumber_book_items_to_lots_json.sql
Partnumber Book — инварианты снимка
При формировании partnumber book обязательно:
qt_partnumber_book_itemsсодержит одну строку наpartnumber— дубликаты по PN не допускаются.lots_jsonсодержит полный состав PN как JSON-массив объектов{lot_name, qty}.- Ignored PN исключаются — партномера с
qt_vendor_partnumber_seen.is_ignored = trueне попадают ни в каталог, ни вpartnumbers_jsonкниги. - Бандлы не разворачиваются в отдельные строки — bundle PN сохраняется одной записью, а состав компонентов уходит в
lots_json. descriptionберётся изlot_partnumbers.description.qt_partnumber_books.partnumbers_jsonхранит отсортированный список PN, входящих в конкретную книгу.- Конфликт разных составов для одного PN недопустим — если при построении книги один и тот же PN даёт разные
lots_json, создание snapshot должно завершиться ошибкой.
QuoteForge Read Contract
QuoteForge must read the active book header first:
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:
SELECT partnumber, lots_json, is_primary_pn, description
FROM qt_partnumber_book_items
WHERE partnumber IN (...PNs from partnumbers_json...);
Contract notes:
partnumbers_jsonis the membership snapshot of the selected book.qt_partnumber_book_itemsis the current canonical catalog of PN compositions.lots_jsonformat is JSON array:[{"lot_name":"CPU_X","qty":2},{"lot_name":"RAM_X","qty":4}]- QuoteForge must not expect
lot_namecolumn inqt_partnumber_book_itemsanymore.
Связанные модули
| Роль | Файл |
|---|---|
| Миграция | migrations/023_vendor_partnumber_global_mapping.sql |
| Миграция | migrations/025_dedup_vendor_seen_by_partnumber.sql |
| Миграции снимков | migrations/026–029 |
| Резолвер | 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