Files
cc-1c-skills/.claude/skills/skd-compile/scripts/skd-compile.py
T
Nick Shirokov 6d119eb473 feat(skd-edit): значение-список параметра в шортхенде (+skd-compile)
Значение по умолчанию у параметра СКД может быть списком (несколько <value>
подряд при valueListAllowed=true). Раньше задать список можно было только через
объектную модель skd-compile; шортхенд (add/modify-parameter, parameters) парсил
value= как скаляр.

Теперь в шортхенде: value=v1, v2, v3 задаёт список (кавычки '...' для запятой
внутри значения). Если задан список (>=2 элементов), valueListAllowed выводится
автоматически. Авто-вывод только в шортхенде — объектная модель остаётся
буквальной (bit-perfect round-trip сохранён).

skd-edit (ps1+py v1.25):
- Split-QuotedCsv/Parse-ValueList — токенайзер по запятым с учётом кавычек, БЕЗ
  разреза по ':' (важно для дат вида 2024-01-01T12:30:45)
- add-parameter: эмит N <value>
- modify-parameter: пред-выемка value=-списка, удаление ВСЕХ старых <value>,
  авто valueListAllowed; scalar value= теперь тоже схлопывает список в один <value>

skd-compile (ps1+py v1.105): тот же разбор списка в Parse-ParamShorthand;
объектная модель не тронута.

Документация: skd-edit/skd-compile SKILL.md (поведение), docs/1c-dcs-spec.md и
docs/skd-dsl-spec.md (формат).

Тесты: add-list, modify list<->scalar, список дат (двоеточия целы), compile-
шортхенд. Полный регресс 413/413 на ps1 и py.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 12:26:57 +03:00

