Документ описывает фактическую реализацию (источники указаны в скобках):
- модель —
backend/app/models.py(TechnologyMaterial,TechnologyOperation,TechnologyParameter,TechnologyRule,ProductConfiguration); - API и валидаторы —
backend/app/products.py(component_spec,operation_spec,parameter_spec,v_condition); - расчёт —
products.plan_components,cost_components,cost_operations,resolve_parameters,matches; - UI —
frontend/src/products.tsx(ProductAdmin,VersionEditor), справка в UI —frontend/src/technologyHelp.tsx.
1. Как теперь работает редактирование #
- Product type (например Lightbox) содержит Technologies (например Light Box · profile construction).
- У технологии есть версии (v1, v2, …). Редактируется текущая версия (status
PUBLISHED) — напрямую, без Create draft.
Обычная правка не создаёт новую версию. Архивные версии (ARCHIVED) — read-only история. - Каждая оценка (Estimate) при выборе технологии замораживает её определение в
ProductConfiguration.technology_snapshot: компоненты, материалы, selector/family, default, waste, condition,
маршрут операций, коэффициенты, параметры, правила и настройки версии. Все пересчёты оценки (Recalculate,
изменение геометрии, LED, правка параметров) используют этот снимок. Цены материалов и ставки операций
заморожены отдельно (price_snapshots), как и раньше. - Поэтому правка технологии не меняет существующие оценки. Новые оценки сразу получают отредактированную технологию.
В старой оценке появляется сообщение “Technology was edited …” и кнопка Update to current technology —
только она переносит оценку на текущее определение (после этого нужен Recalculate, как раньше при “Update to vN”). - Create draft / Publish остались в API (
POST /api/technologies/{id}/versions,POST /api/technology-versions/{id}/publish),
но скрыты из обычного UI. Если черновик уже существует, его можно открыть в Version history и опубликовать.
2. Поля компонента (Components & materials) #
| Поле | Хранится как | Значение |
|---|---|---|
| Component | component_code, строка UPPER_SNAKE_CASE (2–60 символов, regex ^[A-Z][A-Z0-9_]{1,59}$) |
Системный код роли. Используется расчётом: имя nesting-группы, ключ строки стоимости, ссылки из операций (колонка Component, драйвер MACHINE_TIME), из параметров (колонка Component у component_value/material_family), метрики <code>_sheets и <code>_cut_length_m. В UI только для чтения: переименование используемого кода ломает эти ссылки. Два компонента могут иметь один код, если их Condition взаимоисключающие (Lightbox: два BACK). |
| Label | строка | Только отображение (строка в разбивке, заголовок nesting-группы). В расчёте не участвует. |
| Method | calculation_method, один из NESTED_SHEET, PERIMETER_LENGTH, AREA, LENGTH_INPUT, MANUAL_COST |
Как считается количество и стоимость (раздел 3). |
| Materials (selector value) | available_materials: список {"material_id": "<id канонического материала>", "value": <число или null>} |
Допустимые материалы и их значение для selector (раздел 4). |
| Selector | selector_parameter: ключ параметра или пусто |
Параметр оценки, значение которого выбирает материал (раздел 5). |
| Family | family_parameter: ключ параметра или пусто |
Параметр оценки, значение которого — название материала (фильтр, раздел 5). |
| Default | default_value: число или пусто |
Значение selector, если параметр selector пуст (раздел 4). |
| Waste × | waste_factor: число 0.5…5 |
Множитель количества для PERIMETER_LENGTH, AREA, LENGTH_INPUT (раздел 6). |
| Quantity parameter (раньше “Qty param”) | quantity_parameter: ключ или пусто |
Откуда LENGTH_INPUT берёт метры, а MANUAL_COST — сумму (раздел 6). |
| Category | cost_category: MATERIALS, MACHINE, LABOUR, OUTSOURCING, LOGISTICS, OPTIONS |
Группа в разбивке. В итогах оценки MATERIALS → Material; MACHINE → колонка cut cost; остальные → other operations; MACHINE и остальные вместе = Operations. |
| Condition | condition: JSON-объект |
Когда строка применяется (раздел 7). {} = всегда. |
| Active | boolean | Неактивная строка полностью пропускается расчётом. |
3. Methods (METHODS в products.py) #
| Method | Что считает | Нужно | Количество |
|---|---|---|---|
NESTED_SHEET |
Раскладка геометрии изделия на листах выбранного материала (одна nesting-группа на код компонента, размер листа берётся из материала). Стоимость = списанная площадь × цена за м² (цена SHEET делится на площадь листа, цена M2 берётся как есть). |
Материал с размерами листа и ценой SHEET или M2. |
Списанные м² по результату nesting (листы минус остатки, возвращённые на склад). Waste не применяется. |
PERIMETER_LENGTH |
Суммарный периметр геометрии изделия (Lightbox: 2W + 2H на короб × количество). | Материал с ценой за METER. |
периметр, м × Waste. |
AREA |
Суммарная площадь геометрии изделия. | Материал с ценой за M2. |
площадь, м² × Waste. |
LENGTH_INPUT |
Длина, введённая в оценке (например профиль рамы). | Quantity parameter (метры), материал за METER. |
значение параметра × Waste (пусто → 0). |
MANUAL_COST |
Денежная сумма (например предложение поставщика; Lightbox LED = модули × цена модуля). | Quantity parameter (EUR). Материал не нужен. | сумма = значение параметра; пусто/0 → предупреждение “no cost entered”. |
Если единица цены материала не подходит методу (например цена за метр у NESTED_SHEET), оценка показывает предупреждение.
4. Materials (selector value) и выбор материала #
Каждый тег — запись {"material_id": "...", "value": ...}. material_id — id канонического материала
(Settings → Materials). В UI тег подписан как <название> — <толщина> mm · <лист>; число после → — selector value.
Отключённые материалы пропускаются (component_entries).
Алгоритм выбора одного материала (plan_components), строго по порядку:
- Если задан Family и у параметра family есть значение — оставить только материалы с
name= этому значению. - Если задан Selector и у его параметра есть значение — взять материал с равным selector value
(числа сравниваются численно:3=3.0). - Иначе, если значение selector пустое и задан Default — взять материал с selector value = Default.
- Иначе (значение selector пустое) — взять первый материал списка.
Если у параметра selector есть значение, но совпадения нет, материал не выбирается, и оценка предупреждает
“no material configured for the selected options”.
Важно. Несколько материалов без selector value и без Selector означают: всегда используется первый.
Сейчас в Lightbox так настроен BACK · Aluminium: Aluminium 1.50 mm и Aluminium 2.00 mm без значений —
всегда берётся 1.50 mm. Чтобы толщину можно было выбирать, задайте записям значения1.5и2, создайте параметр
(напримерback_thickness_mm, типcomponent_value, ComponentBACK) и укажите его в Selector.
5. Selector и Family #
Selector — ключ (lower_snake_case) параметра оценки этой технологии, например face_thickness_mm.
Его текущее значение в оценке (ручной выбор → значение Auto-правила → default параметра) сравнивается
с selector value материалов. Параметр типа component_value, у которого колонка Component = этот компонент,
автоматически предлагает в оценке selector values его материалов (так появляется «Face thickness 3 / 4 / 5 mm»).
Family — ключ параметра типа material_family, значение которого — название материала,
например face_material = "PMMA opal". Он сужает список до одного семейства, поэтому
“PMMA opal 3 mm” и “PMMA clear 3 mm” могут иметь одинаковый selector value 3.
Пустой Selector/Family просто не используется. Серое слово family в пустом поле — это placeholder, не значение.
UI предлагает ключи параметров этой технологии (datalist); вводить можно только lower_snake_case.
6. Waste и Quantity parameter #
Waste × умножает измеренное количество до умножения на цену; только для PERIMETER_LENGTH, AREA,
LENGTH_INPUT. Пример: периметр 10 м, Waste 1.05 → 10.5 м × 3.40 €/м = 35.70 €. 1 = без припуска.
NESTED_SHEET его игнорирует — раскладка уже даёт реальный расход и остатки; MANUAL_COST тоже.
Quantity parameter — ключ параметра оценки. LENGTH_INPUT читает из него метры, MANUAL_COST — сумму в EUR.
Другие методы его игнорируют. Пусто: LENGTH_INPUT = 0, MANUAL_COST = 0 с предупреждением.
Примеры: frame_length_m (рама Channel Letters), led_cost (сумма поставщика; у Lightbox — модули × цена модуля,
вычисляется LED-раскладкой).
6a. Operation route и ставки #
Технология не хранит ставок. Каждый шаг маршрута ссылается на операцию из Settings → Operations
(выбирается из списка), и ставка всегда берётся из этой операции (operation.rate + operation.unit).
Нужна другая ставка — выберите или создайте операцию с этой ставкой. Поле Rate override удалено из редактора,
API его отклоняет. Старые значения остаются только в архивных версиях и в замороженных оценках.
| Unit операции | Rate означает | Для каких шагов |
|---|---|---|
HOUR (или pricing per_hour) |
€ в час | шаги по времени: заполнен Min / unit и/или Fixed min |
MINUTE |
€ в минуту (показывается как €/ч × 60) | шаги по времени |
METER, M2, PIECE, SHEET, TRIP, FIXED |
€ за единицу | шаги за единицу: Min / unit и Fixed min пустые |
Шаг по времени (заполнен Min / unit или Fixed min, либо драйвер PER_MINUTE):
минуты = Fixed min + (Quantity source × Coefficient) × Min / unit, стоимость = минуты ÷ 60 × €/ч операции.
Пустой Min / unit считается как 1 минута на единицу Quantity source; для чисто фиксированного времени пишите 0.
Шаг за единицу (оба поля пустые): стоимость = Quantity source × Coefficient × ставка операции.
Пример Light Box, сборка 45 минут на ящик: Quantity source box_count, Min / unit 45, Fixed min 0;
45 минут на весь заказ: Min / unit 0, Fixed min 45. Ставка — у операции «Assemble 4 corners» в Settings → Operations
(Unit HOUR, Rate, например, 21).
Название драйвера для таких шагов — только подпись. Свои формулы есть у MACHINE_TIME (подача CNC из машинного профиля
материала) и у драйверов Channel Letters (BODY_ASSEMBLY, PVC_TRIM_GLUING, ASSEMBLE_ON_BASE, CLOSE_FACE),
но и они используют почасовую ставку операции. Если шаг по времени ссылается на непочасовую операцию,
расчёт и редактор показывают предупреждение (раньше получалось молча 0).
Миграция f7b9d1e3a5c4 перенесла существующие Rate override в операции без изменения стоимости: совпадающие со ставкой
операции — удалены; отличающиеся — маршрут переключён на отдельную операцию с этой ставкой
(например «LED installation · Aluminium / Powder Coated», 21 €/ч). Операции Light Box стали почасовыми
(покраска и UV — за м²).
7. Condition #
Проверяется функцией products.matches(condition, values); values — значения параметров оценки по ключам.
Тот же синтаксис используют Condition операций и Visible if параметров. UI и API проверяют синтаксис
и не дают сохранить неверное условие.
| Нужно | Пишите | Смысл |
|---|---|---|
| всегда | {} |
без условия |
| одно значение | {"back_material": "ACP"} |
равно "ACP" |
| boolean | {"frame": true} |
поле Frame = Yes |
| число | {"profile_mm": 100} |
равно 100 (100 = 100.0) |
| одно из | {"graphics": {"in": ["CUT_VINYL", "PRINTED_VINYL"]}} |
значение в списке |
| ни одно из | {"painting_mode": {"not_in": ["NONE"]}} |
значение не в списке |
| не равно | {"painting_mode": {"ne": "NONE"}} |
отличается ("eq" — равно) |
| несколько (И) | {"frame": true, "frame_paint": true} |
все условия сразу |
| нет значения | {"led_cost": null} |
значение null; отсутствующее или скрытое (Visible if) поле имеет значение null |
Не поддерживается: ИЛИ между разными ключами, <, >, диапазоны, формулы. Неизвестные операторы
(например "gt") раньше молча игнорировались — теперь отклоняются с ошибкой. Ключи должны быть параметрами этой технологии.
Сравнение учитывает тип: "100" (строка) ≠ 100 (число), "true" ≠ true. Пишите значение так, как его хранит поле:
choice → строка, Yes/No → true/false, числовые поля → число.
8. Синтаксис по полям #
| Поле | Ожидаемый тип | Правильно | Неправильно |
|---|---|---|---|
| Component | код | FACE |
face, "FACE" |
| Label | текст | Face · PMMA opal |
(пусто) |
| Selector value материала | число или пусто | 3, 1.5 |
"3", 3 mm |
| Selector / Family | ключ или пусто | face_thickness_mm |
Face thickness, {face_thickness_mm} |
| Default | число или пусто | 3 |
"3", [3], {"value":3} |
| Waste × | число 0.5–5 | 1.05 |
5%, 105 |
| Quantity parameter | ключ или пусто | frame_length_m |
frame length |
| Condition / Visible if | JSON-объект | {"letter_depth_mm": 100} |
letter_depth_mm=100, {'frame': true}, [], "" |
| Default параметра (Estimate fields) | JSON-объект с "value" |
{"value": 3}, {"value": "ACP"} |
3 |
| Options параметра | JSON-массив объектов | [{"value": "ACP", "label": "ACP"}] |
ACP, ALUMINIUM |
Правила JSON:
{ }— объект (пары ключ → значение),[ ]— список. Condition — всегда объект; списки только внутриin/not_in.- Ключи и строки — в двойных кавычках:
"ACP". Одинарные'ACP'— невалидный JSON. - Числа без кавычек, через точку:
100,1.5. - Логические значения строчными без кавычек:
true/false; пустое значение —null. - Пары и элементы списка разделяются запятыми, без запятой в конце:
{"a": 1, "b": 2}. - Пустое поле Condition сохраняется как
{}(Always).""и[]отклоняются.
9. Примеры из текущих технологий #
Channel Letters · Aluminium / Powder Coated · FACE
| Component | Method | Materials | Selector | Family | Default | Waste | Quantity parameter | Condition |
|---|---|---|---|---|---|---|---|---|
FACE |
NESTED_SHEET |
PMMA opal 3/4/5 mm → 3, 4, 5; PMMA clear 3/5 mm → 3, 5 | face_thickness_mm |
face_material |
3 |
1 |
— | {} |
Эта строка означает: лицевая панель раскладывается на листах; в оценке выбирается семейство (face_material, например
“PMMA opal”) и толщина (face_thickness_mm); движок оставляет PMMA opal и берёт запись со значением выбранной толщины;
без выбора — 3 mm; применяется всегда.
Channel Letters · Aluminium / Powder Coated · RETURN
RETURN |
PERIMETER_LENGTH |
Aluminium return coil 60 mm → 60; 100 mm → 100 | return_depth_mm |
— | 60 |
1.05 |
— | {} |
|---|---|---|---|---|---|---|---|---|
Эта строка означает: борт покупается погонными метрами — периметр букв × 1.05; глубина буквы (return_depth_mm,
часто задаётся правилом по высоте) выбирает ленту 60 или 100 mm.
Lightbox · Light Box · profile construction · FACE
FACE |
NESTED_SHEET |
PMMA opal 3/4/5 mm → 3, 4, 5 | face_thickness_mm |
— | 3 |
1 |
— | {} |
|---|---|---|---|---|---|---|---|---|
Эта строка означает: одна лицевая панель на короб (Width × Height) раскладывается на листах PMMA opal;
толщина из оценки выбирает 3, 4 или 5 mm, по умолчанию 3 mm.
Lightbox · Light Box · profile construction · BACK (две строки)
| Component | Method | Materials | Selector | Family | Default | Waste | Quantity parameter | Condition |
|---|---|---|---|---|---|---|---|---|
BACK |
NESTED_SHEET |
ACP 3.00 mm | — | — | — | 1 |
— | {"back_material": "ACP"} |
BACK |
NESTED_SHEET |
Aluminium 1.50 mm, Aluminium 2.00 mm (без значений) | — | — | — | 1 |
— | {"back_material": "ALUMINIUM"} |
Эти строки означают: применяется ровно одна строка BACK в зависимости от поля Back material; обе используют код BACK,
поэтому nesting-группа всегда называется BACK; у алюминиевой строки нет Selector, поэтому всегда берётся первый
материал — Aluminium 1.50 mm.
Channel Letters · Aluminium / Powder Coated · FRAME
FRAME |
LENGTH_INPUT |
Aluminium frame profile 40×20 | — | — | — | 1.1 |
frame_length_m |
{"frame": true} |
|---|---|---|---|---|---|---|---|---|
Эта строка означает: только если в оценке включена рама — введённая длина рамы (frame_length_m) × 1.1 покупается
как профиль рамы за метр и показывается в группе Options.