- Add glob pattern support (* and ?) for ignore rules stored in qt_vendor_partnumber_seen (is_pattern flag, migration 041) - Pattern matching applied in stock/competitor import, partnumber book snapshot, and vendor mappings list (Go-side via NormalizeKey) - BulkUpsertMappings: replace N+1 loop with two batch SQL upserts, validating all lots in a single query (~1500 queries → 3-4) - CSV import: multi-lot per PN via repeated rows, optional qty column - CSV export: updated column format vendor;partnumber;lot_name;qty;description;ignore;notes - UI: ignore patterns section with add/delete, import progress feedback - Update bible-local/vendor-mapping.md with new CSV format Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
7.6 KiB
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.sqlmigrations/026_add_partnumber_books.sqlmigrations/027_fix_partnumber_books_version_length.sqlmigrations/028_add_description_to_partnumber_book_items.sqlmigrations/031_drop_is_primary_pn.sqlmigrations/032_drop_legacy_vendor_mapping_tables.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книги. - Многокомпонентные PN не разворачиваются в отдельные строки — PN сохраняется одной записью, а состав компонентов уходит в
lots_json. descriptionберётся изqt_partnumber_book_items.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, 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_nameoris_primary_pncolumns inqt_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 / Export (Global Vendor Mappings UI)
- Формат CSV для Excel (RU locale): разделитель
; - Кодировка:
UTF-8(BOM допускается и поддерживается) - Колонки:
vendor;partnumber;lot_name;qty;description;ignore - Допустим импорт как с заголовком, так и без заголовка (в фиксированном порядке колонок выше)
- Пустые строки пропускаются
- Для строки обязательны
partnumberи (lot_nameили заполненныйignore) - Если
ignoreзаполнен иlot_nameпустой, строка помечается как ignored вqt_vendor_partnumber_seen
Маппинг один p/n → несколько lot
Один p/n можно привязать к нескольким lot через несколько строк с одинаковым partnumber.
Строки аккумулируются, затем сохраняются как один lots_json (upsert).
vendor;partnumber;lot_name;qty;description;ignore
Intel;E-2336;Server_Intel;1;Процессор Intel;
Intel;E-2336;Server_AMD;2;;
Samsung;MZ7L3480HCHQ-00A07;SSD_480GB;;;
qtyнеобязательно; если пусто или 0 — считается1descriptionберётся из первой строки p/n- Каждый повторный импорт того же p/n перезаписывает весь
lots_json
Экспорт незамапленных p/n
Кнопка «Выгрузить незамапленные» генерирует CSV в том же формате с пустыми lot_name и qty — для заполнения пользователем и последующего импорта.