2907 lines
140 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
# skd-compile v1.105 — Compile 1C DCS from JSON
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
import json
import os
import re
import sys
import uuid
def esc_xml(s):
return s.replace('&', '&amp;').replace('<', '&lt;').replace('>', '&gt;')
def fmt_dec(v):
"""Format decimal: 30.0 → '30', 16.625 → '16.625' (match PS1 output)."""
return str(int(v)) if v == int(v) else str(v)
def resolve_query_value(val, base_dir):
if not val.startswith("@"):
return val
file_path = val[1:]
if os.path.isabs(file_path):
candidates = [file_path]
else:
candidates = [
os.path.join(base_dir, file_path),
os.path.join(os.getcwd(), file_path),
]
for c in candidates:
if os.path.exists(c):
with open(c, 'r', encoding='utf-8-sig') as f:
return f.read().rstrip()
print(f"Query file not found: {file_path} (searched: {', '.join(candidates)})", file=sys.stderr)
sys.exit(1)
def emit_mltext(lines, indent, tag, text, no_xsi_type=False):
# Empty value → self-closing tag (matches platform output)
if text is None or (isinstance(text, str) and text == ''):
if no_xsi_type:
lines.append(f"{indent}<{tag}/>")
else:
lines.append(f'{indent}<{tag} xsi:type="v8:LocalStringType"/>')
return
if not text:
lines.append(f"{indent}<{tag}/>")
return
if no_xsi_type:
lines.append(f"{indent}<{tag}>")
else:
lines.append(f'{indent}<{tag} xsi:type="v8:LocalStringType">')
# Multi-lang: object form { ru: "...", en: "..." } -- one <v8:item> per language
if isinstance(text, dict):
for lang, content in text.items():
lines.append(f"{indent}\t<v8:item>")
lines.append(f"{indent}\t\t<v8:lang>{esc_xml(str(lang))}</v8:lang>")
lines.append(f"{indent}\t\t<v8:content>{esc_xml(str(content))}</v8:content>")
lines.append(f"{indent}\t</v8:item>")
else:
lines.append(f"{indent}\t<v8:item>")
lines.append(f"{indent}\t\t<v8:lang>ru</v8:lang>")
lines.append(f"{indent}\t\t<v8:content>{esc_xml(str(text))}</v8:content>")
lines.append(f"{indent}\t</v8:item>")
lines.append(f"{indent}</{tag}>")
def new_uuid():
return str(uuid.uuid4())
def write_utf8_bom(path, content):
with open(path, 'w', encoding='utf-8-sig', newline='') as f:
f.write(content)
# --- Type system ---
TYPE_SYNONYMS = {
# Russian names (lowercase)
"\u0447\u0438\u0441\u043b\u043e": "decimal",
"\u0441\u0442\u0440\u043e\u043a\u0430": "string",
"\u0431\u0443\u043b\u0435\u0432\u043e": "boolean",
"\u0434\u0430\u0442\u0430": "date",
"\u0434\u0430\u0442\u0430\u0432\u0440\u0435\u043c\u044f": "dateTime",
"\u0432\u0440\u0435\u043c\u044f": "time",
"\u0441\u0442\u0430\u043d\u0434\u0430\u0440\u0442\u043d\u044b\u0439\u043f\u0435\u0440\u0438\u043e\u0434": "StandardPeriod",
# English canonical (lowercase)
"bool": "boolean",
"str": "string",
"int": "decimal",
"integer": "decimal",
"number": "decimal",
"num": "decimal",
# Reference synonyms (Russian, lowercase)
"\u0441\u043f\u0440\u0430\u0432\u043e\u0447\u043d\u0438\u043a\u0441\u0441\u044b\u043b\u043a\u0430": "CatalogRef",
"\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0441\u0441\u044b\u043b\u043a\u0430": "DocumentRef",
"\u043f\u0435\u0440\u0435\u0447\u0438\u0441\u043b\u0435\u043d\u0438\u0435\u0441\u0441\u044b\u043b\u043a\u0430": "EnumRef",
"\u043f\u043b\u0430\u043d\u0441\u0447\u0435\u0442\u043e\u0432\u0441\u0441\u044b\u043b\u043a\u0430": "ChartOfAccountsRef",
"\u043f\u043b\u0430\u043d\u0432\u0438\u0434\u043e\u0432\u0445\u0430\u0440\u0430\u043a\u0442\u0435\u0440\u0438\u0441\u0442\u0438\u043a\u0441\u0441\u044b\u043b\u043a\u0430": "ChartOfCharacteristicTypesRef",
}
def resolve_type_str(type_str):
if not type_str:
return type_str
# Check for parameterized types: число(15,2), строка(100), etc.
m = re.match(r'^([^(]+)\((.+)\)$', type_str)
if m:
base_name = m.group(1).strip()
params = m.group(2)
resolved = TYPE_SYNONYMS.get(base_name.lower())
if resolved:
return f"{resolved}({params})"
return type_str
# Check for reference types: СправочникСсылка.Организации -> CatalogRef.Организации
if '.' in type_str:
dot_idx = type_str.index('.')
prefix = type_str[:dot_idx]
suffix = type_str[dot_idx:] # includes the dot
resolved = TYPE_SYNONYMS.get(prefix.lower())
if resolved:
return f"{resolved}{suffix}"
return type_str
# Simple name lookup (case-insensitive)
resolved = TYPE_SYNONYMS.get(type_str.lower())
if resolved:
return resolved
return type_str
def emit_value_type(lines, type_spec, indent):
if not type_spec:
return
# Multi-type: iterate and emit each type with its qualifiers
if isinstance(type_spec, list):
for t in type_spec:
emit_single_value_type(lines, str(t), indent)
return
emit_single_value_type(lines, str(type_spec), indent)
def emit_single_value_type(lines, type_str, indent):
if not type_str:
return
# Resolve synonyms first
type_str = resolve_type_str(type_str)
# boolean
if type_str == 'boolean':
lines.append(f'{indent}<v8:Type>xs:boolean</v8:Type>')
return
# string, string(N), string(N,fix) — fix → AllowedLength=Fixed
m = re.match(r'^string(\((\d+)(,(fix|fixed))?\))?$', type_str)
if m:
length = m.group(2) if m.group(2) else '0'
al = 'Fixed' if m.group(4) else 'Variable'
lines.append(f'{indent}<v8:Type>xs:string</v8:Type>')
lines.append(f'{indent}<v8:StringQualifiers>')
lines.append(f'{indent}\t<v8:Length>{length}</v8:Length>')
lines.append(f'{indent}\t<v8:AllowedLength>{al}</v8:AllowedLength>')
lines.append(f'{indent}</v8:StringQualifiers>')
return
# decimal forms (defaults — bare decimal = money 10,2; decimal(N) = integer N,0):
# decimal → 10,2,Any
# decimal(N) → N,0,Any
# decimal(N,nonneg) → N,0,Nonnegative
# decimal(N,M) → N,M,Any
# decimal(N,M,nonneg) → N,M,Nonnegative
m = re.match(r'^decimal(\((\d+)(,(\d+))?(,nonneg)?\))?$', type_str)
if m:
if not m.group(1):
digits, fraction, sign = '10', '2', 'Any'
else:
digits = m.group(2)
fraction = m.group(4) if m.group(4) else '0'
sign = 'Nonnegative' if m.group(5) else 'Any'
lines.append(f'{indent}<v8:Type>xs:decimal</v8:Type>')
lines.append(f'{indent}<v8:NumberQualifiers>')
lines.append(f'{indent}\t<v8:Digits>{digits}</v8:Digits>')
lines.append(f'{indent}\t<v8:FractionDigits>{fraction}</v8:FractionDigits>')
lines.append(f'{indent}\t<v8:AllowedSign>{sign}</v8:AllowedSign>')
lines.append(f'{indent}</v8:NumberQualifiers>')
return
# date / dateTime / time — all use xs:dateTime, differ only in DateFractions
m = re.match(r'^(date|dateTime|time)$', type_str)
if m:
fractions_map = {'date': 'Date', 'dateTime': 'DateTime', 'time': 'Time'}
fractions = fractions_map[type_str]
lines.append(f'{indent}<v8:Type>xs:dateTime</v8:Type>')
lines.append(f'{indent}<v8:DateQualifiers>')
lines.append(f'{indent}\t<v8:DateFractions>{fractions}</v8:DateFractions>')
lines.append(f'{indent}</v8:DateQualifiers>')
return
# StandardPeriod
if type_str == 'StandardPeriod':
lines.append(f'{indent}<v8:Type>v8:StandardPeriod</v8:Type>')
return
# Reference types: CatalogRef.XXX, DocumentRef.XXX, EnumRef.XXX, etc.
if re.match(r'^(CatalogRef|DocumentRef|EnumRef|ChartOfAccountsRef|ChartOfCharacteristicTypesRef)\.', type_str):
lines.append(f'{indent}<v8:Type xmlns:d5p1="http://v8.1c.ru/8.1/data/enterprise/current-config">d5p1:{esc_xml(type_str)}</v8:Type>')
return
# TypeSet (композитный тип-набор): голое имя без точки.
if re.match(r'^(CatalogRef|DocumentRef|EnumRef|ChartOfAccountsRef|ChartOfCharacteristicTypesRef|ChartOfCalculationTypesRef|BusinessProcessRef|TaskRef|ExchangePlanRef|InformationRegisterRef|AnyRef)$', type_str):
lines.append(f'{indent}<v8:TypeSet xmlns:d5p1="http://v8.1c.ru/8.1/data/enterprise/current-config">d5p1:{esc_xml(type_str)}</v8:TypeSet>')
return
# Fallback -- assume dot-qualified types are also config references
if '.' in type_str:
lines.append(f'{indent}<v8:Type xmlns:d5p1="http://v8.1c.ru/8.1/data/enterprise/current-config">d5p1:{esc_xml(type_str)}</v8:Type>')
return
lines.append(f'{indent}<v8:Type>{esc_xml(type_str)}</v8:Type>')
# --- Field shorthand parser ---
def parse_field_shorthand(s):
result = {
'dataPath': '', 'field': '', 'title': '', 'type': '',
'roles': [], 'restrict': [], 'appearance': {},
'roleExtras': {},
}
# Extract @roles
role_matches = re.findall(r'@(\w+)', s)
for m in role_matches:
result['roles'].append(m)
s = re.sub(r'\s*@\w+', '', s)
# Extract #restrictions
restrict_matches = re.findall(r'#(\w+)', s)
for m in restrict_matches:
result['restrict'].append(m)
s = re.sub(r'\s*#\w+', '', s)
# Extract role kv=value (e.g. balanceGroupName=Сумма)
for m in re.finditer(r'(\w+)=(\S+)', s):
result['roleExtras'][m.group(1)] = m.group(2)
s = re.sub(r'\s*\w+=\S+', '', s)
# Split name: type
s = s.strip()
if ':' in s:
parts = s.split(':', 1)
result['dataPath'] = parts[0].strip()
result['type'] = resolve_type_str(parts[1].strip())
else:
result['dataPath'] = s
result['field'] = result['dataPath']
return result
# Universal role spec parser: string / list / dict / None
# Returns {'tokens': [...], 'extras': {...}}
def parse_role_spec(spec):
tokens = []
extras = {}
if spec is None:
pass
elif isinstance(spec, str):
if ' ' not in spec and '=' not in spec:
tokens.append(spec)
else:
s = spec.strip()
for m in re.finditer(r'@(\w+)', s):
tokens.append(m.group(1))
s = re.sub(r'\s*@\w+', '', s).strip()
for m in re.finditer(r'(\w+)=(\S+)', s):
extras[m.group(1)] = m.group(2)
elif isinstance(spec, list):
for t in spec:
tokens.append(str(t))
elif isinstance(spec, dict):
for k, v in spec.items():
if isinstance(v, bool):
if v:
tokens.append(k)
elif isinstance(v, (int, float, str)):
extras[k] = str(v)
# Deprecated alias: balanceGroup → balanceGroupName
if 'balanceGroup' in extras and 'balanceGroupName' not in extras:
extras['balanceGroupName'] = extras.pop('balanceGroup')
return {'tokens': tokens, 'extras': extras}
# --- Total field shorthand parser ---
def parse_total_shorthand(s):
parts = s.split(':', 1)
data_path = parts[0].strip()
func_part = parts[1].strip()
# Known DCS aggregate functions (ru + en)
_agg_funcs = {'Сумма','Количество','Минимум','Максимум','Среднее',
'Sum','Count','Min','Max','Avg',
'Minimum','Maximum','Average'}
if re.match(r'^\w+\(', func_part):
return {'dataPath': data_path, 'expression': func_part}
elif func_part in _agg_funcs:
return {'dataPath': data_path, 'expression': f'{func_part}({data_path})'}
else:
# Identity or custom expression — use as-is
return {'dataPath': data_path, 'expression': func_part}
# --- Parameter shorthand parser ---
def split_value_list_csv(s):
"""Split on top-level commas (respecting single/double quotes), strip quotes,
drop empties. No ':' handling — values may contain colons (dateTime)."""
result = []
if s is None:
return result
items = []
buf = []
in_quote = None
for ch in s:
if in_quote:
buf.append(ch)
if ch == in_quote:
in_quote = None
elif ch in ("'", '"'):
in_quote = ch
buf.append(ch)
elif ch == ',':
items.append("".join(buf))
buf = []
else:
buf.append(ch)
if buf:
items.append("".join(buf))
for raw in items:
t = raw.strip()
if len(t) >= 2 and ((t[0] == "'" and t[-1] == "'") or (t[0] == '"' and t[-1] == '"')):
t = t[1:-1]
if t != "":
result.append(t)
return result
def parse_param_shorthand(s):
result = {'name': '', 'type': '', 'value': None, 'autoDates': False, 'title': None}
# Extract @autoDates flag
if '@autoDates' in s:
result['autoDates'] = True
s = re.sub(r'\s*@autoDates', '', s)
# Extract @valueList flag
if '@valueList' in s:
result['valueListAllowed'] = True
s = re.sub(r'\s*@valueList', '', s)
# Extract @hidden flag
if '@hidden' in s:
result['hidden'] = True
s = re.sub(r'\s*@hidden', '', s)
# Extract optional [Title] (mirrors parse_field_shorthand)
m = re.search(r'\[([^\]]*)\]', s)
if m:
result['title'] = m.group(1).strip()
s = re.sub(r'\s*\[[^\]]*\]\s*', ' ', s).strip()
# Split "Name: Type = Value" — RHS may be empty (`= ` / `=`) → treated as empty value
m = re.match(r'^([^:]+):\s*(\S+)(\s*=\s*(.*))?$', s)
if m:
result['name'] = m.group(1).strip()
result['type'] = resolve_type_str(m.group(2).strip())
if m.group(4):
rhs = m.group(4).strip()
items = split_value_list_csv(rhs)
if len(items) >= 2:
# Multi-value default → list; valueListAllowed implied
result['value'] = items
result['valueListAllowed'] = True
elif len(items) == 1:
result['value'] = items[0]
else:
result['value'] = rhs
else:
result['name'] = s.strip()
return result
# --- Calculated field shorthand parser ---
def parse_calc_shorthand(s):
# Pattern: "Name [Title]: type = Expression #noField #noFilter ...".
# - `[Title]` is extracted only from the LHS of '=' so that `[...]` inside
# an expression (e.g. index access) isn't interpreted as a title.
# - `#restrict` flags use a known-names pattern and are extracted globally —
# the docs put them after `=`, and the closed flag set avoids matching
# `#word` that happens to appear inside a string literal.
restrict_pattern = r'#(noField|noFilter|noCondition|noGroup|noOrder)\b'
restrict = re.findall(restrict_pattern, s)
s = re.sub(r'\s*' + restrict_pattern, '', s)
eq_idx = s.find('=')
if eq_idx > 0:
lhs = s[:eq_idx]
rhs = s[eq_idx + 1:].strip()
else:
lhs = s
rhs = ''
title = ''
m = re.search(r'\[([^\]]+)\]', lhs)
if m:
title = m.group(1)
lhs = re.sub(r'\s*\[[^\]]+\]', '', lhs)
lhs = lhs.strip()
type_str = ''
data_path = lhs
if ':' in lhs:
colon_idx = lhs.index(':')
data_path = lhs[:colon_idx].strip()
type_str = resolve_type_str(lhs[colon_idx + 1:].strip())
return {
'dataPath': data_path,
'expression': rhs,
'type': type_str,
'title': title,
'restrict': restrict,
}
# --- DataParameter shorthand parser ---
PERIOD_VARIANTS = [
"Custom", "Today", "ThisWeek", "ThisTenDays", "ThisMonth", "ThisQuarter",
"ThisHalfYear", "ThisYear", "FromBeginningOfThisWeek", "FromBeginningOfThisTenDays",
"FromBeginningOfThisMonth", "FromBeginningOfThisQuarter", "FromBeginningOfThisHalfYear",
"FromBeginningOfThisYear", "LastWeek", "LastTenDays", "LastMonth", "LastQuarter",
"LastHalfYear", "LastYear", "NextDay", "NextWeek", "NextTenDays", "NextMonth",
"NextQuarter", "NextHalfYear", "NextYear", "TillEndOfThisWeek", "TillEndOfThisTenDays",
"TillEndOfThisMonth", "TillEndOfThisQuarter", "TillEndOfThisHalfYear", "TillEndOfThisYear",
]
def parse_data_param_shorthand(s):
result = {'parameter': '', 'value': None, 'use': True, 'userSettingID': None, 'viewMode': None}
# Extract @flags
if '@user' in s:
result['userSettingID'] = 'auto'
s = re.sub(r'\s*@user', '', s)
if '@off' in s:
result['use'] = False
s = re.sub(r'\s*@off', '', s)
if '@quickAccess' in s:
result['viewMode'] = 'QuickAccess'
s = re.sub(r'\s*@quickAccess', '', s)
if '@normal' in s:
result['viewMode'] = 'Normal'
s = re.sub(r'\s*@normal', '', s)
s = s.strip()
# Split "Name = Value"
m = re.match(r'^([^=]+)=\s*(.+)$', s)
if m:
result['parameter'] = m.group(1).strip()
val_str = m.group(2).strip()
if val_str in PERIOD_VARIANTS:
result['value'] = {'variant': val_str}
elif re.match(r'^\d{4}-\d{2}-\d{2}T', val_str):
result['value'] = val_str
elif val_str == 'true' or val_str == 'false':
result['value'] = val_str == 'true'
else:
result['value'] = val_str
else:
result['parameter'] = s
return result
# --- Filter item shorthand parser ---
def parse_filter_shorthand(s):
result = {'field': '', 'op': 'Equal', 'value': None, 'use': True,
'userSettingID': None, 'viewMode': None, 'presentation': None}
# Extract @flags
if '@user' in s:
result['userSettingID'] = 'auto'
s = re.sub(r'\s*@user', '', s)
if '@off' in s:
result['use'] = False
s = re.sub(r'\s*@off', '', s)
if '@quickAccess' in s:
result['viewMode'] = 'QuickAccess'
s = re.sub(r'\s*@quickAccess', '', s)
if '@normal' in s:
result['viewMode'] = 'Normal'
s = re.sub(r'\s*@normal', '', s)
if '@inaccessible' in s:
result['viewMode'] = 'Inaccessible'
s = re.sub(r'\s*@inaccessible', '', s)
s = s.strip()
# Operators sorted longest first
op_patterns = [
'<>', '>=', '<=', '=', '>', '<',
r'notIn\b', r'in\b', r'inHierarchy\b', r'inListByHierarchy\b',
r'notContains\b', r'contains\b', r'notBeginsWith\b', r'beginsWith\b',
r'notFilled\b', r'filled\b',
]
op_joined = '|'.join(op_patterns)
m = re.match(rf'^(.+?)\s+({op_joined})\s*(.*)?$', s)
if m:
result['field'] = m.group(1).strip()
op_raw = m.group(2).strip()
val_part = m.group(3).strip() if m.group(3) else ''
# Parse value (skip "_" which means empty/placeholder)
if val_part and val_part != '_':
if val_part == 'true' or val_part == 'false':
result['value'] = val_part == 'true'
result['valueType'] = 'xs:boolean'
elif re.match(r'^\d{4}-\d{2}-\d{2}T', val_part):
result['value'] = val_part
result['valueType'] = 'xs:dateTime'
elif re.match(r'^\d+(\.\d+)?$', val_part):
result['value'] = val_part
result['valueType'] = 'xs:decimal'
elif re.match(r'^(Перечисление|Справочник|ПланСчетов|Документ|ПланВидовХарактеристик|ПланВидовРасчета)\.', val_part):
result['value'] = val_part
result['valueType'] = 'dcscor:DesignTimeValue'
else:
result['value'] = val_part
result['valueType'] = 'xs:string'
result['op'] = op_raw
else:
result['field'] = s
return result
# --- Comparison type mapper ---
COMPARISON_TYPES = {
'=': 'Equal', '<>': 'NotEqual',
'>': 'Greater', '>=': 'GreaterOrEqual',
'<': 'Less', '<=': 'LessOrEqual',
'in': 'InList', 'notIn': 'NotInList',
'inHierarchy': 'InHierarchy', 'inListByHierarchy': 'InListByHierarchy',
'contains': 'Contains', 'notContains': 'NotContains',
'beginsWith': 'BeginsWith', 'notBeginsWith': 'NotBeginsWith',
'filled': 'Filled', 'notFilled': 'NotFilled',
}
# --- Output parameter type detection ---
OUTPUT_PARAM_TYPES = {
"\u0417\u0430\u0433\u043e\u043b\u043e\u0432\u043e\u043a": "mltext",
"\u0412\u044b\u0432\u043e\u0434\u0438\u0442\u044c\u0417\u0430\u0433\u043e\u043b\u043e\u0432\u043e\u043a": "dcsset:DataCompositionTextOutputType",
"\u0412\u044b\u0432\u043e\u0434\u0438\u0442\u044c\u041f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b\u0414\u0430\u043d\u043d\u044b\u0445": "dcsset:DataCompositionTextOutputType",
"\u0412\u044b\u0432\u043e\u0434\u0438\u0442\u044c\u041e\u0442\u0431\u043e\u0440": "dcsset:DataCompositionTextOutputType",
"\u041c\u0430\u043a\u0435\u0442\u041e\u0444\u043e\u0440\u043c\u043b\u0435\u043d\u0438\u044f": "xs:string",
"\u0420\u0430\u0441\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0435\u041f\u043e\u043b\u0435\u0439\u0413\u0440\u0443\u043f\u043f\u0438\u0440\u043e\u0432\u043a\u0438": "dcsset:DataCompositionGroupFieldsPlacement",
"\u0420\u0430\u0441\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0435\u0420\u0435\u043a\u0432\u0438\u0437\u0438\u0442\u043e\u0432": "dcsset:DataCompositionAttributesPlacement",
"\u0413\u043e\u0440\u0438\u0437\u043e\u043d\u0442\u0430\u043b\u044c\u043d\u043e\u0435\u0420\u0430\u0441\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0435\u041e\u0431\u0449\u0438\u0445\u0418\u0442\u043e\u0433\u043e\u0432": "dcscor:DataCompositionTotalPlacement",
"\u0412\u0435\u0440\u0442\u0438\u043a\u0430\u043b\u044c\u043d\u043e\u0435\u0420\u0430\u0441\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0435\u041e\u0431\u0449\u0438\u0445\u0418\u0442\u043e\u0433\u043e\u0432": "dcscor:DataCompositionTotalPlacement",
"\u0420\u0430\u0441\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0435\u041e\u0431\u0449\u0438\u0445\u0418\u0442\u043e\u0433\u043e\u0432": "dcscor:DataCompositionTotalPlacement",
"\u0420\u0430\u0441\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0435\u0418\u0442\u043e\u0433\u043e\u0432": "dcscor:DataCompositionTotalPlacement",
"\u0420\u0430\u0441\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0435\u0413\u0440\u0443\u043f\u043f\u0438\u0440\u043e\u0432\u043a\u0438": "dcsset:DataCompositionFieldGroupPlacement",
"\u0420\u0430\u0441\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0435\u0420\u0435\u0441\u0443\u0440\u0441\u043e\u0432": "dcsset:DataCompositionResourcesPlacement",
"\u0422\u0438\u043f\u041c\u0430\u043a\u0435\u0442\u0430": "dcsset:DataCompositionGroupTemplateType",
}
# ===== Emit sections =====
def emit_data_sources(lines, data_sources):
for ds in data_sources:
lines.append('\t<dataSource>')
lines.append(f'\t\t<name>{esc_xml(ds["name"])}</name>')
lines.append(f'\t\t<dataSourceType>{esc_xml(ds["type"])}</dataSourceType>')
lines.append('\t</dataSource>')
# === Fields ===
def emit_input_parameters(lines, ip, indent):
if not ip:
return
items = list(ip)
if len(items) == 0:
return
lines.append(f'{indent}<inputParameters>')
for item in items:
lines.append(f'{indent}\t<dcscor:item>')
if 'use' in item and item['use'] is False:
lines.append(f'{indent}\t\t<dcscor:use>false</dcscor:use>')
lines.append(f'{indent}\t\t<dcscor:parameter>{esc_xml(str(item.get("parameter", "")))}</dcscor:parameter>')
if 'choiceParameters' in item:
cp_items = list(item['choiceParameters']) if item['choiceParameters'] else []
if len(cp_items) == 0:
lines.append(f'{indent}\t\t<dcscor:value xsi:type="dcscor:ChoiceParameters"/>')
else:
lines.append(f'{indent}\t\t<dcscor:value xsi:type="dcscor:ChoiceParameters">')
for cp in cp_items:
lines.append(f'{indent}\t\t\t<dcscor:item>')
lines.append(f'{indent}\t\t\t\t<dcscor:choiceParameter>{esc_xml(str(cp.get("name", "")))}</dcscor:choiceParameter>')
for v in cp.get('values', []) or []:
if isinstance(v, bool):
vs = 'true' if v else 'false'
lines.append(f'{indent}\t\t\t\t<dcscor:value xsi:type="xs:boolean">{vs}</dcscor:value>')
elif isinstance(v, (int, float)):
lines.append(f'{indent}\t\t\t\t<dcscor:value xsi:type="xs:decimal">{v}</dcscor:value>')
else:
lines.append(f'{indent}\t\t\t\t<dcscor:value xsi:type="dcscor:DesignTimeValue">{esc_xml(str(v))}</dcscor:value>')
lines.append(f'{indent}\t\t\t</dcscor:item>')
lines.append(f'{indent}\t\t</dcscor:value>')
elif 'choiceParameterLinks' in item:
cpl_items = list(item['choiceParameterLinks']) if item['choiceParameterLinks'] else []
if len(cpl_items) == 0:
lines.append(f'{indent}\t\t<dcscor:value xsi:type="dcscor:ChoiceParameterLinks"/>')
else:
lines.append(f'{indent}\t\t<dcscor:value xsi:type="dcscor:ChoiceParameterLinks">')
for cpl in cpl_items:
lines.append(f'{indent}\t\t\t<dcscor:item>')
lines.append(f'{indent}\t\t\t\t<dcscor:choiceParameter>{esc_xml(str(cpl.get("name", "")))}</dcscor:choiceParameter>')
lines.append(f'{indent}\t\t\t\t<dcscor:value>{esc_xml(str(cpl.get("value", "")))}</dcscor:value>')
mode = cpl.get('mode') or 'Auto'
lines.append(f'{indent}\t\t\t\t<dcscor:mode xmlns:d8p1="http://v8.1c.ru/8.1/data/enterprise" xsi:type="d8p1:LinkedValueChangeMode">{mode}</dcscor:mode>')
lines.append(f'{indent}\t\t\t</dcscor:item>')
lines.append(f'{indent}\t\t</dcscor:value>')
elif 'value' in item:
val = item['value']
# Явный кастомный type из decompile: {uri, name}
vt_src = item.get('valueType')
custom_uri = None; custom_name = None
if isinstance(vt_src, dict):
custom_uri = vt_src.get('uri')
custom_name = vt_src.get('name')
if custom_uri and custom_name:
lines.append(f'{indent}\t\t<dcscor:value xmlns:dN="{custom_uri}" xsi:type="dN:{custom_name}">{esc_xml(str(val))}</dcscor:value>')
elif isinstance(val, bool):
vstr = 'true' if val else 'false'
lines.append(f'{indent}\t\t<dcscor:value xsi:type="xs:boolean">{vstr}</dcscor:value>')
elif isinstance(val, (int, float)):
lines.append(f'{indent}\t\t<dcscor:value xsi:type="xs:decimal">{val}</dcscor:value>')
elif isinstance(val, dict):
# Multilang dict {ru, en, ...} → LocalStringType
emit_mltext(lines, f'{indent}\t\t', 'dcscor:value', val)
else:
lines.append(f'{indent}\t\t<dcscor:value xsi:type="xs:string">{esc_xml(str(val))}</dcscor:value>')
lines.append(f'{indent}\t</dcscor:item>')
lines.append(f'{indent}</inputParameters>')
def emit_field(lines, field_def, indent):
if isinstance(field_def, str):
f = parse_field_shorthand(field_def)
else:
f = {
'dataPath': str(field_def.get('dataPath', '')) or str(field_def.get('field', '')),
'field': str(field_def.get('field', '')) or str(field_def.get('dataPath', '')),
'title': field_def.get('title') if field_def.get('title') else '',
'type': (
[resolve_type_str(str(t)) for t in field_def['type']]
if isinstance(field_def['type'], list)
else resolve_type_str(str(field_def['type']))
) if field_def.get('type') else '',
'roles': [],
'restrict': [],
'appearance': {},
'roleExtras': {},
}
# Parse role (string shorthand / list / dict — единый формат с /skd-edit set-field-role)
if field_def.get('role') is not None:
parsed = parse_role_spec(field_def['role'])
f['roles'] = parsed['tokens']
f['roleExtras'] = parsed['extras']
# Parse restrictions
if field_def.get('restrict'):
f['restrict'] = list(field_def['restrict'])
# Parse appearance (сохраняем значение как есть — может быть string или multilang dict)
if field_def.get('appearance'):
for k, v in field_def['appearance'].items():
f['appearance'][k] = v
if field_def.get('presentationExpression'):
f['presentationExpression'] = str(field_def['presentationExpression'])
# attrRestrict
if field_def.get('attrRestrict'):
f['attrRestrict'] = list(field_def['attrRestrict'])
# availableValues — array of {value, presentation}
if field_def.get('availableValues'):
f['availableValues'] = field_def['availableValues']
# orderExpression — {expression, orderType, autoOrder}
if field_def.get('orderExpression'):
f['orderExpression'] = field_def['orderExpression']
# inputParameters — массив элементов, типизированных по форме value
if field_def.get('inputParameters') is not None:
f['inputParameters'] = field_def['inputParameters']
# folder: true → DataSetFieldFolder
if field_def.get('folder') is True:
f['folder'] = True
# DataSetFieldFolder — только dataPath + title
if f.get('folder'):
lines.append(f'{indent}<field xsi:type="DataSetFieldFolder">')
lines.append(f'{indent}\t<dataPath>{esc_xml(f["dataPath"])}</dataPath>')
if f.get('title'):
emit_mltext(lines, f'{indent}\t', 'title', f['title'])
lines.append(f'{indent}</field>')
return
lines.append(f'{indent}<field xsi:type="DataSetFieldField">')
lines.append(f'{indent}\t<dataPath>{esc_xml(f["dataPath"])}</dataPath>')
lines.append(f'{indent}\t<field>{esc_xml(f["field"])}</field>')
# Title
if f.get('title'):
emit_mltext(lines, f'{indent}\t', 'title', f['title'])
# UseRestriction
restrict_map = {
'noField': 'field', 'noFilter': 'condition', 'noCondition': 'condition',
'noGroup': 'group', 'noOrder': 'order',
}
if f.get('restrict') and len(f['restrict']) > 0:
lines.append(f'{indent}\t<useRestriction>')
for r in f['restrict']:
xml_name = restrict_map.get(str(r))
if xml_name:
lines.append(f'{indent}\t\t<{xml_name}>true</{xml_name}>')
lines.append(f'{indent}\t</useRestriction>')
# AttributeUseRestriction
if f.get('attrRestrict') and len(f['attrRestrict']) > 0:
lines.append(f'{indent}\t<attributeUseRestriction>')
for r in f['attrRestrict']:
xml_name = restrict_map.get(str(r))
if xml_name:
lines.append(f'{indent}\t\t<{xml_name}>true</{xml_name}>')
lines.append(f'{indent}\t</attributeUseRestriction>')
# Role
extras = f.get('roleExtras') or {}
has_extras = len(extras) > 0
if (f.get('roles') and len(f['roles']) > 0) or has_extras:
lines.append(f'{indent}\t<role>')
for role in f.get('roles', []):
if role == 'period':
# @period — sugar для periodNumber=1 + periodType=Main; extras могут переопределить.
if 'periodNumber' not in extras:
lines.append(f'{indent}\t\t<dcscom:periodNumber>1</dcscom:periodNumber>')
if 'periodType' not in extras:
lines.append(f'{indent}\t\t<dcscom:periodType>Main</dcscom:periodType>')
else:
lines.append(f'{indent}\t\t<dcscom:{role}>true</dcscom:{role}>')
for k, v in extras.items():
lines.append(f'{indent}\t\t<dcscom:{k}>{esc_xml(str(v))}</dcscom:{k}>')
lines.append(f'{indent}\t</role>')
# OrderExpression — после role, до valueType
if f.get('orderExpression'):
oe_raw = f['orderExpression']
oe_list = oe_raw if isinstance(oe_raw, list) else [oe_raw]
for oe in oe_list:
expr = str(oe.get('expression', ''))
o_type = str(oe.get('orderType', 'Asc'))
auto = oe.get('autoOrder', False)
auto_str = 'true' if auto else 'false'
lines.append(f'{indent}\t<orderExpression>')
lines.append(f'{indent}\t\t<dcscom:expression>{esc_xml(expr)}</dcscom:expression>')
lines.append(f'{indent}\t\t<dcscom:orderType>{o_type}</dcscom:orderType>')
lines.append(f'{indent}\t\t<dcscom:autoOrder>{auto_str}</dcscom:autoOrder>')
lines.append(f'{indent}\t</orderExpression>')
# ValueType
if f.get('type'):
lines.append(f'{indent}\t<valueType>')
emit_value_type(lines, f['type'], f'{indent}\t\t')
lines.append(f'{indent}\t</valueType>')
# AvailableValues — list of allowed values with optional multilang presentation
if f.get('availableValues'):
for av in f['availableValues']:
lines.append(f'{indent}\t<availableValue>')
av_val = av.get('value')
av_type = str(av.get('valueType', '')) if av.get('valueType') else ''
if not av_type:
if isinstance(av_val, bool):
av_type = 'xs:boolean'
elif isinstance(av_val, (int, float)):
av_type = 'xs:decimal'
elif re.match(r'^\d{4}-\d{2}-\d{2}T', str(av_val)):
av_type = 'xs:dateTime'
else:
av_type = 'xs:string'
av_str = str(av_val).lower() if isinstance(av_val, bool) else esc_xml(str(av_val))
lines.append(f'{indent}\t\t<value xsi:type="{av_type}">{av_str}</value>')
if av.get('presentation'):
emit_mltext(lines, f'{indent}\t\t', 'presentation', av['presentation'])
lines.append(f'{indent}\t</availableValue>')
# Appearance
if f.get('appearance') and len(f['appearance']) > 0:
lines.append(f'{indent}\t<appearance>')
for key, val in f['appearance'].items():
# \u0413\u043e\u0440\u0438\u0437\u043e\u043d\u0442\u0430\u043b\u044c\u043d\u043e\u0435\u041f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0435 \u0442\u0440\u0435\u0431\u0443\u0435\u0442 \u0441\u043f\u0435\u0446\u0438\u0430\u043b\u044c\u043d\u043e\u0433\u043e xsi:type, \u043d\u0435 \u0441\u0442\u0440\u043e\u043a\u0430
if key == '\u0413\u043e\u0440\u0438\u0437\u043e\u043d\u0442\u0430\u043b\u044c\u043d\u043e\u0435\u041f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0435' and not isinstance(val, dict):
lines.append(f'{indent}\t\t<dcscor:item xsi:type="dcsset:SettingsParameterValue">')
lines.append(f'{indent}\t\t\t<dcscor:parameter>{esc_xml(key)}</dcscor:parameter>')
lines.append(f'{indent}\t\t\t<dcscor:value xsi:type="v8ui:HorizontalAlign">{esc_xml(str(val))}</dcscor:value>')
lines.append(f'{indent}\t\t</dcscor:item>')
else:
emit_appearance_value(lines, key, val, f'{indent}\t\t')
lines.append(f'{indent}\t</appearance>')
# PresentationExpression
if f.get('presentationExpression'):
lines.append(f'{indent}\t<presentationExpression>{esc_xml(f["presentationExpression"])}</presentationExpression>')
# InputParameters — в конце field
if f.get('inputParameters'):
emit_input_parameters(lines, f['inputParameters'], f'{indent}\t')
lines.append(f'{indent}</field>')
# === DataSets ===
def emit_data_set(lines, ds, indent, default_source, tag_name='dataSet'):
# Determine type
if ds.get('items'):
ds_type = 'DataSetUnion'
elif ds.get('objectName'):
ds_type = 'DataSetObject'
else:
ds_type = 'DataSetQuery'
lines.append(f'{indent}<{tag_name} xsi:type="{ds_type}">')
lines.append(f'{indent}\t<name>{esc_xml(str(ds.get("name", "")))}</name>')
# Fields
if ds.get('fields'):
for f in ds['fields']:
emit_field(lines, f, f'{indent}\t')
# DataSource (not for Union)
if ds_type != 'DataSetUnion':
src = str(ds['source']) if ds.get('source') else default_source
lines.append(f'{indent}\t<dataSource>{esc_xml(src)}</dataSource>')
# Type-specific content
if ds_type == 'DataSetQuery':
query_text = resolve_query_value(str(ds.get("query", "")), query_base_dir)
lines.append(f'{indent}\t<query>{esc_xml(query_text)}</query>')
if ds.get('autoFillFields') is False:
lines.append(f'{indent}\t<autoFillFields>false</autoFillFields>')
elif ds_type == 'DataSetObject':
lines.append(f'{indent}\t<objectName>{esc_xml(str(ds["objectName"]))}</objectName>')
elif ds_type == 'DataSetUnion':
for item in ds['items']:
# Union inner items are wrapped as <item xsi:type="...">
emit_data_set(lines, item, f'{indent}\t', default_source, tag_name='item')
lines.append(f'{indent}</{tag_name}>')
def emit_data_sets(lines, defn, default_source):
for ds in defn['dataSets']:
emit_data_set(lines, ds, '\t', default_source)
# === DataSetLinks ===
def emit_data_set_links(lines, defn):
if not defn.get('dataSetLinks'):
return
for link in defn['dataSetLinks']:
lines.append('\t<dataSetLink>')
src_ds = str(link.get('source') or link.get('sourceDataSet') or '')
dst_ds = str(link.get('dest') or link.get('destinationDataSet') or '')
src_ex = str(link.get('sourceExpr') or link.get('sourceExpression') or '')
dst_ex = str(link.get('destExpr') or link.get('destinationExpression') or '')
lines.append(f'\t\t<sourceDataSet>{esc_xml(src_ds)}</sourceDataSet>')
lines.append(f'\t\t<destinationDataSet>{esc_xml(dst_ds)}</destinationDataSet>')
lines.append(f'\t\t<sourceExpression>{esc_xml(src_ex)}</sourceExpression>')
lines.append(f'\t\t<destinationExpression>{esc_xml(dst_ex)}</destinationExpression>')
if link.get('parameter'):
lines.append(f'\t\t<parameter>{esc_xml(str(link["parameter"]))}</parameter>')
if link.get('parameterListAllowed'):
lines.append('\t\t<parameterListAllowed>true</parameterListAllowed>')
if link.get('startExpression') is not None:
lines.append(f'\t\t<startExpression>{esc_xml(str(link["startExpression"]))}</startExpression>')
if link.get('linkConditionExpression') is not None:
lines.append(f'\t\t<linkConditionExpression>{esc_xml(str(link["linkConditionExpression"]))}</linkConditionExpression>')
lines.append('\t</dataSetLink>')
# === CalculatedFields ===
def emit_calc_fields(lines, defn):
if not defn.get('calculatedFields'):
return
restrict_map = {
'noField': 'field', 'noFilter': 'condition', 'noCondition': 'condition',
'noGroup': 'group', 'noOrder': 'order',
}
for cf in defn['calculatedFields']:
# Collect dataPath/expression/title/type/restrict/appearance from either
# shorthand string or object form. Object form accepts dataPath/field/name
# as synonyms; useRestriction/restrict accepts object, array, or flag string.
title = ''
type_str = ''
restrict_tokens = []
restrict_obj = None
appearance = None
if isinstance(cf, str):
parsed = parse_calc_shorthand(cf)
data_path = parsed['dataPath']
expression = parsed['expression']
title = parsed.get('title', '') or ''
type_str = parsed.get('type', '') or ''
restrict_tokens = list(parsed.get('restrict') or [])
else:
data_path = str(cf.get('dataPath') or cf.get('field') or cf.get('name') or '')
expression = str(cf.get('expression', ''))
if cf.get('title'):
title = cf['title']
if cf.get('type'):
type_str = resolve_type_str(str(cf['type']))
restrict_val = cf.get('restrict') if cf.get('restrict') is not None else cf.get('useRestriction')
if restrict_val:
if isinstance(restrict_val, dict):
restrict_obj = restrict_val
elif isinstance(restrict_val, str):
# Flag-string form: "#noField #noFilter #noGroup #noOrder" (or without `#`)
for tok in restrict_val.split():
t = tok.strip().lstrip('#')
if t:
restrict_tokens.append(t)
else:
# Array form: ["noField", "noFilter", ...]
for r in restrict_val:
restrict_tokens.append(str(r))
appearance = cf.get('appearance')
lines.append('\t<calculatedField>')
lines.append(f'\t\t<dataPath>{esc_xml(data_path)}</dataPath>')
lines.append(f'\t\t<expression>{esc_xml(expression)}</expression>')
if title:
emit_mltext(lines, '\t\t', 'title', title)
if type_str:
lines.append('\t\t<valueType>')
emit_value_type(lines, type_str, '\t\t\t')
lines.append('\t\t</valueType>')
if restrict_obj or restrict_tokens:
lines.append('\t\t<useRestriction>')
if restrict_obj:
for xml_name, flag in restrict_obj.items():
if flag:
lines.append(f'\t\t\t<{esc_xml(str(xml_name))}>true</{esc_xml(str(xml_name))}>')
else:
for r in restrict_tokens:
xml_name = restrict_map.get(str(r))
if xml_name:
lines.append(f'\t\t\t<{xml_name}>true</{xml_name}>')
lines.append('\t\t</useRestriction>')
if appearance:
lines.append('\t\t<appearance>')
for k, v in appearance.items():
if k == 'ГоризонтальноеПоложение' and not isinstance(v, dict):
lines.append('\t\t\t<dcscor:item xsi:type="dcsset:SettingsParameterValue">')
lines.append(f'\t\t\t\t<dcscor:parameter>{esc_xml(k)}</dcscor:parameter>')
lines.append(f'\t\t\t\t<dcscor:value xsi:type="v8ui:HorizontalAlign">{esc_xml(str(v))}</dcscor:value>')
lines.append('\t\t\t</dcscor:item>')
else:
emit_appearance_value(lines, k, v, '\t\t\t')
lines.append('\t\t</appearance>')
lines.append('\t</calculatedField>')
# === TotalFields ===
def emit_total_fields(lines, defn):
if not defn.get('totalFields'):
return
for tf in defn['totalFields']:
if isinstance(tf, str):
parsed = parse_total_shorthand(tf)
groups = None
else:
parsed = {
'dataPath': str(tf.get('dataPath', '')),
'expression': str(tf.get('expression', '')),
}
groups = tf.get('group')
lines.append('\t<totalField>')
lines.append(f'\t\t<dataPath>{esc_xml(parsed["dataPath"])}</dataPath>')
lines.append(f'\t\t<expression>{esc_xml(parsed["expression"])}</expression>')
if groups:
if isinstance(groups, list):
for g in groups:
lines.append(f'\t\t<group>{esc_xml(str(g))}</group>')
else:
lines.append(f'\t\t<group>{esc_xml(str(groups))}</group>')
lines.append('\t</totalField>')
# === Parameters ===
def is_empty_value(v):
if v is None:
return True
s = str(v).strip()
if s == '':
return True
if s == '_':
return True
if s.lower() == 'null':
return True
return False
def emit_empty_value(lines, type_str, indent, tag_prefix='', value_list_allowed=False):
if value_list_allowed:
return
t = type_str or ''
# Нормализация: убираем префикс xs: (валидный для valueType из decompile/DSL)
t_bare = t[3:] if t.startswith('xs:') else t
pf = tag_prefix
if t == '':
lines.append(f'{indent}<{pf}value xsi:nil="true"/>')
elif t == 'StandardPeriod':
lines.append(f'{indent}<{pf}value xsi:type="v8:StandardPeriod">')
lines.append(f'{indent}\t<v8:variant xsi:type="v8:StandardPeriodVariant">Custom</v8:variant>')
lines.append(f'{indent}\t<v8:startDate>0001-01-01T00:00:00</v8:startDate>')
lines.append(f'{indent}\t<v8:endDate>0001-01-01T00:00:00</v8:endDate>')
lines.append(f'{indent}</{pf}value>')
elif re.match(r'^string', t_bare):
lines.append(f'{indent}<{pf}value xsi:type="xs:string"/>')
elif re.match(r'^(date|time)', t_bare):
lines.append(f'{indent}<{pf}value xsi:type="xs:dateTime">0001-01-01T00:00:00</{pf}value>')
elif re.match(r'^decimal', t_bare):
lines.append(f'{indent}<{pf}value xsi:type="xs:decimal">0</{pf}value>')
elif t_bare == 'boolean':
lines.append(f'{indent}<{pf}value xsi:type="xs:boolean">false</{pf}value>')
else:
# Ref types or unknown — safe nil
lines.append(f'{indent}<{pf}value xsi:nil="true"/>')
def emit_param_value(lines, type_str, val, indent, value_list_allowed=False):
if is_empty_value(val):
emit_empty_value(lines, type_str, indent, '', value_list_allowed)
return
# val может быть строкой (variant only) или dict {variant, startDate?, endDate?}.
variant_str = None
sd_str = None
ed_str = None
if isinstance(val, dict):
variant_str = str(val.get('variant')) if val.get('variant') is not None else None
sd_str = str(val['startDate']) if 'startDate' in val else None
ed_str = str(val['endDate']) if 'endDate' in val else None
val_str = variant_str if variant_str else str(val)
if type_str == 'StandardPeriod':
# Platform-pattern: startDate/endDate ТОЛЬКО для variant=Custom.
lines.append(f'{indent}<value xsi:type="v8:StandardPeriod">')
lines.append(f'{indent}\t<v8:variant xsi:type="v8:StandardPeriodVariant">{esc_xml(val_str)}</v8:variant>')
if val_str == 'Custom':
sd_out = sd_str if sd_str else '0001-01-01T00:00:00'
ed_out = ed_str if ed_str else '0001-01-01T00:00:00'
lines.append(f'{indent}\t<v8:startDate>{esc_xml(sd_out)}</v8:startDate>')
lines.append(f'{indent}\t<v8:endDate>{esc_xml(ed_out)}</v8:endDate>')
lines.append(f'{indent}</value>')
elif type_str and re.match(r'^date', type_str):
lines.append(f'{indent}<value xsi:type="xs:dateTime">{esc_xml(val_str)}</value>')
elif type_str == 'boolean':
lines.append(f'{indent}<value xsi:type="xs:boolean">{esc_xml(val_str)}</value>')
elif type_str and re.match(r'^decimal', type_str):
lines.append(f'{indent}<value xsi:type="xs:decimal">{esc_xml(val_str)}</value>')
elif type_str and re.match(r'^string', type_str):
lines.append(f'{indent}<value xsi:type="xs:string">{esc_xml(val_str)}</value>')
elif type_str and re.match(r'^(CatalogRef|DocumentRef|EnumRef|ChartOfAccountsRef|ChartOfCharacteristicTypesRef|ChartOfCalculationTypesRef|BusinessProcessRef|TaskRef|ExchangePlanRef)\.', type_str):
lines.append(f'{indent}<value xsi:type="dcscor:DesignTimeValue">{esc_xml(val_str)}</value>')
else:
# Guess from value
if re.match(r'^\d{4}-\d{2}-\d{2}T', val_str):
lines.append(f'{indent}<value xsi:type="xs:dateTime">{esc_xml(val_str)}</value>')
elif val_str == 'true' or val_str == 'false':
lines.append(f'{indent}<value xsi:type="xs:boolean">{esc_xml(val_str)}</value>')
elif re.match(r'^(ПланСчетов|Справочник|Перечисление|Документ|ПланВидовХарактеристик|ПланВидовРасчета|БизнесПроцесс|Задача|РегистрСведений|ПланОбмена|ChartOfAccounts|Catalog|Enum|Document|ChartOfCharacteristicTypes|ChartOfCalculationTypes|BusinessProcess|Task|InformationRegister|ExchangePlan)\.', val_str):
lines.append(f'{indent}<value xsi:type="dcscor:DesignTimeValue">{esc_xml(val_str)}</value>')
else:
lines.append(f'{indent}<value xsi:type="xs:string">{esc_xml(val_str)}</value>')
def emit_single_param(lines, p, parsed):
lines.append('\t<parameter>')
lines.append(f'\t\t<name>{esc_xml(parsed["name"])}</name>')
# Title (from parsed first, then from object form; accept `presentation` as
# a synonym — 1C UI labels a parameter's caption "Представление").
title = ''
if parsed.get('title'):
title = parsed['title']
elif p is not None and not isinstance(p, str) and p.get('title'):
title = p['title']
elif p is not None and not isinstance(p, str) and p.get('presentation'):
title = p['presentation']
if title:
emit_mltext(lines, '\t\t', 'title', title)
# ValueType
if parsed.get('type'):
lines.append('\t\t<valueType>')
emit_value_type(lines, parsed['type'], '\t\t\t')
lines.append('\t\t</valueType>')
# Value — for valueListAllowed params Designer omits <value> when empty
vla = bool(parsed.get('valueListAllowed'))
p_type = parsed.get('type', '')
if isinstance(p_type, (list, tuple)):
# Composite type — Designer writes xsi:nil for any empty composite;
# non-empty composite values are uncommon and would need per-type tagging.
if is_empty_value(parsed.get('value')):
if not vla:
lines.append('\t\t<value xsi:nil="true"/>')
elif parsed.get('nilValue') is True:
# Принудительный xsi:nil даже когда тип известен (для bit-perfect round-trip).
if not vla:
lines.append('\t\t<value xsi:nil="true"/>')
elif isinstance(parsed.get('value'), list):
# Multi-value (массив значений по умолчанию для valueListAllowed-параметра).
for v in parsed['value']:
emit_param_value(lines, p_type, v, '\t\t', False)
else:
emit_param_value(lines, p_type, parsed.get('value'), '\t\t', vla)
# Hidden implies useRestriction=true + availableAsField=false
if parsed.get('hidden') is True:
parsed['availableAsField'] = False
parsed['useRestriction'] = True
# UseRestriction — платформа всегда эмитит этот тег у параметра (true/false)
ur_emit = (
parsed.get('useRestriction') is True
or (p is not None and not isinstance(p, str) and p.get('useRestriction') is True)
)
lines.append(f'\t\t<useRestriction>{"true" if ur_emit else "false"}</useRestriction>')
# Expression
if parsed.get('expression'):
lines.append(f'\t\t<expression>{esc_xml(parsed["expression"])}</expression>')
if parsed.get('hidden'):
parsed['availableAsField'] = False
# AvailableAsField
if parsed.get('availableAsField') is False:
lines.append('\t\t<availableAsField>false</availableAsField>')
# ValueListAllowed
if parsed.get('valueListAllowed'):
lines.append('\t\t<valueListAllowed>true</valueListAllowed>')
# AvailableValues
if p is not None and not isinstance(p, str) and p.get('availableValues'):
for av in p['availableValues']:
lines.append('\t\t<availableValue>')
if is_empty_value(av.get('value')):
emit_empty_value(lines, parsed.get('type', ''), '\t\t\t', '', False)
else:
av_v = av['value']
if isinstance(av_v, bool):
lines.append(f'\t\t\t<value xsi:type="xs:boolean">{str(av_v).lower()}</value>')
elif isinstance(av_v, (int, float)):
lines.append(f'\t\t\t<value xsi:type="xs:decimal">{av_v}</value>')
else:
av_val = str(av_v)
av_type = 'xs:string'
if re.match(r'^(Перечисление|Справочник|ПланСчетов|Документ|ПланВидовХарактеристик|ПланВидовРасчета)\.', av_val):
av_type = 'dcscor:DesignTimeValue'
lines.append(f'\t\t\t<value xsi:type="{av_type}">{esc_xml(av_val)}</value>')
# `title` accepted as synonym of `presentation` — both map to the same UI label.
av_pres = av.get('presentation') or av.get('title') or ''
if av_pres:
emit_mltext(lines, '\t\t\t', 'presentation', av_pres)
lines.append('\t\t</availableValue>')
# DenyIncompleteValues
deny = parsed.get('denyIncompleteValues') is True or (
p is not None and not isinstance(p, str) and p.get('denyIncompleteValues') is True)
if deny:
lines.append('\t\t<denyIncompleteValues>true</denyIncompleteValues>')
# Use
use_val = None
if p is not None and not isinstance(p, str) and p.get('use'):
use_val = str(p['use'])
elif parsed.get('use'):
use_val = str(parsed['use'])
if use_val:
lines.append(f'\t\t<use>{esc_xml(use_val)}</use>')
# InputParameters на параметре (ФорматРедактирования и т.п.)
if p is not None and not isinstance(p, str) and p.get('inputParameters'):
emit_input_parameters(lines, p['inputParameters'], '\t\t')
lines.append('\t</parameter>')
_all_params = []
def emit_parameters(lines, defn):
global _all_params
_all_params = []
if not defn.get('parameters'):
return
for p in defn['parameters']:
if isinstance(p, str):
parsed = parse_param_shorthand(p)
else:
# Composite type: ["string(10,fix)", "CatalogRef.X"] → list of resolved
# strings; emit_value_type handles lists, empty value falls through to nil.
raw_type = p.get('type')
if isinstance(raw_type, (list, tuple)):
resolved_type = [resolve_type_str(str(t)) for t in raw_type]
elif raw_type:
resolved_type = resolve_type_str(str(raw_type))
else:
resolved_type = ''
parsed = {
'name': str(p.get('name', '')),
'type': resolved_type,
'value': p.get('value'),
'autoDates': False,
}
if p.get('expression'):
parsed['expression'] = str(p['expression'])
if p.get('availableAsField') is False:
parsed['availableAsField'] = False
if p.get('valueListAllowed') is True:
parsed['valueListAllowed'] = True
if p.get('hidden') is True:
parsed['hidden'] = True
if p.get('autoDates') is True:
parsed['autoDates'] = True
if p.get('nilValue') is True:
parsed['nilValue'] = True
# @autoDates implies use=Always + denyIncompleteValues=true by default
# (derived &НачалоПериода/&КонецПериода need a populated period).
# Explicit values in object form override these defaults.
if parsed.get('autoDates'):
is_obj = p is not None and not isinstance(p, str)
if not (is_obj and p.get('use') is not None):
parsed['use'] = 'Always'
if not (is_obj and p.get('denyIncompleteValues') is not None):
parsed['denyIncompleteValues'] = True
emit_single_param(lines, p, parsed)
# Track parameter for auto dataParameters
_all_params.append({
'name': parsed['name'],
'hidden': bool(parsed.get('hidden')),
'type': parsed.get('type', ''),
'value': parsed.get('value'),
})
# @autoDates: auto-generate НачалоПериода and КонецПериода (canonical БСП pattern).
# type=dateTime + DateFractions=DateTime — иначе КонецПериода обрезается до 00:00:00
# и запрос `Дата МЕЖДУ &НачалоПериода И &КонецПериода` теряет данные за последний день.
if parsed.get('autoDates'):
param_name = parsed['name']
begin_parsed = {
'name': '\u041d\u0430\u0447\u0430\u043b\u043e\u041f\u0435\u0440\u0438\u043e\u0434\u0430',
'title': '\u041d\u0430\u0447\u0430\u043b\u043e \u043f\u0435\u0440\u0438\u043e\u0434\u0430',
'type': 'dateTime', 'value': '0001-01-01T00:00:00',
'useRestriction': True,
'expression': f'&{param_name}.\u0414\u0430\u0442\u0430\u041d\u0430\u0447\u0430\u043b\u0430',
}
emit_single_param(lines, None, begin_parsed)
end_parsed = {
'name': '\u041a\u043e\u043d\u0435\u0446\u041f\u0435\u0440\u0438\u043e\u0434\u0430',
'title': '\u041a\u043e\u043d\u0435\u0446 \u043f\u0435\u0440\u0438\u043e\u0434\u0430',
'type': 'dateTime', 'value': '0001-01-01T00:00:00',
'useRestriction': True,
'expression': f'&{param_name}.\u0414\u0430\u0442\u0430\u041e\u043a\u043e\u043d\u0447\u0430\u043d\u0438\u044f',
}
emit_single_param(lines, None, end_parsed)
# === AreaTemplate DSL ===
AREA_STYLE_PRESETS = {
'none': {
'font': None, 'fontSize': None, 'bold': False, 'italic': False,
'hAlign': None, 'vAlign': None, 'wrap': False,
'bgColor': None, 'textColor': None,
'borderColor': None, 'borders': False,
},
'data': {
'font': 'Arial', 'fontSize': 10, 'bold': False, 'italic': False,
'hAlign': None, 'vAlign': None, 'wrap': False,
'bgColor': 'style:ReportGroup1BackColor', 'textColor': None,
'borderColor': 'style:ReportLineColor', 'borders': True,
},
'header': {
'font': 'Arial', 'fontSize': 10, 'bold': False, 'italic': False,
'hAlign': 'Center', 'vAlign': None, 'wrap': True,
'bgColor': 'style:ReportHeaderBackColor', 'textColor': None,
'borderColor': 'style:ReportLineColor', 'borders': True,
},
'subheader': {
'font': 'Arial', 'fontSize': 10, 'bold': False, 'italic': False,
'hAlign': 'Center', 'vAlign': None, 'wrap': True,
'bgColor': None, 'textColor': None,
'borderColor': 'style:ReportLineColor', 'borders': True,
},
'total': {
'font': 'Arial', 'fontSize': 10, 'bold': False, 'italic': False,
'hAlign': None, 'vAlign': None, 'wrap': False,
'bgColor': None, 'textColor': None,
'borderColor': 'style:ReportLineColor', 'borders': True,
},
}
def load_user_styles(base_dir, output_path=None):
# Search order (first found wins): 1) definition dir, 2) cwd, 3) scan-up from OutputPath for presets/skills/skd/
search_paths = [
os.path.join(base_dir, 'skd-styles.json'),
os.path.join(os.getcwd(), 'skd-styles.json'),
]
if output_path:
scan_dir = os.path.dirname(output_path)
while scan_dir:
search_paths.append(os.path.join(scan_dir, 'presets', 'skills', 'skd', 'skd-styles.json'))
parent_dir = os.path.dirname(scan_dir)
if parent_dir == scan_dir:
break
scan_dir = parent_dir
for p in search_paths:
if os.path.isfile(p):
with open(p, 'r', encoding='utf-8-sig') as f:
user_styles = json.load(f)
for name, overrides in user_styles.items():
base = dict(AREA_STYLE_PRESETS.get(name, AREA_STYLE_PRESETS['data']))
base.update(overrides)
AREA_STYLE_PRESETS[name] = base
return
def _emit_color_value(lines, color, indent):
# Префиксы style:/web:/win: → соответствующий xmlns + dN:Name
color_prefix_to_uri = {
'style:': 'http://v8.1c.ru/8.1/data/ui/style',
'web:': 'http://v8.1c.ru/8.1/data/ui/colors/web',
'win:': 'http://v8.1c.ru/8.1/data/ui/colors/windows',
}
for pfx, uri in color_prefix_to_uri.items():
if color.startswith(pfx):
name = color[len(pfx):]
lines.append(f'{indent}<dcscor:value xmlns:d8p1="{uri}" xsi:type="v8ui:Color">d8p1:{name}</dcscor:value>')
return
lines.append(f'{indent}<dcscor:value xsi:type="v8ui:Color">{esc_xml(color)}</dcscor:value>')
def _emit_cell_appearance(lines, style, width=0, v_merge=False, h_merge=False, min_height=0, extra_items=None):
ind = '\t\t\t\t\t\t'
# Если ничего внутри appearance не будет — не эмитим блок вовсе
# (оригинал платформы для cells без атрибутов не пишет <appearance></appearance>).
has_content = bool(
style.get('bgColor') or style.get('textColor') or style.get('borders') or
style.get('font') or style.get('hAlign') or style.get('vAlign') or style.get('wrap') or
(width > 0) or (min_height > 0) or v_merge or h_merge or
(extra_items and len(extra_items) > 0)
)
if not has_content:
return
lines.append('\t\t\t\t\t<dcsat:appearance>')
# Background color
if style.get('bgColor'):
lines.append(f'{ind}<dcscor:item>')
lines.append(f'{ind}\t<dcscor:parameter>\u0426\u0432\u0435\u0442\u0424\u043e\u043d\u0430</dcscor:parameter>')
_emit_color_value(lines, style['bgColor'], f'{ind}\t')
lines.append(f'{ind}</dcscor:item>')
# Text color
if style.get('textColor'):
lines.append(f'{ind}<dcscor:item>')
lines.append(f'{ind}\t<dcscor:parameter>\u0426\u0432\u0435\u0442\u0422\u0435\u043a\u0441\u0442\u0430</dcscor:parameter>')
_emit_color_value(lines, style['textColor'], f'{ind}\t')
lines.append(f'{ind}</dcscor:item>')
# Borders
if style.get('borders'):
if style.get('borderColor'):
lines.append(f'{ind}<dcscor:item>')
lines.append(f'{ind}\t<dcscor:parameter>\u0426\u0432\u0435\u0442\u0413\u0440\u0430\u043d\u0438\u0446\u044b</dcscor:parameter>')
_emit_color_value(lines, style['borderColor'], f'{ind}\t')
lines.append(f'{ind}</dcscor:item>')
lines.append(f'{ind}<dcscor:item>')
lines.append(f'{ind}\t<dcscor:parameter>\u0421\u0442\u0438\u043b\u044c\u0413\u0440\u0430\u043d\u0438\u0446\u044b</dcscor:parameter>')
lines.append(f'{ind}\t<dcscor:value xsi:type="v8ui:Line" width="0" gap="false">')
lines.append(f'{ind}\t\t<v8ui:style xsi:type="v8ui:SpreadsheetDocumentCellLineType">None</v8ui:style>')
lines.append(f'{ind}\t</dcscor:value>')
for side in ['\u0421\u043b\u0435\u0432\u0430', '\u0421\u0432\u0435\u0440\u0445\u0443', '\u0421\u043f\u0440\u0430\u0432\u0430', '\u0421\u043d\u0438\u0437\u0443']:
lines.append(f'{ind}\t<dcscor:item>')
lines.append(f'{ind}\t\t<dcscor:parameter>\u0421\u0442\u0438\u043b\u044c\u0413\u0440\u0430\u043d\u0438\u0446\u044b.{side}</dcscor:parameter>')
lines.append(f'{ind}\t\t<dcscor:value xsi:type="v8ui:Line" width="1" gap="false">')
lines.append(f'{ind}\t\t\t<v8ui:style xsi:type="v8ui:SpreadsheetDocumentCellLineType">Solid</v8ui:style>')
lines.append(f'{ind}\t\t</dcscor:value>')
lines.append(f'{ind}\t</dcscor:item>')
lines.append(f'{ind}</dcscor:item>')
# Font (skip if style has no font configured \u2014 for "none" preset)
if style.get('font'):
bold_str = 'true' if style.get('bold') else 'false'
italic_str = 'true' if style.get('italic') else 'false'
lines.append(f'{ind}<dcscor:item>')
lines.append(f'{ind}\t<dcscor:parameter>\u0428\u0440\u0438\u0444\u0442</dcscor:parameter>')
lines.append(f'{ind}\t<dcscor:value xsi:type="v8ui:Font" faceName="{style["font"]}" height="{style["fontSize"]}" bold="{bold_str}" italic="{italic_str}" underline="false" strikeout="false" kind="Absolute" scale="100"/>')
lines.append(f'{ind}</dcscor:item>')
# Horizontal alignment
if style.get('hAlign'):
lines.append(f'{ind}<dcscor:item>')
lines.append(f'{ind}\t<dcscor:parameter>\u0413\u043e\u0440\u0438\u0437\u043e\u043d\u0442\u0430\u043b\u044c\u043d\u043e\u0435\u041f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0435</dcscor:parameter>')
lines.append(f'{ind}\t<dcscor:value xsi:type="v8ui:HorizontalAlign">{esc_xml(style["hAlign"])}</dcscor:value>')
lines.append(f'{ind}</dcscor:item>')
# Vertical alignment
if style.get('vAlign'):
lines.append(f'{ind}<dcscor:item>')
lines.append(f'{ind}\t<dcscor:parameter>\u0412\u0435\u0440\u0442\u0438\u043a\u0430\u043b\u044c\u043d\u043e\u0435\u041f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0435</dcscor:parameter>')
lines.append(f'{ind}\t<dcscor:value xsi:type="v8ui:VerticalAlign">{esc_xml(style["vAlign"])}</dcscor:value>')
lines.append(f'{ind}</dcscor:item>')
# Wrap
if style.get('wrap'):
lines.append(f'{ind}<dcscor:item>')
lines.append(f'{ind}\t<dcscor:parameter>\u0420\u0430\u0437\u043c\u0435\u0449\u0435\u043d\u0438\u0435</dcscor:parameter>')
lines.append(f'{ind}\t<dcscor:value xsi:type="dcscor:DataCompositionTextPlacementType">Wrap</dcscor:value>')
lines.append(f'{ind}</dcscor:item>')
# Width
if width and width > 0:
lines.append(f'{ind}<dcscor:item>')
lines.append(f'{ind}\t<dcscor:parameter>\u041c\u0438\u043d\u0438\u043c\u0430\u043b\u044c\u043d\u0430\u044f\u0428\u0438\u0440\u0438\u043d\u0430</dcscor:parameter>')
lines.append(f'{ind}\t<dcscor:value xsi:type="xs:decimal">{fmt_dec(width)}</dcscor:value>')
lines.append(f'{ind}</dcscor:item>')
lines.append(f'{ind}<dcscor:item>')
lines.append(f'{ind}\t<dcscor:parameter>\u041c\u0430\u043a\u0441\u0438\u043c\u0430\u043b\u044c\u043d\u0430\u044f\u0428\u0438\u0440\u0438\u043d\u0430</dcscor:parameter>')
lines.append(f'{ind}\t<dcscor:value xsi:type="xs:decimal">{fmt_dec(width)}</dcscor:value>')
lines.append(f'{ind}</dcscor:item>')
# Min height
if min_height and min_height > 0:
lines.append(f'{ind}<dcscor:item>')
lines.append(f'{ind}\t<dcscor:parameter>\u041c\u0438\u043d\u0438\u043c\u0430\u043b\u044c\u043d\u0430\u044f\u0412\u044b\u0441\u043e\u0442\u0430</dcscor:parameter>')
lines.append(f'{ind}\t<dcscor:value xsi:type="xs:decimal">{min_height}</dcscor:value>')
lines.append(f'{ind}</dcscor:item>')
# Vertical merge
if v_merge:
lines.append(f'{ind}<dcscor:item>')
lines.append(f'{ind}\t<dcscor:parameter>\u041e\u0431\u044a\u0435\u0434\u0438\u043d\u044f\u0442\u044c\u041f\u043e\u0412\u0435\u0440\u0442\u0438\u043a\u0430\u043b\u0438</dcscor:parameter>')
lines.append(f'{ind}\t<dcscor:value xsi:type="xs:boolean">true</dcscor:value>')
lines.append(f'{ind}</dcscor:item>')
# Horizontal merge
if h_merge:
lines.append(f'{ind}<dcscor:item>')
lines.append(f'{ind}\t<dcscor:parameter>\u041e\u0431\u044a\u0435\u0434\u0438\u043d\u044f\u0442\u044c\u041f\u043e\u0413\u043e\u0440\u0438\u0437\u043e\u043d\u0442\u0430\u043b\u0438</dcscor:parameter>')
lines.append(f'{ind}\t<dcscor:value xsi:type="xs:boolean">true</dcscor:value>')
lines.append(f'{ind}</dcscor:item>')
# Extra appearance items (e.g. drilldown)
if extra_items:
for ei in extra_items:
lines.append(ei)
lines.append('\t\t\t\t\t</dcsat:appearance>')
# Cell может быть string ("text"/"{param}"/"|"/">"/null) или объектом {value, style}.
def _get_cell_value(cell):
if cell is None:
return None
if isinstance(cell, str):
return cell
if isinstance(cell, dict):
if 'value' in cell:
return cell['value']
return cell # multilang dict without wrapper
return None
def _get_cell_style_or_default(cell, default_style):
if isinstance(cell, dict) and 'style' in cell:
s_name = str(cell['style'])
if s_name in AREA_STYLE_PRESETS:
return AREA_STYLE_PRESETS[s_name]
print(f"Warning: Unknown cell style preset '{s_name}', falling back to template default", file=sys.stderr)
return default_style
def _emit_area_template_dsl(lines, t):
style_name = str(t.get('style', '')) or 'data'
if style_name not in AREA_STYLE_PRESETS:
print(f"Warning: Unknown area style preset '{style_name}', falling back to 'data'", file=sys.stderr)
style_name = 'data'
style = AREA_STYLE_PRESETS[style_name]
rows = list(t['rows'])
widths = list(t.get('widths', []))
min_height = float(t.get('minHeight', 0))
col_count = len(widths) if widths else len(rows[0])
# Build vertical merge map
v_merge = {}
for r in range(len(rows) - 1, 0, -1):
v_merge[r] = {}
for c in range(col_count):
cell_val = _get_cell_value(rows[r][c]) if c < len(rows[r]) else None
if cell_val == '|':
v_merge[r][c] = True
if 0 not in v_merge:
v_merge[0] = {}
# Build horizontal merge map
h_merge = {}
for r in range(len(rows)):
h_merge[r] = {}
for c in range(col_count):
cell_val = _get_cell_value(rows[r][c]) if c < len(rows[r]) else None
if cell_val == '>':
h_merge[r][c] = True
# Build drilldown map: param_name -> drilldown_value (только shortcut-форма: drilldown — строка).
# Форма C (drilldown — объект) — DetailsAreaTemplateParameter с произвольным именем, в map не идёт.
drilldown_map = {}
if t.get('parameters'):
for tp in t['parameters']:
dd = tp.get('drilldown')
if dd and isinstance(dd, str):
drilldown_map[str(tp['name'])] = dd
lines.append('\t<template>')
lines.append(f'\t\t<name>{esc_xml(str(t["name"]))}</name>')
lines.append('\t\t<template xmlns:dcsat="http://v8.1c.ru/8.1/data-composition-system/area-template" xsi:type="dcsat:AreaTemplate">')
for r in range(len(rows)):
lines.append('\t\t\t<dcsat:item xsi:type="dcsat:TableRow">')
for c in range(col_count):
cell_raw = rows[r][c] if c < len(rows[r]) else None
cell_val = _get_cell_value(cell_raw)
cell_style = _get_cell_style_or_default(cell_raw, style)
w = float(widths[c]) if c < len(widths) else 0
is_v_merged = v_merge.get(r, {}).get(c, False)
is_h_merged = h_merge.get(r, {}).get(c, False)
lines.append('\t\t\t\t<dcsat:tableCell>')
if is_v_merged:
_emit_cell_appearance(lines, cell_style, w, True)
elif is_h_merged:
_emit_cell_appearance(lines, cell_style, w, h_merge=True)
else:
cell_extra_items = []
if isinstance(cell_val, dict):
# Multilang static text — эмитим напрямую
lines.append('\t\t\t\t\t<dcsat:item xsi:type="dcsat:Field">')
emit_mltext(lines, '\t\t\t\t\t\t', 'dcsat:value', cell_val)
lines.append('\t\t\t\t\t</dcsat:item>')
elif cell_val is not None and str(cell_val) != '':
cell_str = str(cell_val)
# Unescape \| and \>
if cell_str == '\\|':
cell_str = '|'
elif cell_str == '\\>':
cell_str = '>'
m = re.match(r'^\{(.+)\}$', cell_str)
if m:
param_name = m.group(1)
lines.append('\t\t\t\t\t<dcsat:item xsi:type="dcsat:Field">')
lines.append(f'\t\t\t\t\t\t<dcsat:value xsi:type="dcscor:Parameter">{esc_xml(param_name)}</dcsat:value>')
lines.append('\t\t\t\t\t</dcsat:item>')
# Build drilldown appearance extra items.
# \u041f\u0440\u0438\u043e\u0440\u0438\u0442\u0435\u0442: per-cell override (cell={value, drilldown}) \u2192 drilldownMap (shortcut form B).
cell_drill_override = None
if isinstance(cell_raw, dict) and 'drilldown' in cell_raw:
cell_drill_override = str(cell_raw['drilldown'])
dd_target = None
if cell_drill_override:
dd_target = cell_drill_override
elif param_name in drilldown_map:
dd_target = f'\u0420\u0430\u0441\u0448\u0438\u0444\u0440\u043e\u0432\u043a\u0430_{drilldown_map[param_name]}'
if dd_target:
cell_extra_items.append('\t\t\t\t\t\t<dcscor:item>')
cell_extra_items.append(f'\t\t\t\t\t\t\t<dcscor:parameter>\u0420\u0430\u0441\u0448\u0438\u0444\u0440\u043e\u0432\u043a\u0430</dcscor:parameter>')
cell_extra_items.append(f'\t\t\t\t\t\t\t<dcscor:value xsi:type="dcscor:Parameter">{esc_xml(dd_target)}</dcscor:value>')
cell_extra_items.append('\t\t\t\t\t\t</dcscor:item>')
else:
lines.append('\t\t\t\t\t<dcsat:item xsi:type="dcsat:Field">')
emit_mltext(lines, '\t\t\t\t\t\t', 'dcsat:value', cell_str)
lines.append('\t\t\t\t\t</dcsat:item>')
h = min_height if r == 0 else 0
_emit_cell_appearance(lines, cell_style, w, False, False, h, cell_extra_items or None)
lines.append('\t\t\t\t</dcsat:tableCell>')
lines.append('\t\t\t</dcsat:item>')
lines.append('\t\t</template>')
if t.get('parameters'):
for tp in t['parameters']:
_emit_area_template_parameter(lines, tp, '\t\t')
lines.append('\t</template>')
# \u042d\u043c\u0438\u0441\u0441\u0438\u044f \u043e\u0434\u043d\u043e\u0433\u043e \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u0430 \u0448\u0430\u0431\u043b\u043e\u043d\u0430. \u0420\u0430\u0437\u043b\u0438\u0447\u0430\u0435\u0442 \u0442\u0440\u0438 \u0444\u043e\u0440\u043c\u044b:
# A. {name, expression} \u2192 ExpressionAreaTemplateParameter
# B. {name, expression, drilldown: "X"} \u2192 Expression + Details(\u0420\u0430\u0441\u0448\u0438\u0444\u0440\u043e\u0432\u043a\u0430_X, \u0418\u043c\u044f\u0420\u0435\u0441\u0443\u0440\u0441\u0430, DrillDown)
# C. {name, drilldown: {field, expression, action?}} \u2192 DetailsAreaTemplateParameter \u0441 \u043f\u0440\u043e\u0438\u0437\u0432\u043e\u043b\u044c\u043d\u044b\u043c \u0438\u043c\u0435\u043d\u0435\u043c
def _emit_area_template_parameter(lines, tp, indent):
dd = tp.get('drilldown')
if isinstance(dd, dict):
# \u0424\u043e\u0440\u043c\u0430 C
dd_field = str(dd.get('field', ''))
dd_expr = str(dd.get('expression', ''))
dd_act = str(dd.get('action') or 'DrillDown')
lines.append(f'{indent}<parameter xmlns:dcsat="http://v8.1c.ru/8.1/data-composition-system/area-template" xsi:type="dcsat:DetailsAreaTemplateParameter">')
lines.append(f'{indent}\t<dcsat:name>{esc_xml(str(tp["name"]))}</dcsat:name>')
lines.append(f'{indent}\t<dcsat:fieldExpression>')
lines.append(f'{indent}\t\t<dcsat:field>{esc_xml(dd_field)}</dcsat:field>')
lines.append(f'{indent}\t\t<dcsat:expression>{esc_xml(dd_expr)}</dcsat:expression>')
lines.append(f'{indent}\t</dcsat:fieldExpression>')
lines.append(f'{indent}\t<dcsat:mainAction>{esc_xml(dd_act)}</dcsat:mainAction>')
lines.append(f'{indent}</parameter>')
return
# \u0424\u043e\u0440\u043c\u0430 A \u0438\u043b\u0438 B
lines.append(f'{indent}<parameter xmlns:dcsat="http://v8.1c.ru/8.1/data-composition-system/area-template" xsi:type="dcsat:ExpressionAreaTemplateParameter">')
lines.append(f'{indent}\t<dcsat:name>{esc_xml(str(tp["name"]))}</dcsat:name>')
lines.append(f'{indent}\t<dcsat:expression>{esc_xml(str(tp.get("expression", "")))}</dcsat:expression>')
lines.append(f'{indent}</parameter>')
if dd and isinstance(dd, str):
# \u0424\u043e\u0440\u043c\u0430 B: shortcut \u0420\u0430\u0441\u0448\u0438\u0444\u0440\u043e\u0432\u043a\u0430_<X> + \u0418\u043c\u044f\u0420\u0435\u0441\u0443\u0440\u0441\u0430 + DrillDown
dd_val = dd
lines.append(f'{indent}<parameter xmlns:dcsat="http://v8.1c.ru/8.1/data-composition-system/area-template" xsi:type="dcsat:DetailsAreaTemplateParameter">')
lines.append(f'{indent}\t<dcsat:name>\u0420\u0430\u0441\u0448\u0438\u0444\u0440\u043e\u0432\u043a\u0430_{esc_xml(dd_val)}</dcsat:name>')
lines.append(f'{indent}\t<dcsat:fieldExpression>')
lines.append(f'{indent}\t\t<dcsat:field>\u0418\u043c\u044f\u0420\u0435\u0441\u0443\u0440\u0441\u0430</dcsat:field>')
lines.append(f'{indent}\t\t<dcsat:expression>"{esc_xml(dd_val)}"</dcsat:expression>')
lines.append(f'{indent}\t</dcsat:fieldExpression>')
lines.append(f'{indent}\t<dcsat:mainAction>DrillDown</dcsat:mainAction>')
lines.append(f'{indent}</parameter>')
# === Templates ===
def emit_templates(lines, defn):
if not defn.get('templates'):
return
for t in defn['templates']:
if t.get('rows'):
_emit_area_template_dsl(lines, t)
else:
lines.append('\t<template>')
lines.append(f'\t\t<name>{esc_xml(str(t["name"]))}</name>')
if t.get('template'):
lines.append(f'\t\t{t["template"]}')
if t.get('parameters'):
for tp in t['parameters']:
_emit_area_template_parameter(lines, tp, '\t\t')
lines.append('\t</template>')
# === FieldTemplates ===
# Привязка <fieldTemplate><field/><template/></fieldTemplate> поля к именованному area-template.
# DSL: "fieldTemplates": [{ "field": "X", "template": "Макет1" }, ...]
def emit_field_templates(lines, defn):
if not defn.get('fieldTemplates'):
return
for ft in defn['fieldTemplates']:
lines.append('\t<fieldTemplate>')
lines.append(f'\t\t<field>{esc_xml(str(ft["field"]))}</field>')
lines.append(f'\t\t<template>{esc_xml(str(ft["template"]))}</template>')
lines.append('\t</fieldTemplate>')
# === GroupTemplates ===
def emit_group_templates(lines, defn):
if not defn.get('groupTemplates'):
return
for gt in defn['groupTemplates']:
ttype = str(gt.get('templateType', '')) or 'Header'
is_header = (ttype == 'GroupHeader')
tag = 'groupHeaderTemplate' if is_header else 'groupTemplate'
xml_ttype = 'Header' if is_header else ttype
lines.append(f'\t<{tag}>')
if gt.get('groupName'):
lines.append(f'\t\t<groupName>{esc_xml(str(gt["groupName"]))}</groupName>')
elif gt.get('groupField'):
lines.append(f'\t\t<groupField>{esc_xml(str(gt["groupField"]))}</groupField>')
lines.append(f'\t\t<templateType>{esc_xml(xml_ttype)}</templateType>')
lines.append(f'\t\t<template>{esc_xml(str(gt["template"]))}</template>')
lines.append(f'\t</{tag}>')
# === Settings Variants ===
def emit_selection_item(lines, item, indent):
if isinstance(item, str):
if item == 'Auto':
lines.append(f'{indent}<dcsset:item xsi:type="dcsset:SelectedItemAuto"/>')
else:
lines.append(f'{indent}<dcsset:item xsi:type="dcsset:SelectedItemField">')
lines.append(f'{indent}\t<dcsset:field>{esc_xml(item)}</dcsset:field>')
lines.append(f'{indent}</dcsset:item>')
return
# Object form: { auto: true, use: false } — отключённый Auto в selection
if item.get('auto') is True:
lines.append(f'{indent}<dcsset:item xsi:type="dcsset:SelectedItemAuto">')
if item.get('use') is False:
lines.append(f'{indent}\t<dcsset:use>false</dcsset:use>')
lines.append(f'{indent}</dcsset:item>')
return
if 'folder' in item:
lines.append(f'{indent}<dcsset:item xsi:type="dcsset:SelectedItemFolder">')
if item.get('field'):
lines.append(f'{indent}\t<dcsset:field>{esc_xml(str(item["field"]))}</dcsset:field>')
emit_mltext(lines, f'{indent}\t', 'dcsset:lwsTitle', item['folder'], no_xsi_type=True)
for sub in (item.get('items') or []):
emit_selection_item(lines, sub, f'{indent}\t')
pl = str(item.get('placement') or 'Auto')
lines.append(f'{indent}\t<dcsset:placement>{esc_xml(pl)}</dcsset:placement>')
lines.append(f'{indent}</dcsset:item>')
return
# field with optional title / use=false / viewMode
lines.append(f'{indent}<dcsset:item xsi:type="dcsset:SelectedItemField">')
if item.get('use') is False:
lines.append(f'{indent}\t<dcsset:use>false</dcsset:use>')
lines.append(f'{indent}\t<dcsset:field>{esc_xml(str(item["field"]))}</dcsset:field>')
if item.get('title'):
emit_mltext(lines, f'{indent}\t', 'dcsset:lwsTitle', item['title'], no_xsi_type=True)
if item.get('viewMode'):
lines.append(f'{indent}\t<dcsset:viewMode>{esc_xml(str(item["viewMode"]))}</dcsset:viewMode>')
lines.append(f'{indent}</dcsset:item>')
def emit_selection(lines, items, indent, skip_auto=False, block_view_mode=None, block_user_setting_id=None):
has_items = items and len(items) > 0
has_block_meta = block_view_mode is not None or block_user_setting_id is not None
if not has_items and not has_block_meta:
return
lines.append(f'{indent}<dcsset:selection>')
for item in (items or []):
if skip_auto and isinstance(item, str) and item == 'Auto':
continue
emit_selection_item(lines, item, f'{indent}\t')
if block_view_mode is not None:
lines.append(f'{indent}\t<dcsset:viewMode>{esc_xml(str(block_view_mode))}</dcsset:viewMode>')
if block_user_setting_id is not None:
uid = new_uuid() if str(block_user_setting_id) == 'auto' else str(block_user_setting_id)
lines.append(f'{indent}\t<dcsset:userSettingID>{esc_xml(uid)}</dcsset:userSettingID>')
lines.append(f'{indent}</dcsset:selection>')
def emit_filter_item(lines, item, indent):
if item.get('group'):
# FilterItemGroup
group_type_map = {'And': 'AndGroup', 'Or': 'OrGroup', 'Not': 'NotGroup'}
group_type = group_type_map.get(str(item['group']), f'{item["group"]}Group')
lines.append(f'{indent}<dcsset:item xsi:type="dcsset:FilterItemGroup">')
lines.append(f'{indent}\t<dcsset:groupType>{group_type}</dcsset:groupType>')
if item.get('items'):
for sub in item['items']:
if isinstance(sub, str):
parsed = parse_filter_shorthand(sub)
sub = {'field': parsed['field'], 'op': parsed['op']}
if parsed['use'] is False:
sub['use'] = False
if parsed.get('value') is not None:
sub['value'] = parsed['value']
if parsed.get('valueType'):
sub['valueType'] = parsed['valueType']
if parsed.get('userSettingID'):
sub['userSettingID'] = parsed['userSettingID']
if parsed.get('viewMode'):
sub['viewMode'] = parsed['viewMode']
emit_filter_item(lines, sub, f'{indent}\t')
if item.get('presentation'):
emit_mltext(lines, f'{indent}\t', 'dcsset:presentation', item['presentation'])
if item.get('viewMode'):
lines.append(f'{indent}\t<dcsset:viewMode>{esc_xml(str(item["viewMode"]))}</dcsset:viewMode>')
if item.get('userSettingID'):
guid = new_uuid() if str(item['userSettingID']) == 'auto' else str(item['userSettingID'])
lines.append(f'{indent}\t<dcsset:userSettingID>{esc_xml(guid)}</dcsset:userSettingID>')
if item.get('userSettingPresentation'):
emit_mltext(lines, f'{indent}\t', 'dcsset:userSettingPresentation', item['userSettingPresentation'])
lines.append(f'{indent}</dcsset:item>')
return
# FilterItemComparison
lines.append(f'{indent}<dcsset:item xsi:type="dcsset:FilterItemComparison">')
if item.get('use') is False:
lines.append(f'{indent}\t<dcsset:use>false</dcsset:use>')
lines.append(f'{indent}\t<dcsset:left xsi:type="dcscor:Field">{esc_xml(str(item["field"]))}</dcsset:left>')
comp_type = COMPARISON_TYPES.get(str(item.get('op', '')), str(item.get('op', '')))
lines.append(f'{indent}\t<dcsset:comparisonType>{esc_xml(comp_type)}</dcsset:comparisonType>')
# Right value: один, несколько (InList) или ValueListType (пустой list-placeholder)
val = item.get('value')
val_is_array = isinstance(val, list)
if val_is_array:
if len(val) == 0:
# Пустой массив → пустой ValueListType placeholder
lines.append(f'{indent}\t<dcsset:right xsi:type="v8:ValueListType">')
lines.append(f'{indent}\t\t<v8:valueType/>')
lines.append(f'{indent}\t\t<v8:lastId xsi:type="xs:decimal">-1</v8:lastId>')
lines.append(f'{indent}\t</dcsset:right>')
else:
for v in val:
vt = str(item.get('valueType', '')) if item.get('valueType') else ''
if not vt:
if isinstance(v, bool):
vt = 'xs:boolean'
elif isinstance(v, (int, float)):
vt = 'xs:decimal'
elif re.match(r'^\d{4}-\d{2}-\d{2}T', str(v)):
vt = 'xs:dateTime'
elif re.match(r'^-?\d+(\.\d+)?$', str(v)):
vt = 'xs:decimal'
elif re.match(r'^(Перечисление|Справочник|ПланСчетов|Документ|ПланВидовХарактеристик|ПланВидовРасчета|БизнесПроцесс|Задача|РегистрСведений|ПланОбмена|Catalog|Enum|Document|ChartOfAccounts|ChartOfCharacteristicTypes|ChartOfCalculationTypes|BusinessProcess|Task|InformationRegister|ExchangePlan)\.', str(v)):
vt = 'dcscor:DesignTimeValue'
else:
vt = 'xs:string'
v_str = str(v).lower() if isinstance(v, bool) else esc_xml(str(v))
lines.append(f'{indent}\t<dcsset:right xsi:type="{vt}">{v_str}</dcsset:right>')
elif val is not None:
vt = str(item.get('valueType', '')) if item.get('valueType') else ''
if not vt:
v = val
if isinstance(v, bool):
vt = 'xs:boolean'
elif isinstance(v, (int, float)):
vt = 'xs:decimal'
elif re.match(r'^\d{4}-\d{2}-\d{2}T', str(v)):
vt = 'xs:dateTime'
elif re.match(r'^-?\d+(\.\d+)?$', str(v)):
vt = 'xs:decimal'
else:
vt = 'xs:string'
if isinstance(val, bool):
v_str = str(val).lower()
else:
v_str = esc_xml(str(val))
lines.append(f'{indent}\t<dcsset:right xsi:type="{vt}">{v_str}</dcsset:right>')
if item.get('presentation'):
emit_mltext(lines, f'{indent}\t', 'dcsset:presentation', item["presentation"])
if item.get('viewMode'):
lines.append(f'{indent}\t<dcsset:viewMode>{esc_xml(str(item["viewMode"]))}</dcsset:viewMode>')
if item.get('userSettingID'):
uid = new_uuid() if str(item['userSettingID']) == 'auto' else str(item['userSettingID'])
lines.append(f'{indent}\t<dcsset:userSettingID>{esc_xml(uid)}</dcsset:userSettingID>')
if item.get('userSettingPresentation'):
emit_mltext(lines, f'{indent}\t', 'dcsset:userSettingPresentation', item["userSettingPresentation"])
lines.append(f'{indent}</dcsset:item>')
def emit_filter(lines, items, indent, block_view_mode=None, block_user_setting_id=None):
has_items = items and len(items) > 0
has_block_meta = block_view_mode is not None or block_user_setting_id is not None
if not has_items and not has_block_meta:
return
lines.append(f'{indent}<dcsset:filter>')
for item in (items or []):
if isinstance(item, str):
parsed = parse_filter_shorthand(item)
filter_obj = {
'field': parsed['field'],
'op': parsed['op'],
}
if parsed['use'] is False:
filter_obj['use'] = False
if parsed.get('value') is not None:
filter_obj['value'] = parsed['value']
if parsed.get('valueType'):
filter_obj['valueType'] = parsed['valueType']
if parsed.get('userSettingID'):
filter_obj['userSettingID'] = parsed['userSettingID']
if parsed.get('viewMode'):
filter_obj['viewMode'] = parsed['viewMode']
emit_filter_item(lines, filter_obj, f'{indent}\t')
else:
emit_filter_item(lines, item, f'{indent}\t')
if block_view_mode is not None:
lines.append(f'{indent}\t<dcsset:viewMode>{esc_xml(str(block_view_mode))}</dcsset:viewMode>')
if block_user_setting_id is not None:
uid = new_uuid() if str(block_user_setting_id) == 'auto' else str(block_user_setting_id)
lines.append(f'{indent}\t<dcsset:userSettingID>{esc_xml(uid)}</dcsset:userSettingID>')
lines.append(f'{indent}</dcsset:filter>')
def emit_order(lines, items, indent, skip_auto=False, block_view_mode=None, block_user_setting_id=None):
has_items = items and len(items) > 0
has_block_meta = block_view_mode is not None or block_user_setting_id is not None
if not has_items and not has_block_meta:
return
lines.append(f'{indent}<dcsset:order>')
for item in (items or []):
if isinstance(item, str):
if item == 'Auto':
if not skip_auto:
lines.append(f'{indent}\t<dcsset:item xsi:type="dcsset:OrderItemAuto"/>')
else:
parts = item.split()
field = parts[0]
direction = 'Asc'
if len(parts) > 1 and re.match(r'(?i)^desc$', parts[1]):
direction = 'Desc'
elif len(parts) > 1 and re.match(r'(?i)^asc$', parts[1]):
direction = 'Asc'
lines.append(f'{indent}\t<dcsset:item xsi:type="dcsset:OrderItemField">')
lines.append(f'{indent}\t\t<dcsset:field>{esc_xml(field)}</dcsset:field>')
lines.append(f'{indent}\t\t<dcsset:orderType>{direction}</dcsset:orderType>')
lines.append(f'{indent}\t</dcsset:item>')
else:
# Object form: { field, direction, viewMode }
if str(item.get('field', '')) == 'Auto' or item.get('type') == 'auto':
if not skip_auto:
lines.append(f'{indent}\t<dcsset:item xsi:type="dcsset:OrderItemAuto"/>')
continue
d = str(item.get('direction', 'Asc'))
if re.match(r'(?i)^desc$', d):
d = 'Desc'
elif re.match(r'(?i)^asc$', d):
d = 'Asc'
lines.append(f'{indent}\t<dcsset:item xsi:type="dcsset:OrderItemField">')
if item.get('use') is False:
lines.append(f'{indent}\t\t<dcsset:use>false</dcsset:use>')
lines.append(f'{indent}\t\t<dcsset:field>{esc_xml(str(item["field"]))}</dcsset:field>')
lines.append(f'{indent}\t\t<dcsset:orderType>{d}</dcsset:orderType>')
if item.get('viewMode'):
lines.append(f'{indent}\t\t<dcsset:viewMode>{esc_xml(str(item["viewMode"]))}</dcsset:viewMode>')
lines.append(f'{indent}\t</dcsset:item>')
if block_view_mode is not None:
lines.append(f'{indent}\t<dcsset:viewMode>{esc_xml(str(block_view_mode))}</dcsset:viewMode>')
if block_user_setting_id is not None:
uid = new_uuid() if str(block_user_setting_id) == 'auto' else str(block_user_setting_id)
lines.append(f'{indent}\t<dcsset:userSettingID>{esc_xml(uid)}</dcsset:userSettingID>')
lines.append(f'{indent}</dcsset:order>')
def emit_appearance_value(lines, key, val, indent):
lines.append(f'{indent}<dcscor:item xsi:type="dcsset:SettingsParameterValue">')
# Top-level Line \u0445\u0440\u0430\u043d\u0438\u0442\u0441\u044f \u043f\u043b\u043e\u0441\u043a\u043e ({@type: "Line", width, gap, style, use?, items?}).
# \u041e\u0431\u044b\u0447\u043d\u044b\u0439 wrapper: {value, use?, items?}.
is_top_level_line = isinstance(val, dict) and val.get('@type') == 'Line'
use_wrapper = False
inner_val = val
nested_items = None
if is_top_level_line:
if val.get('use') is False:
use_wrapper = True
nested_items = val.get('items')
elif isinstance(val, dict) and 'value' in val:
inner_val = val['value']
if val.get('use') is False:
use_wrapper = True
nested_items = val.get('items')
if use_wrapper:
lines.append(f'{indent}\t<dcscor:use>false</dcscor:use>')
lines.append(f'{indent}\t<dcscor:parameter>{esc_xml(key)}</dcscor:parameter>')
# Line dict ({@type: "Line", width, gap, style}) \u2192 <dcscor:value xsi:type="v8ui:Line" ...>
if isinstance(inner_val, dict) and inner_val.get('@type') == 'Line':
lw = inner_val.get('width', 0)
lg = 'true' if inner_val.get('gap') else 'false'
ls = str(inner_val.get('style', 'None'))
lines.append(f'{indent}\t<dcscor:value xsi:type="v8ui:Line" width="{lw}" gap="{lg}">')
lines.append(f'{indent}\t\t<v8ui:style xsi:type="v8ui:SpreadsheetDocumentCellLineType">{esc_xml(ls)}</v8ui:style>')
lines.append(f'{indent}\t</dcscor:value>')
# Font dict ({@type: "Font", ref, faceName, height, bold, ...}) \u2192 <dcscor:value xsi:type="v8ui:Font" .../>
elif isinstance(inner_val, dict) and inner_val.get('@type') == 'Font':
attr_parts = []
for attr_name in ('ref', 'faceName', 'height', 'bold', 'italic', 'underline', 'strikeout', 'kind', 'scale'):
if attr_name in inner_val:
attr_parts.append(f'{attr_name}="{esc_xml(str(inner_val[attr_name]))}"')
lines.append(f'{indent}\t<dcscor:value xsi:type="v8ui:Font" {" ".join(attr_parts)}/>')
elif isinstance(inner_val, dict):
emit_mltext(lines, f'{indent}\t', 'dcscor:value', inner_val)
else:
actual_val = str(inner_val) if inner_val is not None else ''
# \u041f\u0430\u0440\u0430\u043c\u0435\u0442\u0440-\u0441\u043f\u0435\u0446\u0438\u0444\u0438\u0447\u043d\u044b\u0439 \u0442\u0438\u043f \u0434\u043b\u044f \u0438\u0437\u0432\u0435\u0441\u0442\u043d\u044b\u0445 appearance keys
key_type_map = {
'\u0420\u0430\u0437\u043c\u0435\u0449\u0435\u043d\u0438\u0435': 'dcscor:DataCompositionTextPlacementType',
'\u0413\u043e\u0440\u0438\u0437\u043e\u043d\u0442\u0430\u043b\u044c\u043d\u043e\u0435\u041f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0435': 'v8ui:HorizontalAlign',
'\u0412\u0435\u0440\u0442\u0438\u043a\u0430\u043b\u044c\u043d\u043e\u0435\u041f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0435': 'v8ui:VerticalAlign',
'\u041e\u0440\u0438\u0435\u043d\u0442\u0430\u0446\u0438\u044f\u0422\u0435\u043a\u0441\u0442\u0430': 'xs:decimal',
'\u0420\u0430\u0441\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0435\u0418\u0442\u043e\u0433\u043e\u0432': 'dcscor:DataCompositionTotalPlacement',
'\u0422\u0438\u043f\u041c\u0430\u043a\u0435\u0442\u0430': 'dcsset:DataCompositionGroupTemplateType',
}
key_type = key_type_map.get(key)
if key_type:
lines.append(f'{indent}\t<dcscor:value xsi:type="{key_type}">{esc_xml(actual_val)}</dcscor:value>')
elif re.match(r'^(style|web|win):', actual_val):
# Внутри <dcsset:settings> префиксы style:/web:/win:/sys: уже объявлены на корне,
# локальный xmlns не нужен — эмитим short form.
lines.append(f'{indent}\t<dcscor:value xsi:type="v8ui:Color">{esc_xml(actual_val)}</dcscor:value>')
elif actual_val == 'true' or actual_val == 'false':
lines.append(f'{indent}\t<dcscor:value xsi:type="xs:boolean">{actual_val}</dcscor:value>')
elif key in ('\u0422\u0435\u043a\u0441\u0442', '\u0417\u0430\u0433\u043e\u043b\u043e\u0432\u043e\u043a', '\u0424\u043e\u0440\u043c\u0430\u0442'):
emit_mltext(lines, f'{indent}\t', 'dcscor:value', actual_val)
elif re.match(r'^-?\d+(\.\d+)?$', actual_val):
lines.append(f'{indent}\t<dcscor:value xsi:type="xs:decimal">{actual_val}</dcscor:value>')
elif key in ('\u0426\u0432\u0435\u0442\u0422\u0435\u043a\u0441\u0442\u0430', '\u0426\u0432\u0435\u0442\u0424\u043e\u043d\u0430', '\u0426\u0432\u0435\u0442\u0413\u0440\u0430\u043d\u0438\u0446\u044b'):
lines.append(f'{indent}\t<dcscor:value xsi:type="v8ui:Color">{esc_xml(actual_val)}</dcscor:value>')
else:
lines.append(f'{indent}\t<dcscor:value xsi:type="xs:string">{esc_xml(actual_val)}</dcscor:value>')
# Nested SettingsParameterValue items (СтильГраницы.Сверху/.Снизу/.Слева/.Справа).
if nested_items and isinstance(nested_items, dict):
for nk, nv in nested_items.items():
emit_appearance_value(lines, nk, nv, f'{indent}\t')
lines.append(f'{indent}</dcscor:item>')
def emit_conditional_appearance(lines, items, indent, block_view_mode=None, block_user_setting_id=None):
has_items = items and len(items) > 0
has_block_meta = block_view_mode is not None or block_user_setting_id is not None
if not has_items and not has_block_meta:
return
lines.append(f'{indent}<dcsset:conditionalAppearance>')
for ca in (items or []):
lines.append(f'{indent}\t<dcsset:item>')
if ca.get('use') is False:
lines.append(f'{indent}\t\t<dcsset:use>false</dcsset:use>')
# Selection
if ca.get('selection') and len(ca['selection']) > 0:
lines.append(f'{indent}\t\t<dcsset:selection>')
for sel in ca['selection']:
lines.append(f'{indent}\t\t\t<dcsset:item>')
lines.append(f'{indent}\t\t\t\t<dcsset:field>{esc_xml(str(sel))}</dcsset:field>')
lines.append(f'{indent}\t\t\t</dcsset:item>')
lines.append(f'{indent}\t\t</dcsset:selection>')
else:
lines.append(f'{indent}\t\t<dcsset:selection/>')
# Filter
if ca.get('filter') and len(ca['filter']) > 0:
emit_filter(lines, ca['filter'], f'{indent}\t\t')
else:
# Платформа эмитит пустой <dcsset:filter/> на каждом condApp item
lines.append(f'{indent}\t\t<dcsset:filter/>')
# Appearance
if ca.get('appearance'):
lines.append(f'{indent}\t\t<dcsset:appearance>')
for k, v in ca['appearance'].items():
emit_appearance_value(lines, k, v, f'{indent}\t\t\t')
lines.append(f'{indent}\t\t</dcsset:appearance>')
# Presentation
if ca.get('presentation'):
# Multilang dict {ru, en, ...} → LocalStringType; иначе — xs:string
if isinstance(ca['presentation'], dict):
emit_mltext(lines, f'{indent}\t\t', 'dcsset:presentation', ca['presentation'])
else:
lines.append(f'{indent}\t\t<dcsset:presentation xsi:type="xs:string">{esc_xml(str(ca["presentation"]))}</dcsset:presentation>')
if ca.get('viewMode'):
lines.append(f'{indent}\t\t<dcsset:viewMode>{esc_xml(str(ca["viewMode"]))}</dcsset:viewMode>')
# UserSettingID
if ca.get('userSettingID'):
uid = new_uuid() if str(ca['userSettingID']) == 'auto' else str(ca['userSettingID'])
lines.append(f'{indent}\t\t<dcsset:userSettingID>{esc_xml(uid)}</dcsset:userSettingID>')
if ca.get('userSettingPresentation'):
emit_mltext(lines, f'{indent}\t\t', 'dcsset:userSettingPresentation', ca['userSettingPresentation'])
# useInXxx — список областей где правило НЕ применяется (DontUse)
if ca.get('useInDontUse'):
use_in_order = ['group', 'hierarchicalGroup', 'overall',
'fieldsHeader', 'header', 'parameters', 'filter',
'resourceFieldsHeader', 'overallHeader', 'overallResourceFieldsHeader']
s = set(ca['useInDontUse'])
for n in use_in_order:
if n in s:
tag = 'useIn' + n[0].upper() + n[1:]
lines.append(f'{indent}\t\t<dcsset:{tag}>DontUse</dcsset:{tag}>')
lines.append(f'{indent}\t</dcsset:item>')
if block_view_mode is not None:
lines.append(f'{indent}\t<dcsset:viewMode>{esc_xml(str(block_view_mode))}</dcsset:viewMode>')
if block_user_setting_id is not None:
uid = new_uuid() if str(block_user_setting_id) == 'auto' else str(block_user_setting_id)
lines.append(f'{indent}\t<dcsset:userSettingID>{esc_xml(uid)}</dcsset:userSettingID>')
lines.append(f'{indent}</dcsset:conditionalAppearance>')
def emit_output_parameters(lines, params, indent):
if not params:
return
lines.append(f'{indent}<dcsset:outputParameters>')
for key, val in params.items():
# wrapper {value, valueType?, use?, items?, viewMode?, userSettingID?, userSettingPresentation?}
use_false = False
wrap_vm = None
wrap_usid = None
wrap_usp = None
wrap_vt = None
wrap_items = None
if isinstance(val, dict) and 'value' in val:
wrap_vt = val.get('valueType')
if val.get('use') is False: use_false = True
wrap_items = val.get('items')
wrap_vm = val.get('viewMode')
wrap_usid = val.get('userSettingID')
wrap_usp = val.get('userSettingPresentation')
val = val['value']
is_font_dict = isinstance(val, dict) and val.get('@type') == 'Font'
if wrap_vt:
ptype = wrap_vt
else:
ptype = OUTPUT_PARAM_TYPES.get(key, 'xs:string')
# Auto-promote to mltext if value is a multilang dict (but not Font)
if not is_font_dict and isinstance(val, dict):
ptype = 'mltext'
lines.append(f'{indent}\t<dcscor:item xsi:type="dcsset:SettingsParameterValue">')
if use_false:
lines.append(f'{indent}\t\t<dcscor:use>false</dcscor:use>')
lines.append(f'{indent}\t\t<dcscor:parameter>{esc_xml(key)}</dcscor:parameter>')
if is_font_dict:
attr_parts = []
for attr_name in ('ref', 'faceName', 'height', 'bold', 'italic', 'underline', 'strikeout', 'kind', 'scale'):
if attr_name in val:
attr_parts.append(f'{attr_name}="{esc_xml(str(val[attr_name]))}"')
lines.append(f'{indent}\t\t<dcscor:value xsi:type="v8ui:Font" {" ".join(attr_parts)}/>')
elif ptype == 'mltext':
emit_mltext(lines, f'{indent}\t\t', 'dcscor:value', val)
else:
lines.append(f'{indent}\t\t<dcscor:value xsi:type="{ptype}">{esc_xml(str(val))}</dcscor:value>')
# Nested sub-параметры (ТипДиаграммы.ВидПодписей и т.п.).
# valueType: строка → xsi:type=string, объект {uri, name} → локальный xmlns:dN.
if wrap_items and isinstance(wrap_items, dict):
for sub_name, sub_wrap in wrap_items.items():
sub_val = sub_wrap
sub_vt = 'xs:string'
sub_use_false = False
sub_uri = None
sub_local_name = None
if isinstance(sub_wrap, dict):
if 'value' in sub_wrap:
sub_val = sub_wrap['value']
if 'valueType' in sub_wrap:
vt = sub_wrap['valueType']
if isinstance(vt, dict) and 'uri' in vt:
sub_uri = str(vt['uri'])
sub_local_name = str(vt['name'])
else:
sub_vt = str(vt)
if sub_wrap.get('use') is False:
sub_use_false = True
lines.append(f'{indent}\t\t<dcscor:item xsi:type="dcsset:SettingsParameterValue">')
if sub_use_false:
lines.append(f'{indent}\t\t\t<dcscor:use>false</dcscor:use>')
lines.append(f'{indent}\t\t\t<dcscor:parameter>{esc_xml(sub_name)}</dcscor:parameter>')
if sub_uri:
lines.append(f'{indent}\t\t\t<dcscor:value xmlns:dN="{sub_uri}" xsi:type="dN:{sub_local_name}">{esc_xml(str(sub_val))}</dcscor:value>')
else:
lines.append(f'{indent}\t\t\t<dcscor:value xsi:type="{sub_vt}">{esc_xml(str(sub_val))}</dcscor:value>')
lines.append(f'{indent}\t\t</dcscor:item>')
if wrap_vm:
lines.append(f'{indent}\t\t<dcsset:viewMode>{esc_xml(str(wrap_vm))}</dcsset:viewMode>')
if wrap_usid:
uid = new_uuid() if str(wrap_usid) == 'auto' else str(wrap_usid)
lines.append(f'{indent}\t\t<dcsset:userSettingID>{esc_xml(uid)}</dcsset:userSettingID>')
if wrap_usp:
emit_mltext(lines, f'{indent}\t\t', 'dcsset:userSettingPresentation', wrap_usp)
lines.append(f'{indent}\t</dcscor:item>')
lines.append(f'{indent}</dcsset:outputParameters>')
def emit_data_parameters(lines, items, indent):
if not items or len(items) == 0:
return
lines.append(f'{indent}<dcsset:dataParameters>')
for dp in items:
# Support string shorthand
if isinstance(dp, str):
parsed = parse_data_param_shorthand(dp)
dp = {
'parameter': parsed['parameter'],
}
if parsed.get('value') is not None:
dp['value'] = parsed['value']
if parsed['use'] is False:
dp['use'] = False
if parsed.get('userSettingID'):
dp['userSettingID'] = parsed['userSettingID']
if parsed.get('viewMode'):
dp['viewMode'] = parsed['viewMode']
lines.append(f'{indent}\t<dcscor:item xsi:type="dcsset:SettingsParameterValue">')
if dp.get('use') is False:
lines.append(f'{indent}\t\t<dcscor:use>false</dcscor:use>')
lines.append(f'{indent}\t\t<dcscor:parameter>{esc_xml(str(dp["parameter"]))}</dcscor:parameter>')
# Value
if dp.get('nilValue') is True:
lines.append(f'{indent}\t\t<dcscor:value xsi:nil="true"/>')
elif is_empty_value(dp.get('value')):
emit_empty_value(lines, str(dp.get('valueType') or ''), f'{indent}\t\t', 'dcscor:', False)
elif dp.get('value') is not None:
val = dp['value']
vtype = str(dp.get('valueType') or '')
if isinstance(val, dict) and val.get('variant'):
# Standard{Period,BeginningDate} — различаем по форме value:
# {variant, date} → SBD
# {variant, startDate, endDate} → SP с датами
# {variant} only → инференс по имени (BeginningOf* → SBD, иначе SP)
variant_str = str(val['variant'])
has_date = 'date' in val
has_sd = 'startDate' in val
is_sbd = has_date or (not has_sd and variant_str.startswith('BeginningOf'))
if is_sbd:
lines.append(f'{indent}\t\t<dcscor:value xsi:type="v8:StandardBeginningDate">')
lines.append(f'{indent}\t\t\t<v8:variant xsi:type="v8:StandardBeginningDateVariant">{esc_xml(variant_str)}</v8:variant>')
if variant_str == 'Custom':
d = str(val.get('date') or '0001-01-01T00:00:00')
lines.append(f'{indent}\t\t\t<v8:date>{esc_xml(d)}</v8:date>')
lines.append(f'{indent}\t\t</dcscor:value>')
else:
# StandardPeriod — platform-pattern: startDate/endDate ТОЛЬКО для variant=Custom.
lines.append(f'{indent}\t\t<dcscor:value xsi:type="v8:StandardPeriod">')
lines.append(f'{indent}\t\t\t<v8:variant xsi:type="v8:StandardPeriodVariant">{esc_xml(variant_str)}</v8:variant>')
if variant_str == 'Custom':
sd = str(val.get('startDate') or '0001-01-01T00:00:00')
ed = str(val.get('endDate') or '0001-01-01T00:00:00')
lines.append(f'{indent}\t\t\t<v8:startDate>{esc_xml(sd)}</v8:startDate>')
lines.append(f'{indent}\t\t\t<v8:endDate>{esc_xml(ed)}</v8:endDate>')
lines.append(f'{indent}\t\t</dcscor:value>')
elif re.match(r'^[a-zA-Z]+:', vtype):
# Полный xsi:type из decompile (например "xs:boolean", "dcscor:DesignTimeValue").
v_str = str(val).lower() if isinstance(val, bool) else str(val)
lines.append(f'{indent}\t\t<dcscor:value xsi:type="{vtype}">{esc_xml(v_str)}</dcscor:value>')
elif vtype == 'boolean' or isinstance(val, bool):
bv = str(val).lower()
lines.append(f'{indent}\t\t<dcscor:value xsi:type="xs:boolean">{esc_xml(bv)}</dcscor:value>')
elif re.match(r'^date', vtype) or re.match(r'^\d{4}-\d{2}-\d{2}T', str(val)):
lines.append(f'{indent}\t\t<dcscor:value xsi:type="xs:dateTime">{esc_xml(str(val))}</dcscor:value>')
elif re.match(r'^decimal', vtype):
lines.append(f'{indent}\t\t<dcscor:value xsi:type="xs:decimal">{esc_xml(str(val))}</dcscor:value>')
elif re.match(r'^string', vtype):
lines.append(f'{indent}\t\t<dcscor:value xsi:type="xs:string">{esc_xml(str(val))}</dcscor:value>')
elif re.match(r'^(\u041f\u043b\u0430\u043d\u0421\u0447\u0435\u0442\u043e\u0432|\u0421\u043f\u0440\u0430\u0432\u043e\u0447\u043d\u0438\u043a|\u041f\u0435\u0440\u0435\u0447\u0438\u0441\u043b\u0435\u043d\u0438\u0435|\u0414\u043e\u043a\u0443\u043c\u0435\u043d\u0442|\u041f\u043b\u0430\u043d\u0412\u0438\u0434\u043e\u0432\u0425\u0430\u0440\u0430\u043a\u0442\u0435\u0440\u0438\u0441\u0442\u0438\u043a|\u041f\u043b\u0430\u043d\u0412\u0438\u0434\u043e\u0432\u0420\u0430\u0441\u0447\u0435\u0442\u0430|\u0411\u0438\u0437\u043d\u0435\u0441\u041f\u0440\u043e\u0446\u0435\u0441\u0441|\u0417\u0430\u0434\u0430\u0447\u0430|\u0420\u0435\u0433\u0438\u0441\u0442\u0440\u0421\u0432\u0435\u0434\u0435\u043d\u0438\u0439|\u041f\u043b\u0430\u043d\u041e\u0431\u043c\u0435\u043d\u0430)\.', str(val)) or re.match(r'^(ChartOfAccounts|Catalog|Enum|Document|ChartOfCharacteristicTypes|ChartOfCalculationTypes|BusinessProcess|Task|InformationRegister|ExchangePlan)\.', str(val)):
lines.append(f'{indent}\t\t<dcscor:value xsi:type="dcscor:DesignTimeValue">{esc_xml(str(val))}</dcscor:value>')
else:
lines.append(f'{indent}\t\t<dcscor:value xsi:type="xs:string">{esc_xml(str(val))}</dcscor:value>')
if dp.get('viewMode'):
lines.append(f'{indent}\t\t<dcsset:viewMode>{esc_xml(str(dp["viewMode"]))}</dcsset:viewMode>')
if dp.get('userSettingID'):
uid = new_uuid() if str(dp['userSettingID']) == 'auto' else str(dp['userSettingID'])
lines.append(f'{indent}\t\t<dcsset:userSettingID>{esc_xml(uid)}</dcsset:userSettingID>')
if dp.get('userSettingPresentation'):
emit_mltext(lines, f'{indent}\t\t', 'dcsset:userSettingPresentation', dp["userSettingPresentation"])
lines.append(f'{indent}\t</dcscor:item>')
lines.append(f'{indent}</dcsset:dataParameters>')
# === Structure items (recursive) ===
def emit_group_items(lines, group_by, indent):
if not group_by or len(group_by) == 0:
return
lines.append(f'{indent}<dcsset:groupItems>')
for field in group_by:
if isinstance(field, str):
if field == 'Auto':
lines.append(f'{indent}\t<dcsset:item xsi:type="dcsset:GroupItemAuto"/>')
continue
lines.append(f'{indent}\t<dcsset:item xsi:type="dcsset:GroupItemField">')
lines.append(f'{indent}\t\t<dcsset:field>{esc_xml(field)}</dcsset:field>')
lines.append(f'{indent}\t\t<dcsset:groupType>Items</dcsset:groupType>')
lines.append(f'{indent}\t\t<dcsset:periodAdditionType>None</dcsset:periodAdditionType>')
lines.append(f'{indent}\t\t<dcsset:periodAdditionBegin xsi:type="xs:dateTime">0001-01-01T00:00:00</dcsset:periodAdditionBegin>')
lines.append(f'{indent}\t\t<dcsset:periodAdditionEnd xsi:type="xs:dateTime">0001-01-01T00:00:00</dcsset:periodAdditionEnd>')
lines.append(f'{indent}\t</dcsset:item>')
else:
lines.append(f'{indent}\t<dcsset:item xsi:type="dcsset:GroupItemField">')
lines.append(f'{indent}\t\t<dcsset:field>{esc_xml(str(field["field"]))}</dcsset:field>')
gt = str(field.get('groupType', 'Items'))
lines.append(f'{indent}\t\t<dcsset:groupType>{esc_xml(gt)}</dcsset:groupType>')
pat = str(field.get('periodAdditionType', 'None'))
lines.append(f'{indent}\t\t<dcsset:periodAdditionType>{esc_xml(pat)}</dcsset:periodAdditionType>')
# Auto-detect: ISO date → xs:dateTime, иначе → dcscor:Field (path).
pab = str(field.get('periodAdditionBegin', '0001-01-01T00:00:00'))
pae = str(field.get('periodAdditionEnd', '0001-01-01T00:00:00'))
pab_t = 'xs:dateTime' if re.match(r'^\d{4}-\d{2}-\d{2}T', pab) else 'dcscor:Field'
pae_t = 'xs:dateTime' if re.match(r'^\d{4}-\d{2}-\d{2}T', pae) else 'dcscor:Field'
lines.append(f'{indent}\t\t<dcsset:periodAdditionBegin xsi:type="{pab_t}">{esc_xml(pab)}</dcsset:periodAdditionBegin>')
lines.append(f'{indent}\t\t<dcsset:periodAdditionEnd xsi:type="{pae_t}">{esc_xml(pae)}</dcsset:periodAdditionEnd>')
lines.append(f'{indent}\t</dcsset:item>')
lines.append(f'{indent}</dcsset:groupItems>')
def parse_structure_shorthand(s):
segments = re.split(r'\s*>\s*', s)
innermost = None
for i in range(len(segments) - 1, -1, -1):
seg = segments[i].strip()
group = {'type': 'group'}
if re.match(r'(?i)^(details|\u0434\u0435\u0442\u0430\u043b\u0438)$', seg):
group['groupBy'] = []
else:
# Named group: "ИмяГруппы[Поле]"
m_named = re.match(r'^(.+)\[(.+)\]$', seg)
if m_named:
group['name'] = m_named.group(1).strip()
group['groupBy'] = [m_named.group(2).strip()]
else:
group['groupBy'] = [seg]
# Платформа в каждую группировку кладёт авто-поле выбора и авто-порядок;
# shorthand должен соответствовать ручному добавлению группировки в конфигураторе.
group['selection'] = ['Auto']
group['order'] = ['Auto']
if innermost is not None:
group['children'] = [innermost]
innermost = group
if innermost:
return [innermost]
return []
def emit_user_fields(lines, items, indent):
if not items or len(items) == 0:
return
lines.append(f'{indent}<dcsset:userFields>')
for uf in items:
u_type = 'UserFieldCase' if uf.get('cases') is not None else 'UserFieldExpression'
lines.append(f'{indent}\t<dcsset:item xsi:type="dcsset:{u_type}">')
if uf.get('dataPath'):
lines.append(f'{indent}\t\t<dcsset:dataPath>{esc_xml(str(uf["dataPath"]))}</dcsset: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')
if d is not None:
if 'expression' in d:
v = str(d['expression'])
lines.append(f'{indent}\t\t<dcsset:detailExpression>{esc_xml(v)}</dcsset:detailExpression>' if v else f'{indent}\t\t<dcsset:detailExpression/>')
if 'presentation' in d:
v = str(d['presentation'])
lines.append(f'{indent}\t\t<dcsset:detailExpressionPresentation>{esc_xml(v)}</dcsset:detailExpressionPresentation>' if v else f'{indent}\t\t<dcsset:detailExpressionPresentation/>')
t = uf.get('total')
if t is not None:
if 'expression' in t:
v = str(t['expression'])
lines.append(f'{indent}\t\t<dcsset:totalExpression>{esc_xml(v)}</dcsset:totalExpression>' if v else f'{indent}\t\t<dcsset:totalExpression/>')
if 'presentation' in t:
v = str(t['presentation'])
lines.append(f'{indent}\t\t<dcsset:totalExpressionPresentation>{esc_xml(v)}</dcsset:totalExpressionPresentation>' if v else f'{indent}\t\t<dcsset:totalExpressionPresentation/>')
else:
cases = uf.get('cases') or []
if len(cases) == 0:
lines.append(f'{indent}\t\t<dcsset:cases/>')
else:
lines.append(f'{indent}\t\t<dcsset:cases>')
for c in cases:
lines.append(f'{indent}\t\t\t<dcsset:item>')
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<dcsset:value xsi:type="xs:boolean">{str(cv).lower()}</dcsset:value>')
elif isinstance(cv, (int, float)):
lines.append(f'{indent}\t\t\t\t<dcsset:value xsi:type="xs:decimal">{cv}</dcsset:value>')
else:
lines.append(f'{indent}\t\t\t\t<dcsset:value xsi:type="xs:string">{esc_xml(str(cv))}</dcsset:value>')
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</dcsset:item>')
lines.append(f'{indent}\t\t</dcsset:cases>')
lines.append(f'{indent}\t</dcsset:item>')
lines.append(f'{indent}</dcsset:userFields>')
def emit_table_axis_block(lines, block, indent, emit_name=True):
"""Shared emitter for table column/row and chart point/series.
Emits name?, groupItems, filter, order, selection, outputParameters,
viewMode?, userSettingID?, userSettingPresentation? — each conditional on
presence in JSON.
"""
if emit_name and block.get('name'):
lines.append(f'{indent}<dcsset:name>{esc_xml(str(block["name"]))}</dcsset:name>')
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('conditionalAppearance'):
emit_conditional_appearance(lines, block['conditionalAppearance'], indent)
if block.get('outputParameters'):
emit_output_parameters(lines, block['outputParameters'], indent)
# nested children (StructureItemGroup внутри table row/column или chart axis).
# Platform-pattern: items внутри row/column/points/series — ВСЕГДА short form (без xsi:type).
if block.get('children'):
for child in block['children']:
emit_structure_item(lines, child, indent, short_group=True)
if block.get('viewMode'):
lines.append(f'{indent}<dcsset:viewMode>{esc_xml(str(block["viewMode"]))}</dcsset:viewMode>')
if block.get('userSettingID'):
uid = new_uuid() if str(block['userSettingID']) == 'auto' else str(block['userSettingID'])
lines.append(f'{indent}<dcsset:userSettingID>{esc_xml(uid)}</dcsset:userSettingID>')
if block.get('userSettingPresentation'):
emit_mltext(lines, indent, 'dcsset:userSettingPresentation', block['userSettingPresentation'])
if block.get('itemsViewMode'):
lines.append(f'{indent}<dcsset:itemsViewMode>{esc_xml(str(block["itemsViewMode"]))}</dcsset:itemsViewMode>')
def emit_structure_item(lines, item, indent, short_group=False):
item_type = str(item.get('type', 'group'))
if item_type == 'group':
# Platform пишет короткую форму (без xsi:type) для groups внутри table row/column,
# explicit StructureItemGroup в остальных случаях.
if short_group:
lines.append(f'{indent}<dcsset:item>')
else:
lines.append(f'{indent}<dcsset:item xsi:type="dcsset:StructureItemGroup">')
if item.get('use') is False:
lines.append(f'{indent}\t<dcsset:use>false</dcsset:use>')
if item.get('name'):
lines.append(f'{indent}\t<dcsset:name>{esc_xml(str(item["name"]))}</dcsset:name>')
emit_group_items(lines, item.get('groupBy') or item.get('groupFields'), f'{indent}\t')
# Emit order/selection only if specified — platform doesn't always emit them on group
if item.get('order'):
emit_order(lines, item['order'], f'{indent}\t', block_view_mode=item.get('orderViewMode'), block_user_setting_id=item.get('orderUserSettingID'))
if item.get('selection'):
emit_selection(lines, item['selection'], f'{indent}\t')
emit_filter(lines, item.get('filter'), f'{indent}\t')
if item.get('conditionalAppearance'):
emit_conditional_appearance(lines, item['conditionalAppearance'], f'{indent}\t')
if item.get('outputParameters'):
emit_output_parameters(lines, item['outputParameters'], f'{indent}\t')
# Nested children — наследуем short_group от родителя.
if item.get('children'):
for child in item['children']:
emit_structure_item(lines, child, f'{indent}\t', short_group=short_group)
# viewMode/itemsViewMode/userSettingID/userSettingPresentation — context-dependent
if item.get('viewMode'):
lines.append(f'{indent}\t<dcsset:viewMode>{esc_xml(str(item["viewMode"]))}</dcsset:viewMode>')
if item.get('userSettingID'):
gid = new_uuid() if str(item['userSettingID']) == 'auto' else str(item['userSettingID'])
lines.append(f'{indent}\t<dcsset:userSettingID>{esc_xml(gid)}</dcsset:userSettingID>')
if item.get('userSettingPresentation'):
emit_mltext(lines, f'{indent}\t', 'dcsset:userSettingPresentation', item['userSettingPresentation'])
if item.get('itemsViewMode'):
lines.append(f'{indent}\t<dcsset:itemsViewMode>{esc_xml(str(item["itemsViewMode"]))}</dcsset:itemsViewMode>')
lines.append(f'{indent}</dcsset:item>')
elif item_type == 'table':
lines.append(f'{indent}<dcsset:item xsi:type="dcsset:StructureItemTable">')
# use=false — отключённая таблица
if item.get('use') is False:
lines.append(f'{indent}\t<dcsset:use>false</dcsset:use>')
if item.get('name'):
lines.append(f'{indent}\t<dcsset:name>{esc_xml(str(item["name"]))}</dcsset:name>')
# Columns
if item.get('columns'):
for col in item['columns']:
lines.append(f'{indent}\t<dcsset:column>')
emit_table_axis_block(lines, col, f'{indent}\t\t')
lines.append(f'{indent}\t</dcsset:column>')
# Rows
if item.get('rows'):
for row in item['rows']:
lines.append(f'{indent}\t<dcsset:row>')
emit_table_axis_block(lines, row, f'{indent}\t\t')
lines.append(f'{indent}\t</dcsset:row>')
# Top-level: selection / conditionalAppearance / outputParameters на самой таблице
if item.get('selection'):
emit_selection(lines, item['selection'], f'{indent}\t')
if item.get('conditionalAppearance'):
emit_conditional_appearance(lines, item['conditionalAppearance'], f'{indent}\t')
if item.get('outputParameters'):
emit_output_parameters(lines, item['outputParameters'], f'{indent}\t')
# columnsViewMode / rowsViewMode — axis-level режим доступности
if item.get('columnsViewMode'):
lines.append(f'{indent}\t<dcsset:columnsViewMode>{esc_xml(str(item["columnsViewMode"]))}</dcsset:columnsViewMode>')
if item.get('rowsViewMode'):
lines.append(f'{indent}\t<dcsset:rowsViewMode>{esc_xml(str(item["rowsViewMode"]))}</dcsset:rowsViewMode>')
# viewMode / userSettingID / userSettingPresentation / itemsViewMode на самой таблице
if item.get('viewMode'):
lines.append(f'{indent}\t<dcsset:viewMode>{esc_xml(str(item["viewMode"]))}</dcsset:viewMode>')
if item.get('userSettingID'):
gid = new_uuid() if str(item['userSettingID']) == 'auto' else str(item['userSettingID'])
lines.append(f'{indent}\t<dcsset:userSettingID>{esc_xml(gid)}</dcsset:userSettingID>')
if item.get('userSettingPresentation'):
emit_mltext(lines, f'{indent}\t', 'dcsset:userSettingPresentation', item['userSettingPresentation'])
if item.get('itemsViewMode'):
lines.append(f'{indent}\t<dcsset:itemsViewMode>{esc_xml(str(item["itemsViewMode"]))}</dcsset:itemsViewMode>')
lines.append(f'{indent}</dcsset:item>')
elif item_type == 'chart':
lines.append(f'{indent}<dcsset:item xsi:type="dcsset:StructureItemChart">')
# use=false — отключённая диаграмма
if item.get('use') is False:
lines.append(f'{indent}\t<dcsset:use>false</dcsset:use>')
if item.get('name'):
lines.append(f'{indent}\t<dcsset:name>{esc_xml(str(item["name"]))}</dcsset:name>')
# Points — single object или массив (multi-series диаграмма)
pts = item.get('points')
if pts:
pts_list = pts if isinstance(pts, list) else [pts]
for pb in pts_list:
lines.append(f'{indent}\t<dcsset:point>')
emit_table_axis_block(lines, pb, f'{indent}\t\t')
lines.append(f'{indent}\t</dcsset:point>')
# Series — single object или массив
srs = item.get('series')
if srs:
srs_list = srs if isinstance(srs, list) else [srs]
for sb in srs_list:
lines.append(f'{indent}\t<dcsset:series>')
emit_table_axis_block(lines, sb, f'{indent}\t\t')
lines.append(f'{indent}\t</dcsset:series>')
# Selection (chart values)
emit_selection(lines, item.get('selection'), f'{indent}\t')
if item.get('outputParameters'):
emit_output_parameters(lines, item['outputParameters'], f'{indent}\t')
# pointsViewMode / seriesViewMode — axis-level режим доступности
if item.get('pointsViewMode'):
lines.append(f'{indent}\t<dcsset:pointsViewMode>{esc_xml(str(item["pointsViewMode"]))}</dcsset:pointsViewMode>')
if item.get('seriesViewMode'):
lines.append(f'{indent}\t<dcsset:seriesViewMode>{esc_xml(str(item["seriesViewMode"]))}</dcsset:seriesViewMode>')
# viewMode / userSettingID / userSettingPresentation / itemsViewMode на самой диаграмме
if item.get('viewMode'):
lines.append(f'{indent}\t<dcsset:viewMode>{esc_xml(str(item["viewMode"]))}</dcsset:viewMode>')
if item.get('userSettingID'):
gid = new_uuid() if str(item['userSettingID']) == 'auto' else str(item['userSettingID'])
lines.append(f'{indent}\t<dcsset:userSettingID>{esc_xml(gid)}</dcsset:userSettingID>')
if item.get('userSettingPresentation'):
emit_mltext(lines, f'{indent}\t', 'dcsset:userSettingPresentation', item['userSettingPresentation'])
if item.get('itemsViewMode'):
lines.append(f'{indent}\t<dcsset:itemsViewMode>{esc_xml(str(item["itemsViewMode"]))}</dcsset:itemsViewMode>')
lines.append(f'{indent}</dcsset:item>')
elif item_type == 'nestedObject':
lines.append(f'{indent}<dcsset:item xsi:type="dcsset:StructureItemNestedObject">')
if item.get('objectID'):
lines.append(f'{indent}\t<dcsset:objectID>{esc_xml(str(item["objectID"]))}</dcsset:objectID>')
lines.append(f'{indent}\t<dcsset:settings>')
s = item.get('settings') or {}
if s.get('selection'): emit_selection(lines, s['selection'], f'{indent}\t\t')
if s.get('filter'): emit_filter(lines, s['filter'], f'{indent}\t\t')
if s.get('order'): emit_order(lines, s['order'], f'{indent}\t\t')
if s.get('conditionalAppearance'): emit_conditional_appearance(lines, s['conditionalAppearance'], f'{indent}\t\t')
if s.get('outputParameters'): emit_output_parameters(lines, s['outputParameters'], f'{indent}\t\t')
lines.append(f'{indent}\t</dcsset:settings>')
lines.append(f'{indent}</dcsset:item>')
def emit_settings_variants(lines, defn):
variants = defn.get('settingsVariants')
# Default variant if none specified
if not variants or len(variants) == 0:
variants = [{
'name': '\u041e\u0441\u043d\u043e\u0432\u043d\u043e\u0439',
'presentation': '\u041e\u0441\u043d\u043e\u0432\u043d\u043e\u0439',
'settings': {
'selection': ['Auto'],
'structure': [{
'type': 'group',
'order': ['Auto'],
'selection': ['Auto'],
}],
},
}]
for v in variants:
lines.append('\t<settingsVariant>')
lines.append(f'\t\t<dcsset:name>{esc_xml(str(v["name"]))}</dcsset:name>')
pres = v.get('presentation') or v.get('title') or v['name']
emit_mltext(lines, '\t\t', 'dcsset:presentation', pres)
lines.append('\t\t<dcsset:settings xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows">')
s = v.get('settings', {})
# Helper: resolve XViewMode/XUserSettingID from settings — emit only if explicitly set
def _block_usid(key):
prop = f'{key}UserSettingID'
return str(s[prop]) if prop in s else None
def _block_vm(key):
prop = f'{key}ViewMode'
if prop in s:
return str(s[prop])
return None
# userFields — пользовательские вычисляемые поля (Expression / Case)
if s.get('userFields'):
emit_user_fields(lines, s['userFields'], '\t\t\t')
# Selection/Filter/Order/CA — эмитим даже если items пустые, но есть
# block-level viewMode/userSettingID. Platform может содержать Auto-items
# на top-level (вместе с явными полями), поэтому не skip_auto.
svm, susid = _block_vm('selection'), _block_usid('selection')
if s.get('selection') or svm is not None or susid is not None:
emit_selection(lines, s.get('selection'), '\t\t\t', block_view_mode=svm, block_user_setting_id=susid)
fvm, fusid = _block_vm('filter'), _block_usid('filter')
if s.get('filter') or fvm is not None or fusid is not None:
emit_filter(lines, s.get('filter'), '\t\t\t', block_view_mode=fvm, block_user_setting_id=fusid)
ovm, ousid = _block_vm('order'), _block_usid('order')
if s.get('order') or ovm is not None or ousid is not None:
emit_order(lines, s.get('order'), '\t\t\t', block_view_mode=ovm, block_user_setting_id=ousid)
cavm, causid = _block_vm('conditionalAppearance'), _block_usid('conditionalAppearance')
if s.get('conditionalAppearance') or cavm is not None or causid is not None:
emit_conditional_appearance(lines, s.get('conditionalAppearance'), '\t\t\t', block_view_mode=cavm, block_user_setting_id=causid)
# OutputParameters (platform does NOT emit <viewMode> on this block)
if s.get('outputParameters'):
emit_output_parameters(lines, s['outputParameters'], '\t\t\t')
# DataParameters
if s.get('dataParameters') == 'auto':
# Auto-generate dataParameters for all non-hidden params.
# Pattern follows 1C Designer / ERP persistence:
# value set (non-default) → emit value, use=true (implicit)
# value missing / Custom period → <use>false</use> + <value xsi:nil="true"/>
auto_dp = []
for ap in _all_params:
if ap['hidden']:
continue
item = {
'parameter': ap['name'],
'userSettingID': 'auto',
}
has_meaningful_value = False
if ap.get('type') == 'StandardPeriod':
variant = 'Custom'
av = ap.get('value')
if av is not None:
if isinstance(av, dict) and av.get('variant'):
variant = str(av['variant'])
elif str(av):
variant = str(av)
item['value'] = {'variant': variant}
if variant != 'Custom':
has_meaningful_value = True
elif not is_empty_value(ap.get('value')):
item['value'] = ap['value']
item['valueType'] = str(ap.get('type') or '')
has_meaningful_value = True
else:
item['nilValue'] = True
if not has_meaningful_value:
item['use'] = False
auto_dp.append(item)
if auto_dp:
emit_data_parameters(lines, auto_dp, '\t\t\t')
elif s.get('dataParameters'):
emit_data_parameters(lines, s['dataParameters'], '\t\t\t')
# Structure (supports string shorthand)
if s.get('structure'):
struct_items = s['structure']
if isinstance(struct_items, str):
struct_items = parse_structure_shorthand(struct_items)
elif isinstance(struct_items, dict):
struct_items = [struct_items]
for item in struct_items:
emit_structure_item(lines, item, '\t\t\t')
# <dcsset:itemsViewMode> on settings — emit only if explicitly set
if s.get('itemsViewMode'):
lines.append(f'\t\t\t<dcsset:itemsViewMode>{esc_xml(str(s["itemsViewMode"]))}</dcsset:itemsViewMode>')
# <dcsset:additionalProperties> — key/value свойства варианта
if s.get('additionalProperties'):
lines.append('\t\t\t<dcsset:additionalProperties>')
for k, v in s['additionalProperties'].items():
lines.append(f'\t\t\t\t<v8:Property name="{esc_xml(str(k))}">')
lines.append(f'\t\t\t\t\t<v8:Value xsi:type="xs:string">{esc_xml(str(v))}</v8:Value>')
lines.append('\t\t\t\t</v8:Property>')
lines.append('\t\t\t</dcsset:additionalProperties>')
lines.append('\t\t</dcsset:settings>')
lines.append('\t</settingsVariant>')
def main():
sys.stdout.reconfigure(encoding="utf-8")
sys.stderr.reconfigure(encoding="utf-8")
parser = argparse.ArgumentParser(description='Compile 1C DCS from JSON', allow_abbrev=False)
parser.add_argument('-DefinitionFile', type=str, default=None)
parser.add_argument('-Value', type=str, default=None)
parser.add_argument('-OutputPath', type=str, required=True)
args = parser.parse_args()
# --- 1. Load and validate JSON ---
if args.DefinitionFile and args.Value:
print("Cannot use both -DefinitionFile and -Value", file=sys.stderr)
sys.exit(1)
if not args.DefinitionFile and not args.Value:
print("Either -DefinitionFile or -Value is required", file=sys.stderr)
sys.exit(1)
if args.DefinitionFile:
def_file = args.DefinitionFile
if not os.path.isabs(def_file):
def_file = os.path.join(os.getcwd(), def_file)
if not os.path.exists(def_file):
print(f"Definition file not found: {def_file}", file=sys.stderr)
sys.exit(1)
with open(def_file, 'r', encoding='utf-8-sig') as f:
json_text = f.read()
else:
json_text = args.Value
defn = json.loads(json_text)
if not defn.get('dataSets') or len(defn['dataSets']) == 0:
print("JSON must have at least one entry in 'dataSets'", file=sys.stderr)
sys.exit(1)
# Base directory for resolving @file references in query
global query_base_dir
query_base_dir = os.path.dirname(def_file) if args.DefinitionFile else os.getcwd()
# Load user style presets
out_path_resolved = args.OutputPath if os.path.isabs(args.OutputPath) else os.path.join(os.getcwd(), args.OutputPath)
load_user_styles(query_base_dir, out_path_resolved)
# --- 2. Resolve defaults ---
# DataSources
data_sources = []
if defn.get('dataSources'):
for ds in defn['dataSources']:
data_sources.append({
'name': str(ds['name']),
'type': str(ds.get('type', 'Local')),
})
else:
data_sources.append({'name': '\u0418\u0441\u0442\u043e\u0447\u043d\u0438\u043a\u0414\u0430\u043d\u043d\u044b\u04451', 'type': 'Local'})
default_source = data_sources[0]['name']
# Auto-name dataSets
ds_index = 1
for ds in defn['dataSets']:
if not ds.get('name'):
ds['name'] = f'\u041d\u0430\u0431\u043e\u0440\u0414\u0430\u043d\u043d\u044b\u0445{ds_index}'
ds_index += 1
# --- 3. Assemble XML ---
lines = []
lines.append('<?xml version="1.0" encoding="UTF-8"?>')
lines.append(
'<DataCompositionSchema xmlns="http://v8.1c.ru/8.1/data-composition-system/schema"'
' xmlns:dcscom="http://v8.1c.ru/8.1/data-composition-system/common"'
' xmlns:dcscor="http://v8.1c.ru/8.1/data-composition-system/core"'
' xmlns:dcsset="http://v8.1c.ru/8.1/data-composition-system/settings"'
' xmlns:v8="http://v8.1c.ru/8.1/data/core"'
' xmlns:v8ui="http://v8.1c.ru/8.1/data/ui"'
' xmlns:xs="http://www.w3.org/2001/XMLSchema"'
' xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">'
)
emit_data_sources(lines, data_sources)
emit_data_sets(lines, defn, default_source)
emit_data_set_links(lines, defn)
emit_calc_fields(lines, defn)
emit_total_fields(lines, defn)
emit_parameters(lines, defn)
emit_templates(lines, defn)
emit_field_templates(lines, defn)
emit_group_templates(lines, defn)
emit_settings_variants(lines, defn)
lines.append('</DataCompositionSchema>')
# --- 4. Write output ---
output_path = args.OutputPath
if not os.path.isabs(output_path):
output_path = os.path.join(os.getcwd(), output_path)
parent_dir = os.path.dirname(output_path)
if parent_dir and not os.path.exists(parent_dir):
os.makedirs(parent_dir, exist_ok=True)
content = '\n'.join(lines) + '\n'
write_utf8_bom(output_path, content)
# --- 5. Statistics ---
ds_count = len(defn['dataSets'])
field_count = 0
for ds in defn['dataSets']:
if ds.get('fields'):
field_count += len(ds['fields'])
calc_count = len(defn['calculatedFields']) if defn.get('calculatedFields') else 0
total_count = len(defn['totalFields']) if defn.get('totalFields') else 0
param_count = len(defn['parameters']) if defn.get('parameters') else 0
variant_count = len(defn['settingsVariants']) if defn.get('settingsVariants') else 1
file_size = os.path.getsize(output_path)
print(f"OK {args.OutputPath}")
print(f" DataSets: {ds_count} Fields: {field_count} Calculated: {calc_count} Totals: {total_count} Params: {param_count} Variants: {variant_count}")
print(f" Size: {file_size} bytes")
if __name__ == '__main__':
main()