diff --git a/.claude/skills/skd-compile/scripts/skd-compile.py b/.claude/skills/skd-compile/scripts/skd-compile.py
index aa5ee862..0eb42577 100644
--- a/.claude/skills/skd-compile/scripts/skd-compile.py
+++ b/.claude/skills/skd-compile/scripts/skd-compile.py
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
-# skd-compile v1.39 — Compile 1C DCS from JSON
+# skd-compile v1.40 — Compile 1C DCS from JSON
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
import json
@@ -1956,6 +1956,68 @@ def parse_structure_shorthand(s):
return []
+def emit_user_fields(lines, items, indent):
+ if not items or len(items) == 0:
+ return
+ lines.append(f'{indent}')
+ for uf in items:
+ u_type = 'UserFieldCase' if uf.get('cases') is not None else 'UserFieldExpression'
+ lines.append(f'{indent}\t')
+ if uf.get('dataPath'):
+ lines.append(f'{indent}\t\t{esc_xml(str(uf["dataPath"]))}')
+ if uf.get('title'):
+ emit_mltext(lines, f'{indent}\t\t', 'dcsset:lwsTitle', uf['title'], no_xsi_type=True)
+ if u_type == 'UserFieldExpression':
+ d = uf.get('detail') or {}
+ if d.get('expression'):
+ lines.append(f'{indent}\t\t{esc_xml(str(d["expression"]))}')
+ if d.get('presentation'):
+ lines.append(f'{indent}\t\t{esc_xml(str(d["presentation"]))}')
+ t = uf.get('total') or {}
+ if t.get('expression'):
+ lines.append(f'{indent}\t\t{esc_xml(str(t["expression"]))}')
+ if t.get('presentation'):
+ lines.append(f'{indent}\t\t{esc_xml(str(t["presentation"]))}')
+ else:
+ cases = uf.get('cases') or []
+ if len(cases) == 0:
+ lines.append(f'{indent}\t\t')
+ else:
+ lines.append(f'{indent}\t\t')
+ for c in cases:
+ lines.append(f'{indent}\t\t\t')
+ if c.get('filter'):
+ emit_filter(lines, c['filter'], f'{indent}\t\t\t\t')
+ if c.get('value') is not None:
+ cv = c['value']
+ if isinstance(cv, bool):
+ lines.append(f'{indent}\t\t\t\t{str(cv).lower()}')
+ elif isinstance(cv, (int, float)):
+ lines.append(f'{indent}\t\t\t\t{cv}')
+ else:
+ lines.append(f'{indent}\t\t\t\t{esc_xml(str(cv))}')
+ if c.get('presentation'):
+ emit_mltext(lines, f'{indent}\t\t\t\t', 'dcsset:lwsPresentationValue', c['presentation'], no_xsi_type=True)
+ lines.append(f'{indent}\t\t\t')
+ lines.append(f'{indent}\t\t')
+ lines.append(f'{indent}\t')
+ lines.append(f'{indent}')
+
+
+def emit_table_axis_block(lines, block, indent):
+ """Shared emitter for table column/row and chart point/series."""
+ gb = block.get('groupBy') or block.get('groupFields')
+ emit_group_items(lines, gb, indent)
+ if block.get('filter'):
+ emit_filter(lines, block['filter'], indent)
+ if block.get('order'):
+ emit_order(lines, block['order'], indent)
+ if block.get('selection'):
+ emit_selection(lines, block['selection'], indent)
+ if block.get('outputParameters'):
+ emit_output_parameters(lines, block['outputParameters'], indent)
+
+
def emit_structure_item(lines, item, indent):
item_type = str(item.get('type', 'group'))
@@ -2001,11 +2063,7 @@ def emit_structure_item(lines, item, indent):
if item.get('columns'):
for col in item['columns']:
lines.append(f'{indent}\t')
- emit_group_items(lines, col.get('groupBy') or col.get('groupFields'), f'{indent}\t\t')
- col_order = col.get('order') or ['Auto']
- emit_order(lines, col_order, f'{indent}\t\t')
- col_sel = col.get('selection') or ['Auto']
- emit_selection(lines, col_sel, f'{indent}\t\t')
+ emit_table_axis_block(lines, col, f'{indent}\t\t')
lines.append(f'{indent}\t')
# Rows
@@ -2014,11 +2072,7 @@ def emit_structure_item(lines, item, indent):
lines.append(f'{indent}\t')
if row.get('name'):
lines.append(f'{indent}\t\t{esc_xml(str(row["name"]))}')
- emit_group_items(lines, row.get('groupBy') or row.get('groupFields'), f'{indent}\t\t')
- row_order = row.get('order') or ['Auto']
- emit_order(lines, row_order, f'{indent}\t\t')
- row_sel = row.get('selection') or ['Auto']
- emit_selection(lines, row_sel, f'{indent}\t\t')
+ emit_table_axis_block(lines, row, f'{indent}\t\t')
lines.append(f'{indent}\t')
lines.append(f'{indent}')
@@ -2032,21 +2086,13 @@ def emit_structure_item(lines, item, indent):
# Points
if item.get('points'):
lines.append(f'{indent}\t')
- emit_group_items(lines, item['points'].get('groupBy') or item['points'].get('groupFields'), f'{indent}\t\t')
- pt_order = item['points'].get('order') or ['Auto']
- emit_order(lines, pt_order, f'{indent}\t\t')
- pt_sel = item['points'].get('selection') or ['Auto']
- emit_selection(lines, pt_sel, f'{indent}\t\t')
+ emit_table_axis_block(lines, item['points'], f'{indent}\t\t')
lines.append(f'{indent}\t')
# Series
if item.get('series'):
lines.append(f'{indent}\t')
- emit_group_items(lines, item['series'].get('groupBy') or item['series'].get('groupFields'), f'{indent}\t\t')
- sr_order = item['series'].get('order') or ['Auto']
- emit_order(lines, sr_order, f'{indent}\t\t')
- sr_sel = item['series'].get('selection') or ['Auto']
- emit_selection(lines, sr_sel, f'{indent}\t\t')
+ emit_table_axis_block(lines, item['series'], f'{indent}\t\t')
lines.append(f'{indent}\t')
# Selection (chart values)
@@ -2108,6 +2154,10 @@ def emit_settings_variants(lines, defn):
return str(s[prop])
return None
+ # userFields — пользовательские вычисляемые поля (Expression / Case)
+ if s.get('userFields'):
+ emit_user_fields(lines, s['userFields'], '\t\t\t')
+
# Selection
if s.get('selection'):
emit_selection(lines, s['selection'], '\t\t\t', skip_auto=True, block_view_mode=_block_vm('selection'))
diff --git a/docs/skd-dsl-spec.md b/docs/skd-dsl-spec.md
index 2c58851b..6332fab1 100644
--- a/docs/skd-dsl-spec.md
+++ b/docs/skd-dsl-spec.md
@@ -494,6 +494,7 @@ XML-маппинг — по `` на каждый элемент:
"name": "Основной",
"presentation": "Основной вариант",
"settings": {
+ "userFields": [...],
"selection": [...],
"filter": [...],
"order": [...],
@@ -780,22 +781,95 @@ XML-маппинг — по `` на каждый элемент:
{ "groupBy": ["Номенклатура"], "selection": ["Auto"], "order": ["Auto"] }
],
"columns": [
- { "groupBy": ["Период"], "selection": ["Auto"], "order": ["Auto"] }
+ {
+ "groupBy": ["Период"],
+ "filter": ["Сумма > 0"],
+ "selection": ["Auto"],
+ "order": ["Auto"],
+ "outputParameters": { "РасположениеИтогов": "None" }
+ }
]
}
```
+Каждая `column`/`row` принимает те же поля что и `group`: `groupBy`/`groupFields`, `filter`, `order`, `selection`, `outputParameters`.
+
#### Диаграмма (chart)
```json
{
"type": "chart",
- "points": { "groupBy": ["Организация"], "order": ["Auto"] },
+ "points": { "groupBy": ["Организация"], "order": ["Auto"], "filter": [...] },
"series": { "groupBy": ["Месяц"], "order": ["Auto"] },
"selection": ["Сумма"]
}
```
+`points` и `series` принимают те же поля что table column/row.
+
+### userFields (пользовательские вычисляемые поля)
+
+Дополнительные поля, которые пользователь может задать в режиме «Изменить вариант» через UI. Хранятся в settings варианта. Два подтипа определяются по содержимому объекта:
+
+**Expression-форма** — поле вычисляется выражением (опционально с разделением для детальных строк и для итогов):
+
+```json
+"userFields": [
+ {
+ "dataPath": "ПользовательскиеПоля.Поле1",
+ "title": { "ru": "Отработано дней", "en": "Days worked" },
+ "detail": {
+ "expression": "Выбор Когда Группа = ... Тогда ОтработаноДней Иначе 0 Конец",
+ "presentation": "Выбор Когда Группа = ... Тогда [Отработано дней] Иначе 0 Конец"
+ },
+ "total": {
+ "expression": "Сумма(Выбор Когда Группа = ... Тогда ОтработаноДней Иначе 0 Конец)",
+ "presentation": "Сумма(Выбор Когда Группа = ... Тогда [Отработано дней] Иначе 0 Конец)"
+ }
+ }
+]
+```
+
+| Поле | Описание |
+|------|----------|
+| `dataPath` | Путь поля в формате `ПользовательскиеПоля.ПолеN` |
+| `title` | Заголовок (строка или multilang dict) |
+| `detail.expression` | Выражение для детальных записей |
+| `detail.presentation` | Тот же expression с подстановкой `[Имя поля]` (для UI) |
+| `total.expression` | Выражение для итоговой строки |
+| `total.presentation` | Same для UI |
+
+**Case-форма** — поле принимает разные значения в зависимости от условий:
+
+```json
+"userFields": [
+ {
+ "dataPath": "ПользовательскиеПоля.Поле1",
+ "title": { "ru": "Вид продаж" },
+ "cases": [
+ {
+ "filter": ["ХозОперация <> Перечисление.ХозяйственныеОперации.РеализацияВРозницу"],
+ "value": 2,
+ "presentation": { "ru": "Только оптовые продажи", "en": "Wholesale only" }
+ },
+ {
+ "filter": ["ХозОперация = Перечисление.ХозяйственныеОперации.РеализацияВРозницу"],
+ "value": 3,
+ "presentation": { "ru": "Только розничные продажи", "en": "Retail only" }
+ }
+ ]
+ }
+]
+```
+
+| Поле | Описание |
+|------|----------|
+| `cases[].filter` | Условие (как в settings filter) |
+| `cases[].value` | Значение поля если условие выполнено (типы автоопределяются: bool/decimal/string) |
+| `cases[].presentation` | Текст значения для UI (multilang) |
+
+Тип элемента определяется автоматически: наличие `cases` → `UserFieldCase`, иначе → `UserFieldExpression`.
+
### viewMode (режим доступности)
`viewMode` управляет доступностью элемента в **пользовательских настройках** отчёта («Изменить вариант…» / «Настройки»). Возможные значения: