BR 1.17 — Привязка опций structural-модификатора к 1С
Зависит от:
Контекст
В 1С Общепит каждая вариация блюда — отдельная номенклатура с уникальным кодом:
| 1С-код | Название в 1С | Цена |
|---|---|---|
00000001234 | Кофе Капучино 250 гр.порц. | 220 ₽ |
00000001235 | Кофе Капучино макси 300 гр.порц. | 250 ₽ |
00000001236 | Кофе Капучино макси 400 гр.порц. | 280 ₽ |
Бухгалтер ведёт техкарты на каждый размер отдельно, сверяет чеки с этими кодами, делает декларации и отчётность.
В нашем POS такие вариации лучше показать одной плиткой «Капучино» с выбором размера через structural-модификатор (см. BR 1.9.2). Меньше плиток на экране, меньше ошибок ввода кассира.
Узкое горлышко — связать две картины: на нашей стороне один продукт + модификатор, на 1С-стороне — три отдельных кода. У modifier_options сейчас НЕТ поля для 1С-кода. Без этого выгрузка чеков в 1С теряет детализацию.
Эта BR — фундамент для выгрузки чеков в 1С
Сама логика выгрузки описана отдельной BR (предлагается ветка «Интеграции с 1С Общепит»). Здесь — только данные в каталоге.
Решение (минимальный скоуп)
Одно новое поле: modifier_options.sku_1c — код номенклатуры 1С для опции structural-модификатора.
При выгрузке отчёта в 1С: если у позиции выбрана structural-опция с sku_1c — используется он. Если у продукта нет structural-модификатора — используется существующее поле products.sku (в которое мы уже пишем '1c:КОД').
Что НЕ добавляем (намеренно, чтобы минимизировать скоуп):
products.sku_1c— уproducts.skuуже хранится'1c:КОД', работает на cifra1/cifra2. Переименование = гигиена без функции.modifier_options.name_1c— оригинальное имя 1С можно класть в существующееmodifier_options.description(уже используется как костыль в cifra2). Имя при выгрузке берётся в 1С на её стороне по коду — нам имя не нужно для матчинга.
Требования
1. Расширение схемы
catalog.modifier_options — добавить одно поле:
| Поле | Тип | Nullable | Описание |
|---|---|---|---|
sku_1c | varchar(50) | да | Код 1С-номенклатуры. Уникален в рамках franchise_id (если задан). Используется при выгрузке отчётов в 1С. |
Также: deleted_at (timestamp, nullable) — если ещё не существует, добавляется. Нужно для soft-delete опций при сохранении исторических заказов (см. ниже).
2. Правила валидации
При создании/обновлении продукта в catalog-service:
- Простой продукт (без structural-модификатора):
products.skuостаётся как есть (значение'1c:КОД'или штрихкод/артикул). - Виртуальный продукт (хочет быть привязанным к 1С через опции): должен иметь ровно один structural-модификатор, у всех опций которого заполнен
sku_1c. - Free-модификаторы:
sku_1cопционально (поле есть впрок для будущего, но валидация не требует — в этой BR не используется).
Ограничение «один structural-мод на продукт» — критично. Алгоритм разрешения 1С-позиции (см. §3) однозначен только если structural-мод один. Многомерные сценарии решаются через «N по M» виртуальных продуктов (см. §4).
Где запретить второй structural — backend или только UI?
При реализации проверить состояние order-service / catalog-service на момент работы:
- Если бэкенд уже принимает несколько structural-модификаторов на продукт (например — продукт «Пицца» с «Размер» + «Тесто» из тестовых fixtures), и в order_modifier_entries это уже сохраняется корректно — резать только на фронте (admin-web показывает 1, бэкенд принимает N). Это снимает риск миграции существующих данных.
- Если бэкенд не готов — добавляем валидацию на бэк-стороне.
- Аудит-запрос перед стартом:
SELECT product_id, COUNT(*) FROM product_modifiers WHERE binding_type='structural' GROUP BY product_id HAVING COUNT(*) > 1— найти проблемные продукты в текущей БД.
Ошибки:
STRUCTURAL_OPTION_MISSING_SKU_1C— у одной из опций structural-мода пустойsku_1c.MULTIPLE_STRUCTURAL_MODIFIERS_NOT_ALLOWED— попытка добавить второй structural-модификатор к продукту (если решено валидировать на бэке).
3. Логика разрешения 1С-позиции для чека
Псевдокод (для будущей выгрузки — реализуется в отдельной BR):
def resolve_1c_position(order_item):
structural_option = order_item.get_structural_option()
if structural_option and structural_option.sku_1c:
return structural_option.sku_1c
if order_item.product.sku and order_item.product.sku.startswith('1c:'):
return order_item.product.sku[3:] # отрезать префикс '1c:'
raise CannotMapTo1C(order_item.product.id)
Snapshot vs lookup
При выгрузке исторических чеков актуальные значения
sku_1cмогут отличаться от тех, что были на момент продажи (бухгалтер перепривязал). В этой BR используется lookup (текущее значение). Если потребуется аудит-точность — отдельная BR «Snapshot 1С-кодов в order_items».
4. Подход к многомерным модификаторам — «N по M»
Если в 1С продукт имеет несколько измерений (размер × молоко × сироп), и каждая комбинация — отдельная номенклатура в 1С:
1С:
Капучино обычный 250 → код 001
Капучино обычный 300 → код 002
Капучино обычный 400 → код 003
Капучино соевый 250 → код 004
Капучино соевый 300 → код 005
Капучино соевый 400 → код 006
Капучино овсяный 250 → код 007
Капучино овсяный 300 → код 008
Капучино овсяный 400 → код 009
Pattern в нашей ERP — отражение структуры 1С:
| Виртуальный продукт | Structural «Объём» — опции |
|---|---|
| Капучино обычный | 250 → 001, 300 → 002, 400 → 003 |
| Капучино соевый | 250 → 004, 300 → 005, 400 → 006 |
| Капучино овсяный | 250 → 007, 300 → 008, 400 → 009 |
3 виртуальных продукта × 3 опции каждый = 9 sku_1c, покрывают все комбинации.
Это полностью покрывается данной BR без новых полей или таблиц. Импорт-скрипт автоматически группирует по «корню имени» — если в 1С позиции названы Кофе Капучино обычный 250, Кофе Капучино обычный 300 — корень Кофе Капучино обычный, образуется один виртуальный продукт.
POS UX компенсация (1 плитка вместо N) — отдельная задача через подкатегории или display_group. Не часть BR 1.17.
Настоящая матрица (отдельная таблица product_combinations с массивом option_ids) — overkill. В реальной 1С такого почти не встречается, потому что у каждой комбинации обязана быть своя техкарта → отдельная позиция. В этой BR не реализуется. При появлении конкретного запроса — отдельная BR.
5. UI
Карточка structural-модификатора (admin-web)
В таблице опций — одна новая колонка: «Код 1С (sku_1c)». Inline-редактирование.
Состояние карточки:
- Если хотя бы одна опция без
sku_1c→ красное предупреждение «Не все опции привязаны к 1С — выгрузка не сработает». - Иконка-подсказка: «Где взять код 1С? — В справочнике номенклатуры 1С, колонка “Код”».
Карточка продукта (admin-web)
Без новой секции «Связь с 1С» — продукт показывает свой статус привязки через состояние модификатора:
- Простой (без modifier’ов) —
skuотображается как сейчас. - Виртуальный (с structural-modifier) — добавляется индикатор: ✅ «Все опции привязаны к 1С» / ⚠️ «Не все опции имеют sku_1c».
POS Desktop
Без изменений. Текущая логика модификаторов уже работает (см. реализацию «Бургер + Соус» в BR 1.8). Базовая цена «материнского» продукта = 0 ₽, цена опции = полная цена варианта.
Кассир видит:
- Плитка «Капучино» (без цены или «от 220 ₽»).
- Тап → модалка с тремя кнопками: 250 мл 220₽, 300 мл 250₽, 400 мл 280₽.
- Выбор → конкретная позиция в корзине с правильной ценой.
6. Soft-delete опций
При удалении опции из админки:
- Запись в
modifier_optionsостаётся (помечаетсяdeleted_at = now()). - В POS-меню и в админке опция не отображается.
- При выгрузке исторических заказов (где
order_modifier_entries.modifier_option_idуказывает на удалённую опцию) —sku_1cвсё ещё читается корректно.
Если поле deleted_at ещё не существует в modifier_options — добавить в миграции. Если уже есть — использовать как есть.
7. Изменение скрипта импорта scripts/1c-import/import-1c-orp-grouped.py
Скрипт уже сейчас делает группировку по «корню имени без размера» — этот режим включён по умолчанию. После реализации BR 1.17 надо:
- Переместить 1С-код опции из
description(где он сейчас как костыль'1c:КОД | оригинал') → в явное полеsku_1c. - Для простых продуктов оставить
sku = '1c:КОД'как сейчас (без изменений). - Сохранить алгоритм извлечения корня:
- Pattern:
\s+(макси\s+)?(\d+)\s*(мл|г|гр)\b\.?\s*$— захватывает trailing размер. - Корень = имя минус trailing размер.
- Минимум 2 позиции с одинаковым корнем → группа.
- Pattern:
Затронутые сервисы
| Сервис | Изменения |
|---|---|
| catalog-service | Liquibase миграция (1 колонка + soft-delete если нет), Entity/DTO, валидация «один structural + опции с sku_1c» |
| admin-bff | Проксирование sku_1c в /api/v1/catalog/modifier-options |
| admin-web | Колонка sku_1c в карточке structural-модификатора, индикатор состояния на продукте |
| order-service | Без изменений (modifier_option_id уже сохраняется в order_modifier_entries) |
| scripts/1c-import/ | Миграция description-костыля в явное поле sku_1c |
| pos-desktop | Без изменений |
Что НЕ входит в эту BR
- Логика выгрузки чеков в 1С — отдельная BR из ветки «Интеграции с 1С Общепит».
products.sku_1c— уже работает черезproducts.skuс префиксом'1c:'. Гигиенический рефакторинг — отдельная задача (не функция).*.name_1c— оригинальные имена 1С хранятся вdescription(если нужны для аудита). Для выгрузки имени не требуются — матчинг по коду.- Free-модификаторы с
sku_1c— поле опционально, логика отдельной BR (например для случая «Молоко соевое доплата» как отдельная номенклатура в 1С). - Многомерные модификаторы через матрицу — решаются паттерном «N по M» (см. §4).
- Snapshot 1С-кодов в order_items — для аудита исторических заказов. Отдельная BR при появлении запроса.
- Bidirectional синхронизация с 1С — не предмет этой BR.
- Управление акциями и скидками — отдельная BR. Сейчас в 1С скидки видны как разные цены под одним кодом; в нашей ERP это будет обрабатываться через
pos.discount.applypermission + ручную правку цены кассиром.
Известные ограничения
-
Двойные цены под одним 1С-кодом в чеках (Шаурма 198/242, Капучино 250/310) — не покрываются BR 1.17. Это акции/скидки, не разные позиции. Решение — отдельная BR по управлению акциями. В рамках 1.17 берётся модальная цена как стандартная.
-
Аверс vs наш POS — синхронизация акций. Сейчас все акции живут в Аверсе. После перехода на наш POS их нужно либо вручную пересоздать в нашей админке, либо кассиры будут применять руками. Не предмет 1.17.
-
Бухгалтер удалит позицию в 1С. Тогда наш
sku_1cустареет → выгрузка пробьётся в deprecated-позицию. Решение: админ обновляетsku_1cв карточке опции. Автоматического sync нет (это другая BR). -
Множественные внешние системы. Если в будущем появится интеграция с СБИС/1С Розница/PayKeeper-каталогом — у нас будет
sku_1c+sku_sbis+ … Это анти-паттерн, лучше будущаяexternal_ids-таблица. В рамках 1.17 фиксируем под 1С специфически. -
Конфликт с PayKeeper Catalog Sync (BR 3.4). В наших цифра1/цифра2 импортах мы пишем в
products.skuзначение'1c:КОД'. Если PayKeeper Catalog Sync читаетproducts.skuдля своего маппинга — возможен конфликт (PK ожидает свой формат, а получает 1С-код).
Проверить при реализации BR 1.17
- В каком поле PaykeeperAdapter хранит маппинг с нашими продуктами (BR 3.4)? Через
sku, отдельную таблицуpaykeeper_catalog_items, или черезproduct_idнапрямую?- Если через
sku— нужно либо мигрировать наш формат'1c:КОД'в отдельное поле (введёмproducts.sku_1cтогда уже), либо рефакторить PK на использованиеproduct_id/ нового поля.- Если через отдельную таблицу /
product_id— конфликта нет,skuсвободен под'1c:КОД'.- Скорее всего PK не знает о модификаторах, поэтому
modifier_options.sku_1cдля PK безопасно в любом случае. Сверять файлerp-paykeeper-adapter/.../CatalogEventConsumer.javaи таблицы PK-БД на момент реализации.
Verification
-
Применить миграцию catalog-service — колонка
sku_1cпоявилась вmodifier_options. Такжеdeleted_atесли не было. -
Через админку:
- Открыть карточку structural-модификатора «Объём» у Капучино → видны 3 опции с колонкой
sku_1c. - У одной опции стереть
sku_1c→ попытка сохранить → ошибкаSTRUCTURAL_OPTION_MISSING_SKU_1C. - Восстановить → сохраняется.
- Попытка добавить второй structural-мод к Капучино → ошибка
MULTIPLE_STRUCTURAL_MODIFIERS_NOT_ALLOWED.
- Открыть карточку structural-модификатора «Объём» у Капучино → видны 3 опции с колонкой
-
Перезалить cifra2 через обновлённый скрипт —
descriptionопций пустеют,sku_1cзаполнены. Проверить SQL-запросом:SELECT mo.name, mo.sku_1c, pmi.price FROM modifier_options mo JOIN modifier_groups mg ON mg.id = mo.modifier_group_id LEFT JOIN price_list_modifier_items pmi ON pmi.modifier_option_id = mo.id WHERE mg.franchise_id = 'c2f0a000-0000-0000-0000-000000000001' ORDER BY mg.name, mo.display_order; -
На POS Desktop (cifra2@nirbi.ru) — без визуальных изменений по сравнению с предыдущим состоянием. Каталог рендерится так же.
-
Soft-delete тест: удалить опцию «Капучино 250 мл» из админки → она исчезает из POS-меню, но запись остаётся в БД с
deleted_at→ если в order_modifier_entries есть исторические ссылки —sku_1cвсё ещё доступен через join.
Связь с другими работами
- Пилот Кофейни Цифра (cifra2) уже работает через костыль
description— после реализации BR 1.17 миграция скриптом без даунтайма. - BR закладывает фундамент для будущей BR «Excel-выгрузка продаж в 1С Общепит» (предположительно 3.6).
- Подход «virtual product + structural options с external_id» универсален — работает для любой бухгалтерской системы (1С Розница, СБИС, etc.). При появлении второй интеграции — рефакторинг в
external_ids-таблицу (отдельная архитектурная BR).