feat(form-decompile,form-compile): Planner design-time Settings из ring-3 (Фаза 1 кластера Chart-Settings)

Реквизит planner-типа несёт <Settings xsi:type="pl:Planner"> — встроенный конфиг
поля-планировщика (элементы расписания + оформление/поведение + шкала времени).
Раньше декомпилятор делал fail-ring3 (третий вид Settings после TypeDescription/
DynamicList). Корпус 8.3.24: Planner Settings = 1 реальная форма (КонтактныеЛица/
ФормаЛиды), всё chart-семейство = 38 форм. Решение (с пользователем): структурный
DSL ради возможности модели СОЗДАВАТЬ дашборды/планировщики, не только раундтрипить.

DSL: ключ planner:{…} на реквизите (docs/form-dsl-spec.md):
- items[] (элементы расписания) + appearance/поведение-скаляры + timeScale
  (placement/levels[]/colors) + period;
- цвета verbatim, шрифт {kind:AutoFont}/ref, граница {width,style}, ML-форматы;
- компилятор подставляет дефолты для пропущенных ключей (краткий авторинг),
  декомпилятор — полный захват (раундтрип бит-в-бит).

Снят fail-ring3 для pl:Planner (Chart/GanttChart остаются — Фазы 2/3).
Заодно фикс: d5p1:Dendrogram отсутствовал в specialTypeNs (эмитился без
xmlns-префикса) — добавлен в карту (ps1+py).

Раундтрип бит-в-бит: синтетика upload/epf/Диаграммы (с items+period) +
реальная ФормаЛиды (без items/period, иные значения скаляров). Зеркало py
(ps1==py байт-в-байт). Кейс chart-fields расширен (+planner +dendrogram),
сертифицирован загрузкой в 1С. Регресс 41/41 (ps1+py).

