Files
PriceForge/bible-local/vendor-mapping.md
Mikhail Chusavitin f73e3d144d Vendor mapping: wildcard ignore patterns, bulk CSV import, multi-lot qty
- 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>
2026-03-19 09:41:48 +03:00

7.6 KiB
Raw Permalink Blame History

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:

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_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/026032
Резолвер 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 — считается 1
  • description берётся из первой строки p/n
  • Каждый повторный импорт того же p/n перезаписывает весь lots_json

Экспорт незамапленных p/n

Кнопка «Выгрузить незамапленные» генерирует CSV в том же формате с пустыми lot_name и qty — для заполнения пользователем и последующего импорта.