Ограничение Phase 1: dimensions/item.dimensionValues пока всегда пустые.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Nick Shirokov
2026-06-10 13:35:10 +03:00
co-authored by Claude Opus 4.8
parent 29f288fe1d
commit f064d53eb2
6 changed files with 664 additions and 19 deletions
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
# form-compile v1.103 — Compile 1C managed form from JSON or object metadata
# form-compile v1.104 — Compile 1C managed form from JSON or object metadata
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
import copy
@@ -2714,6 +2714,181 @@ def emit_border_tag(lines, val, indent):
lines.append(f'{indent}</Border>')
# ─────────────────────────────────────────────────────────────────────────────
# Planner design-time <Settings xsi:type="pl:Planner"> — зеркало Emit-PlannerSettings (ps1).
PLANNER_NS = 'http://v8.1c.ru/8.3/data/planner'
CHART_NS = 'http://v8.1c.ru/8.2/data/chart'
def _pl_get(o, k, default=None):
if isinstance(o, dict) and o.get(k) is not None:
return o[k]
return default
def _pl_bool(v):
if isinstance(v, bool):
return 'true' if v else 'false'
if str(v) == 'True':
return 'true'
if str(v) == 'False':
return 'false'
return str(v)
def emit_planner_color(lines, tag, o, key, ind):
lines.append(f'{ind}<pl:{tag}>{esc_xml(str(_pl_get(o, key, "auto")))}</pl:{tag}>')
def emit_planner_font(lines, o, ind):
f = _pl_get(o, 'font')
if f is None:
lines.append(f'{ind}<pl:font kind="AutoFont"/>')
return
emit_font_tag(lines, 'pl:font', f, ind)
def emit_planner_border(lines, o, ind, key='border'):
b = _pl_get(o, key)
bw = _pl_get(b, 'width', 1) if b else 1
bs = _pl_get(b, 'style', 'Single') if b else 'Single'
lines.append(f'{ind}<pl:border width="{bw}">')
lines.append(f'{ind}\t<v8ui:style xsi:type="v8ui:ControlBorderType">{esc_xml(str(bs))}</v8ui:style>')
lines.append(f'{ind}</pl:border>')
def emit_planner_level(lines, lv, cns, ind):
li = f'{ind}\t'
lines.append(f'{ind}<level xmlns="{cns}">')
lines.append(f'{li}<measure>{esc_xml(str(_pl_get(lv, "measure", "Hour")))}</measure>')
lines.append(f'{li}<interval>{_pl_get(lv, "interval", 1)}</interval>')
lines.append(f'{li}<show>{_pl_bool(_pl_get(lv, "show", True))}</show>')
line = _pl_get(lv, 'line')
lw = _pl_get(line, 'width', 1) if line else 1
lg = _pl_get(line, 'gap', False) if line else False
lst = _pl_get(line, 'style', 'Solid') if line else 'Solid'
lines.append(f'{li}<line width="{lw}" gap="{_pl_bool(lg)}">')
lines.append(f'{li}\t<v8ui:style xsi:type="v8ui:ChartLineType">{esc_xml(str(lst))}</v8ui:style>')
lines.append(f'{li}</line>')
lines.append(f'{li}<scaleColor>{esc_xml(str(_pl_get(lv, "scaleColor", "auto")))}</scaleColor>')
lines.append(f'{li}<dayFormatRule>{esc_xml(str(_pl_get(lv, "dayFormatRule", "MonthDayWeekDay")))}</dayFormatRule>')
fmt = _pl_get(lv, 'format')
if fmt is None:
fmt = {'#': 'DF="HH:mm"', 'ru': 'DF="HH:mm"'}
lines.append(f'{li}<format>')
emit_ml_items(lines, f'{li}\t', fmt)
lines.append(f'{li}</format>')
labels = _pl_get(lv, 'labels')
ticks = _pl_get(labels, 'ticks', 0) if labels else 0
lines.append(f'{li}<labels>')
lines.append(f'{li}\t<ticks>{ticks}</ticks>')
lines.append(f'{li}</labels>')
lines.append(f'{li}<backColor>{esc_xml(str(_pl_get(lv, "backColor", "auto")))}</backColor>')
lines.append(f'{li}<textColor>{esc_xml(str(_pl_get(lv, "textColor", "auto")))}</textColor>')
lines.append(f'{li}<showPereodicalLabels>{_pl_bool(_pl_get(lv, "showPereodicalLabels", True))}</showPereodicalLabels>')
lines.append(f'{ind}</level>')
def emit_planner_timescale(lines, ts, ind):
cns = CHART_NS
ci = f'{ind}\t'
lines.append(f'{ind}<pl:timeScale>')
placement = _pl_get(ts, 'placement', 'Left') if ts else 'Left'
lines.append(f'{ci}<placement xmlns="{cns}">{esc_xml(str(placement))}</placement>')
levels = _pl_get(ts, 'levels', []) if ts else []
if not levels:
levels = [None]
for lv in levels:
emit_planner_level(lines, lv, cns, ci)
transp = _pl_get(ts, 'transparent', False) if ts else False
lines.append(f'{ci}<transparent xmlns="{cns}">{_pl_bool(transp)}</transparent>')
tbc = _pl_get(ts, 'backColor', 'auto') if ts else 'auto'
ttc = _pl_get(ts, 'textColor', 'auto') if ts else 'auto'
tcl = _pl_get(ts, 'currentLevel', 0) if ts else 0
lines.append(f'{ci}<backColor xmlns="{cns}">{esc_xml(str(tbc))}</backColor>')
lines.append(f'{ci}<textColor xmlns="{cns}">{esc_xml(str(ttc))}</textColor>')
lines.append(f'{ci}<currentLevel xmlns="{cns}">{tcl}</currentLevel>')
lines.append(f'{ind}</pl:timeScale>')
def emit_planner_item(lines, it, ind):
lines.append(f'{ind}<pl:item>')
ii = f'{ind}\t'
val = _pl_get(it, 'value')
if val is None:
lines.append(f'{ii}<pl:value xsi:nil="true"/>')
else:
lines.append(f'{ii}<pl:value xsi:type="xs:string">{esc_xml(str(val))}</pl:value>')
lines.append(f'{ii}<pl:text>{esc_xml(str(_pl_get(it, "text", "")))}</pl:text>')
tt = _pl_get(it, 'tooltip', '')
if str(tt) == '':
lines.append(f'{ii}<pl:tooltip/>')
else:
lines.append(f'{ii}<pl:tooltip>{esc_xml(str(tt))}</pl:tooltip>')
lines.append(f'{ii}<pl:begin>{_pl_get(it, "begin", "0001-01-01T00:00:00")}</pl:begin>')
lines.append(f'{ii}<pl:end>{_pl_get(it, "end", "0001-01-01T00:00:00")}</pl:end>')
emit_planner_color(lines, 'borderColor', it, 'borderColor', ii)
emit_planner_color(lines, 'backColor', it, 'backColor', ii)
emit_planner_color(lines, 'textColor', it, 'textColor', ii)
emit_planner_font(lines, it, ii)
lines.append(f'{ii}<pl:dimensionValues/>')
lines.append(f'{ii}<pl:replacementDate>{_pl_get(it, "replacementDate", "0001-01-01T00:00:00")}</pl:replacementDate>')
lines.append(f'{ii}<pl:deleted>{_pl_bool(_pl_get(it, "deleted", False))}</pl:deleted>')
iid = _pl_get(it, 'id')
if iid is None:
import uuid
iid = str(uuid.uuid4())
lines.append(f'{ii}<pl:id>{iid}</pl:id>')
lines.append(f'{ii}<pl:textFormatted>{_pl_bool(_pl_get(it, "textFormatted", False))}</pl:textFormatted>')
emit_planner_border(lines, it, ii, 'border')
lines.append(f'{ii}<pl:editMode>{esc_xml(str(_pl_get(it, "editMode", "EnableEdit")))}</pl:editMode>')
lines.append(f'{ind}</pl:item>')
def emit_planner_settings(lines, pl, ind):
lines.append(f'{ind}<Settings xmlns:pl="{PLANNER_NS}" xsi:type="pl:Planner">')
si = f'{ind}\t'
for it in _pl_get(pl, 'items', []):
emit_planner_item(lines, it, si)
emit_planner_color(lines, 'borderColor', pl, 'borderColor', si)
emit_planner_color(lines, 'backColor', pl, 'backColor', si)
emit_planner_color(lines, 'textColor', pl, 'textColor', si)
emit_planner_color(lines, 'lineColor', pl, 'lineColor', si)
emit_planner_font(lines, pl, si)
lines.append(f'{si}<pl:beginOfRepresentationPeriod>{_pl_get(pl, "beginOfRepresentationPeriod", "0001-01-01T00:00:00")}</pl:beginOfRepresentationPeriod>')
lines.append(f'{si}<pl:endOfRepresentationPeriod>{_pl_get(pl, "endOfRepresentationPeriod", "0001-01-01T00:00:00")}</pl:endOfRepresentationPeriod>')
lines.append(f'{si}<pl:alignElementsOfTimeScale>{_pl_bool(_pl_get(pl, "alignElementsOfTimeScale", True))}</pl:alignElementsOfTimeScale>')
lines.append(f'{si}<pl:displayTimeScaleWrapHeaders>{_pl_bool(_pl_get(pl, "displayTimeScaleWrapHeaders", True))}</pl:displayTimeScaleWrapHeaders>')
lines.append(f'{si}<pl:displayWrapHeaders>{_pl_bool(_pl_get(pl, "displayWrapHeaders", True))}</pl:displayWrapHeaders>')
wfmt = _pl_get(pl, 'timeScaleWrapHeadersFormat')
if wfmt is None:
wfmt = {'#': 'DLF="DD"', 'ru': 'DLF="DD"'}
emit_mltext(lines, si, 'pl:timeScaleWrapHeadersFormat', wfmt)
lines.append(f'{si}<pl:periodicVariantUnit>{esc_xml(str(_pl_get(pl, "periodicVariantUnit", "Day")))}</pl:periodicVariantUnit>')
lines.append(f'{si}<pl:periodicVariantRepetition>{_pl_get(pl, "periodicVariantRepetition", 1)}</pl:periodicVariantRepetition>')
lines.append(f'{si}<pl:timeScaleWrapBeginIndent>{_pl_get(pl, "timeScaleWrapBeginIndent", 0)}</pl:timeScaleWrapBeginIndent>')
lines.append(f'{si}<pl:timeScaleWrapEndIndent>{_pl_get(pl, "timeScaleWrapEndIndent", 0)}</pl:timeScaleWrapEndIndent>')
emit_planner_timescale(lines, _pl_get(pl, 'timeScale'), si)
period = _pl_get(pl, 'period')
if period:
lines.append(f'{si}<pl:period>')
lines.append(f'{si}\t<pl:begin>{_pl_get(period, "begin", "0001-01-01T00:00:00")}</pl:begin>')
lines.append(f'{si}\t<pl:end>{_pl_get(period, "end", "0001-01-01T00:00:00")}</pl:end>')
lines.append(f'{si}</pl:period>')
lines.append(f'{si}<pl:displayCurrentDate>{_pl_bool(_pl_get(pl, "displayCurrentDate", True))}</pl:displayCurrentDate>')
lines.append(f'{si}<pl:itemsTimeRepresentation>{esc_xml(str(_pl_get(pl, "itemsTimeRepresentation", "BeginTime")))}</pl:itemsTimeRepresentation>')
lines.append(f'{si}<pl:itemsBehaviorWhenSpaceInsufficient>{esc_xml(str(_pl_get(pl, "itemsBehaviorWhenSpaceInsufficient", "CollapseItems")))}</pl:itemsBehaviorWhenSpaceInsufficient>')
lines.append(f'{si}<pl:autoMinColumnWidth>{_pl_bool(_pl_get(pl, "autoMinColumnWidth", True))}</pl:autoMinColumnWidth>')
lines.append(f'{si}<pl:autoMinRowHeight>{_pl_bool(_pl_get(pl, "autoMinRowHeight", True))}</pl:autoMinRowHeight>')
lines.append(f'{si}<pl:minColumnWidth>{_pl_get(pl, "minColumnWidth", 0)}</pl:minColumnWidth>')
lines.append(f'{si}<pl:minRowHeight>{_pl_get(pl, "minRowHeight", 0)}</pl:minRowHeight>')
lines.append(f'{si}<pl:fixDimensionsHeader>{esc_xml(str(_pl_get(pl, "fixDimensionsHeader", "auto")))}</pl:fixDimensionsHeader>')
lines.append(f'{si}<pl:fixTimeScaleHeader>{esc_xml(str(_pl_get(pl, "fixTimeScaleHeader", "auto")))}</pl:fixTimeScaleHeader>')
emit_planner_border(lines, pl, si, 'border')
lines.append(f'{si}<pl:newItemsTextType>{esc_xml(str(_pl_get(pl, "newItemsTextType", "String")))}</pl:newItemsTextType>')
lines.append(f'{ind}</Settings>')
def emit_appearance(lines, el, indent, profile='field'):
if not isinstance(el, dict):
return
@@ -3063,6 +3238,7 @@ def emit_single_type(lines, type_str, indent):
"d5p1:TextDocument": "http://v8.1c.ru/8.1/data/txtedt",
"d5p1:Chart": "http://v8.1c.ru/8.2/data/chart",
"d5p1:GanttChart": "http://v8.1c.ru/8.2/data/chart",
"d5p1:Dendrogram": "http://v8.1c.ru/8.2/data/chart",
"d5p1:FlowchartContextType": "http://v8.1c.ru/8.2/data/graphscheme",
"d5p1:DataAnalysisTimeIntervalUnitType": "http://v8.1c.ru/8.2/data/data-analysis",
"d5p1:GeographicalSchema": "http://v8.1c.ru/8.2/data/geo",
@@ -4480,6 +4656,9 @@ def emit_attributes(lines, attrs, indent, conditional_appearance=None):
break
if has_vt:
emit_type(lines, '' if vt_spec is None else str(vt_spec), inner, tag="Settings", tag_attrs=' xsi:type="v8:TypeDescription"')
# Planner design-time <Settings xsi:type="pl:Planner"> (встроенный конфиг планировщика).
if attr.get('planner') is not None:
emit_planner_settings(lines, attr['planner'], inner)
if attr.get('main') is True:
lines.append(f'{inner}<MainAttribute>true</MainAttribute>')