#!/usr/bin/env python3
# skd-compile v1.11 — 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('&', '&').replace('<', '<').replace('>', '>').replace('"', '"')
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):
if not text:
lines.append(f"{indent}<{tag}/>")
return
lines.append(f'{indent}<{tag} xsi:type="v8:LocalStringType">')
lines.append(f"{indent}\t")
lines.append(f"{indent}\t\tru")
lines.append(f"{indent}\t\t{esc_xml(text)}")
lines.append(f"{indent}\t")
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",
"\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_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}xs:boolean')
return
# string or string(N)
m = re.match(r'^string(\((\d+)\))?$', type_str)
if m:
length = m.group(2) if m.group(2) else '0'
lines.append(f'{indent}xs:string')
lines.append(f'{indent}')
lines.append(f'{indent}\t{length}')
lines.append(f'{indent}\tVariable')
lines.append(f'{indent}')
return
# decimal(D,F) or decimal(D,F,nonneg)
m = re.match(r'^decimal\((\d+),(\d+)(,nonneg)?\)$', type_str)
if m:
digits = m.group(1)
fraction = m.group(2)
sign = 'Nonnegative' if m.group(3) else 'Any'
lines.append(f'{indent}xs:decimal')
lines.append(f'{indent}')
lines.append(f'{indent}\t{digits}')
lines.append(f'{indent}\t{fraction}')
lines.append(f'{indent}\t{sign}')
lines.append(f'{indent}')
return
# date / dateTime
m = re.match(r'^(date|dateTime)$', type_str)
if m:
fractions_map = {'date': 'Date', 'dateTime': 'DateTime'}
fractions = fractions_map[type_str]
lines.append(f'{indent}xs:dateTime')
lines.append(f'{indent}')
lines.append(f'{indent}\t{fractions}')
lines.append(f'{indent}')
return
# StandardPeriod
if type_str == 'StandardPeriod':
lines.append(f'{indent}v8:StandardPeriod')
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}d5p1:{esc_xml(type_str)}')
return
# Fallback -- assume dot-qualified types are also config references
if '.' in type_str:
lines.append(f'{indent}d5p1:{esc_xml(type_str)}')
return
lines.append(f'{indent}{esc_xml(type_str)}')
# --- Field shorthand parser ---
def parse_field_shorthand(s):
result = {
'dataPath': '', 'field': '', 'title': '', 'type': '',
'roles': [], 'restrict': [], 'appearance': {},
}
# 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)
# 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
# --- 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 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"
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):
result['value'] = m.group(4).strip()
else:
result['name'] = s.strip()
return result
# --- Calculated field shorthand parser ---
def parse_calc_shorthand(s):
idx = s.find('=')
if idx > 0:
return {
'dataPath': s[:idx].strip(),
'expression': s[idx + 1:].strip(),
}
return {'dataPath': s.strip(), 'expression': ''}
# --- 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",
}
# ===== Emit sections =====
def emit_data_sources(lines, data_sources):
for ds in data_sources:
lines.append('\t')
lines.append(f'\t\t{esc_xml(ds["name"])}')
lines.append(f'\t\t{esc_xml(ds["type"])}')
lines.append('\t')
# === Fields ===
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': str(field_def.get('title', '')) if field_def.get('title') else '',
'type': resolve_type_str(str(field_def['type'])) if field_def.get('type') else '',
'roles': [],
'restrict': [],
'appearance': {},
}
# Parse role
if field_def.get('role'):
if isinstance(field_def['role'], str):
f['roles'] = [field_def['role']]
else:
# Object form -- collect truthy keys
for k, v in field_def['role'].items():
if v is True:
f['roles'].append(k)
# Parse restrictions
if field_def.get('restrict'):
f['restrict'] = list(field_def['restrict'])
# Parse appearance
if field_def.get('appearance'):
for k, v in field_def['appearance'].items():
f['appearance'][k] = str(v)
if field_def.get('presentationExpression'):
f['presentationExpression'] = str(field_def['presentationExpression'])
# attrRestrict
if field_def.get('attrRestrict'):
f['attrRestrict'] = list(field_def['attrRestrict'])
# role object extras
if field_def.get('role') and not isinstance(field_def['role'], str):
f['roleObj'] = field_def['role']
lines.append(f'{indent}')
lines.append(f'{indent}\t{esc_xml(f["dataPath"])}')
lines.append(f'{indent}\t{esc_xml(f["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')
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')
# AttributeUseRestriction
if f.get('attrRestrict') and len(f['attrRestrict']) > 0:
lines.append(f'{indent}\t')
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')
# Role
if (f.get('roles') and len(f['roles']) > 0) or f.get('roleObj'):
lines.append(f'{indent}\t')
for role in f.get('roles', []):
if role == 'period':
lines.append(f'{indent}\t\t1')
lines.append(f'{indent}\t\tMain')
else:
lines.append(f'{indent}\t\ttrue')
if f.get('roleObj'):
ro = f['roleObj']
if ro.get('accountTypeExpression'):
lines.append(f'{indent}\t\t{esc_xml(str(ro["accountTypeExpression"]))}')
if ro.get('balanceGroup'):
lines.append(f'{indent}\t\t{esc_xml(str(ro["balanceGroup"]))}')
lines.append(f'{indent}\t')
# ValueType
if f.get('type'):
lines.append(f'{indent}\t')
emit_value_type(lines, f['type'], f'{indent}\t\t')
lines.append(f'{indent}\t')
# Appearance
if f.get('appearance') and len(f['appearance']) > 0:
lines.append(f'{indent}\t')
for key, val in f['appearance'].items():
lines.append(f'{indent}\t\t')
lines.append(f'{indent}\t\t\t{esc_xml(key)}')
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':
lines.append(f'{indent}\t\t\t{esc_xml(val)}')
else:
lines.append(f'{indent}\t\t\t{esc_xml(val)}')
lines.append(f'{indent}\t\t')
lines.append(f'{indent}\t')
# PresentationExpression
if f.get('presentationExpression'):
lines.append(f'{indent}\t{esc_xml(f["presentationExpression"])}')
lines.append(f'{indent}')
# === DataSets ===
def emit_data_set(lines, ds, indent, default_source):
# Determine type
if ds.get('items'):
ds_type = 'DataSetUnion'
elif ds.get('objectName'):
ds_type = 'DataSetObject'
else:
ds_type = 'DataSetQuery'
lines.append(f'{indent}')
lines.append(f'{indent}\t{esc_xml(str(ds.get("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{esc_xml(src)}')
# Type-specific content
if ds_type == 'DataSetQuery':
query_text = resolve_query_value(str(ds.get("query", "")), query_base_dir)
lines.append(f'{indent}\t{esc_xml(query_text)}')
if ds.get('autoFillFields') is False:
lines.append(f'{indent}\tfalse')
elif ds_type == 'DataSetObject':
lines.append(f'{indent}\t{esc_xml(str(ds["objectName"]))}')
elif ds_type == 'DataSetUnion':
for item in ds['items']:
emit_data_set(lines, item, f'{indent}\t', default_source)
lines.append(f'{indent}')
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')
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{esc_xml(src_ds)}')
lines.append(f'\t\t{esc_xml(dst_ds)}')
lines.append(f'\t\t{esc_xml(src_ex)}')
lines.append(f'\t\t{esc_xml(dst_ex)}')
if link.get('parameter'):
lines.append(f'\t\t{esc_xml(str(link["parameter"]))}')
lines.append('\t')
# === CalculatedFields ===
def emit_calc_fields(lines, defn):
if not defn.get('calculatedFields'):
return
for cf in defn['calculatedFields']:
if isinstance(cf, str):
parsed = parse_calc_shorthand(cf)
is_obj = False
else:
parsed = {
'dataPath': str(cf.get('dataPath') or cf.get('field', '')),
'expression': str(cf.get('expression', '')),
}
is_obj = True
lines.append('\t')
lines.append(f'\t\t{esc_xml(parsed["dataPath"])}')
lines.append(f'\t\t{esc_xml(parsed["expression"])}')
if is_obj:
if cf.get('title'):
emit_mltext(lines, '\t\t', 'title', str(cf['title']))
if cf.get('type'):
cf_type = resolve_type_str(str(cf['type']))
lines.append('\t\t')
emit_value_type(lines, cf_type, '\t\t\t')
lines.append('\t\t')
restrict_val = cf.get('restrict') or cf.get('useRestriction')
if restrict_val:
lines.append('\t\t')
if isinstance(restrict_val, dict):
# Object form: { "field": true, "condition": true, ... }
for xml_name, flag in restrict_val.items():
if flag:
lines.append(f'\t\t\t<{esc_xml(str(xml_name))}>true{esc_xml(str(xml_name))}>')
else:
# Array form: ["noField", "noFilter", ...]
restrict_map = {
'noField': 'field', 'noFilter': 'condition', 'noCondition': 'condition',
'noGroup': 'group', 'noOrder': 'order',
}
for r in restrict_val:
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')
if cf.get('appearance'):
lines.append('\t\t')
for k, v in cf['appearance'].items():
lines.append('\t\t\t')
lines.append(f'\t\t\t\t{esc_xml(k)}')
lines.append(f'\t\t\t\t{esc_xml(str(v))}')
lines.append('\t\t\t')
lines.append('\t\t')
lines.append('\t')
# === 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')
lines.append(f'\t\t{esc_xml(parsed["dataPath"])}')
lines.append(f'\t\t{esc_xml(parsed["expression"])}')
if groups:
if isinstance(groups, list):
for g in groups:
lines.append(f'\t\t{esc_xml(str(g))}')
else:
lines.append(f'\t\t{esc_xml(str(groups))}')
lines.append('\t')
# === Parameters ===
def emit_param_value(lines, type_str, val, indent):
if val is None:
return
val_str = str(val)
if type_str == 'StandardPeriod':
# Always emit startDate/endDate to match how 1C Designer saves the schema.
lines.append(f'{indent}')
lines.append(f'{indent}\t{esc_xml(val_str)}')
lines.append(f'{indent}\t0001-01-01T00:00:00')
lines.append(f'{indent}\t0001-01-01T00:00:00')
lines.append(f'{indent}')
elif type_str and re.match(r'^date', type_str):
lines.append(f'{indent}{esc_xml(val_str)}')
elif type_str == 'boolean':
lines.append(f'{indent}{esc_xml(val_str)}')
elif type_str and re.match(r'^decimal', type_str):
lines.append(f'{indent}{esc_xml(val_str)}')
elif type_str and re.match(r'^string', type_str):
lines.append(f'{indent}{esc_xml(val_str)}')
else:
# Guess from value
if re.match(r'^\d{4}-\d{2}-\d{2}T', val_str):
lines.append(f'{indent}{esc_xml(val_str)}')
elif val_str == 'true' or val_str == 'false':
lines.append(f'{indent}{esc_xml(val_str)}')
elif re.match(r'^(ПланСчетов|Справочник|Перечисление|Документ|ПланВидовХарактеристик|ПланВидовРасчета|БизнесПроцесс|Задача|РегистрСведений|ПланОбмена|ChartOfAccounts|Catalog|Enum|Document|ChartOfCharacteristicTypes|ChartOfCalculationTypes|BusinessProcess|Task|InformationRegister|ExchangePlan)\.', val_str):
lines.append(f'{indent}{esc_xml(val_str)}')
else:
lines.append(f'{indent}{esc_xml(val_str)}')
def emit_single_param(lines, p, parsed):
lines.append('\t')
lines.append(f'\t\t{esc_xml(parsed["name"])}')
# Title (from parsed first, then from object form)
title = ''
if parsed.get('title'):
title = str(parsed['title'])
elif p is not None and not isinstance(p, str) and p.get('title'):
title = str(p['title'])
if title:
emit_mltext(lines, '\t\t', 'title', title)
# ValueType
if parsed.get('type'):
lines.append('\t\t')
emit_value_type(lines, parsed['type'], '\t\t\t')
lines.append('\t\t')
# Value
emit_param_value(lines, parsed.get('type', ''), parsed.get('value'), '\t\t')
# Hidden implies useRestriction=true + availableAsField=false
if parsed.get('hidden') is True:
parsed['availableAsField'] = False
parsed['useRestriction'] = True
# UseRestriction
if parsed.get('useRestriction') is True or (p is not None and not isinstance(p, str) and p.get('useRestriction') is True):
lines.append('\t\ttrue')
# Expression
if parsed.get('expression'):
lines.append(f'\t\t{esc_xml(parsed["expression"])}')
if parsed.get('hidden'):
parsed['availableAsField'] = False
# AvailableAsField
if parsed.get('availableAsField') is False:
lines.append('\t\tfalse')
# ValueListAllowed
if parsed.get('valueListAllowed'):
lines.append('\t\ttrue')
# AvailableValues
if p is not None and not isinstance(p, str) and p.get('availableValues'):
for av in p['availableValues']:
av_val = str(av.get('value', ''))
av_type = 'xs:string'
if re.match(r'^(Перечисление|Справочник|ПланСчетов|Документ|ПланВидовХарактеристик|ПланВидовРасчета)\.', av_val):
av_type = 'dcscor:DesignTimeValue'
lines.append('\t\t')
lines.append(f'\t\t\t{esc_xml(av_val)}')
if av.get('presentation'):
lines.append('\t\t\t')
lines.append('\t\t\t\t')
lines.append('\t\t\t\t\tru')
lines.append(f'\t\t\t\t\t{esc_xml(str(av["presentation"]))}')
lines.append('\t\t\t\t')
lines.append('\t\t\t')
lines.append('\t\t')
# DenyIncompleteValues
if p is not None and not isinstance(p, str) and p.get('denyIncompleteValues') is True:
lines.append('\t\ttrue')
# Use
if p is not None and not isinstance(p, str) and p.get('use'):
lines.append(f'\t\t')
lines.append('\t')
_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:
parsed = {
'name': str(p.get('name', '')),
'type': resolve_type_str(str(p['type'])) if p.get('type') else '',
'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
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)
if parsed.get('autoDates'):
param_name = parsed['name']
begin_parsed = {
'name': '\u0414\u0430\u0442\u0430\u041d\u0430\u0447\u0430\u043b\u0430',
'title': '\u041d\u0430\u0447\u0430\u043b\u043e \u043f\u0435\u0440\u0438\u043e\u0434\u0430',
'type': 'date', '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': '\u0414\u0430\u0442\u0430\u041e\u043a\u043e\u043d\u0447\u0430\u043d\u0438\u044f',
'title': '\u041a\u043e\u043d\u0435\u0446 \u043f\u0435\u0440\u0438\u043e\u0434\u0430',
'type': 'date', '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 = {
'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):
for d in [base_dir, os.getcwd()]:
p = os.path.join(d, 'skd-styles.json')
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):
if color.startswith('style:'):
style_name = color[6:]
lines.append(f'{indent}d8p1:{style_name}')
else:
lines.append(f'{indent}{esc_xml(color)}')
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'
lines.append('\t\t\t\t')
# Background color
if style.get('bgColor'):
lines.append(f'{ind}')
lines.append(f'{ind}\t\u0426\u0432\u0435\u0442\u0424\u043e\u043d\u0430')
_emit_color_value(lines, style['bgColor'], f'{ind}\t')
lines.append(f'{ind}')
# Text color
if style.get('textColor'):
lines.append(f'{ind}')
lines.append(f'{ind}\t\u0426\u0432\u0435\u0442\u0422\u0435\u043a\u0441\u0442\u0430')
_emit_color_value(lines, style['textColor'], f'{ind}\t')
lines.append(f'{ind}')
# Borders
if style.get('borders'):
if style.get('borderColor'):
lines.append(f'{ind}')
lines.append(f'{ind}\t\u0426\u0432\u0435\u0442\u0413\u0440\u0430\u043d\u0438\u0446\u044b')
_emit_color_value(lines, style['borderColor'], f'{ind}\t')
lines.append(f'{ind}')
lines.append(f'{ind}')
lines.append(f'{ind}\t\u0421\u0442\u0438\u043b\u044c\u0413\u0440\u0430\u043d\u0438\u0446\u044b')
lines.append(f'{ind}\t')
lines.append(f'{ind}\t\tNone')
lines.append(f'{ind}\t')
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')
lines.append(f'{ind}\t\t\u0421\u0442\u0438\u043b\u044c\u0413\u0440\u0430\u043d\u0438\u0446\u044b.{side}')
lines.append(f'{ind}\t\t')
lines.append(f'{ind}\t\t\tSolid')
lines.append(f'{ind}\t\t')
lines.append(f'{ind}\t')
lines.append(f'{ind}')
# Font
bold_str = 'true' if style.get('bold') else 'false'
italic_str = 'true' if style.get('italic') else 'false'
lines.append(f'{ind}')
lines.append(f'{ind}\t\u0428\u0440\u0438\u0444\u0442')
lines.append(f'{ind}\t')
lines.append(f'{ind}')
# Horizontal alignment
if style.get('hAlign'):
lines.append(f'{ind}')
lines.append(f'{ind}\t\u0413\u043e\u0440\u0438\u0437\u043e\u043d\u0442\u0430\u043b\u044c\u043d\u043e\u0435\u041f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0435')
lines.append(f'{ind}\t{esc_xml(style["hAlign"])}')
lines.append(f'{ind}')
# Vertical alignment
if style.get('vAlign'):
lines.append(f'{ind}')
lines.append(f'{ind}\t\u0412\u0435\u0440\u0442\u0438\u043a\u0430\u043b\u044c\u043d\u043e\u0435\u041f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0435')
lines.append(f'{ind}\t{esc_xml(style["vAlign"])}')
lines.append(f'{ind}')
# Wrap
if style.get('wrap'):
lines.append(f'{ind}')
lines.append(f'{ind}\t\u0420\u0430\u0437\u043c\u0435\u0449\u0435\u043d\u0438\u0435')
lines.append(f'{ind}\tWrap')
lines.append(f'{ind}')
# Width
if width and width > 0:
lines.append(f'{ind}')
lines.append(f'{ind}\t\u041c\u0438\u043d\u0438\u043c\u0430\u043b\u044c\u043d\u0430\u044f\u0428\u0438\u0440\u0438\u043d\u0430')
lines.append(f'{ind}\t{fmt_dec(width)}')
lines.append(f'{ind}')
lines.append(f'{ind}')
lines.append(f'{ind}\t\u041c\u0430\u043a\u0441\u0438\u043c\u0430\u043b\u044c\u043d\u0430\u044f\u0428\u0438\u0440\u0438\u043d\u0430')
lines.append(f'{ind}\t{fmt_dec(width)}')
lines.append(f'{ind}')
# Min height
if min_height and min_height > 0:
lines.append(f'{ind}')
lines.append(f'{ind}\t\u041c\u0438\u043d\u0438\u043c\u0430\u043b\u044c\u043d\u0430\u044f\u0412\u044b\u0441\u043e\u0442\u0430')
lines.append(f'{ind}\t{min_height}')
lines.append(f'{ind}')
# Vertical merge
if v_merge:
lines.append(f'{ind}')
lines.append(f'{ind}\t\u041e\u0431\u044a\u0435\u0434\u0438\u043d\u044f\u0442\u044c\u041f\u043e\u0412\u0435\u0440\u0442\u0438\u043a\u0430\u043b\u0438')
lines.append(f'{ind}\ttrue')
lines.append(f'{ind}')
# Horizontal merge
if h_merge:
lines.append(f'{ind}')
lines.append(f'{ind}\t\u041e\u0431\u044a\u0435\u0434\u0438\u043d\u044f\u0442\u044c\u041f\u043e\u0413\u043e\u0440\u0438\u0437\u043e\u043d\u0442\u0430\u043b\u0438')
lines.append(f'{ind}\ttrue')
lines.append(f'{ind}')
# Extra appearance items (e.g. drilldown)
if extra_items:
for ei in extra_items:
lines.append(ei)
lines.append('\t\t\t\t')
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 = rows[r][c] if c < len(rows[r]) else None
if isinstance(cell_val, str) and 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 = rows[r][c] if c < len(rows[r]) else None
if isinstance(cell_val, str) and cell_val == '>':
h_merge[r][c] = True
# Build drilldown map: param_name -> drilldown_value
drilldown_map = {}
if t.get('parameters'):
for tp in t['parameters']:
if tp.get('drilldown'):
drilldown_map[str(tp['name'])] = str(tp['drilldown'])
lines.append('\t')
lines.append(f'\t\t{esc_xml(str(t["name"]))}')
lines.append('\t\t')
for r in range(len(rows)):
lines.append('\t\t\t')
for c in range(col_count):
cell_val = rows[r][c] if c < len(rows[r]) else None
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')
if is_v_merged:
_emit_cell_appearance(lines, style, w, True)
elif is_h_merged:
_emit_cell_appearance(lines, style, w, h_merge=True)
else:
cell_extra_items = []
if 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')
lines.append(f'\t\t\t\t\t\t{esc_xml(param_name)}')
lines.append('\t\t\t\t\t')
# Build drilldown appearance extra items
if param_name in drilldown_map:
dd_val = drilldown_map[param_name]
cell_extra_items.append('\t\t\t\t\t')
cell_extra_items.append(f'\t\t\t\t\t\t\u0420\u0430\u0441\u0448\u0438\u0444\u0440\u043e\u0432\u043a\u0430')
cell_extra_items.append(f'\t\t\t\t\t\t\u0420\u0430\u0441\u0448\u0438\u0444\u0440\u043e\u0432\u043a\u0430_{dd_val}')
cell_extra_items.append('\t\t\t\t\t')
else:
lines.append('\t\t\t\t\t')
lines.append('\t\t\t\t\t\t')
lines.append('\t\t\t\t\t\t\t')
lines.append('\t\t\t\t\t\t\t\tru')
lines.append(f'\t\t\t\t\t\t\t\t{esc_xml(cell_str)}')
lines.append('\t\t\t\t\t\t\t')
lines.append('\t\t\t\t\t\t')
lines.append('\t\t\t\t\t')
h = min_height if r == 0 else 0
_emit_cell_appearance(lines, style, w, False, False, h, cell_extra_items or None)
lines.append('\t\t\t\t')
lines.append('\t\t\t')
lines.append('\t\t')
if t.get('parameters'):
for tp in t['parameters']:
lines.append('\t\t')
lines.append(f'\t\t\t{esc_xml(str(tp["name"]))}')
lines.append(f'\t\t\t{esc_xml(str(tp["expression"]))}')
lines.append('\t\t')
# Drilldown parameter
if tp.get('drilldown'):
dd_val = str(tp['drilldown'])
lines.append('\t\t')
lines.append(f'\t\t\t\u0420\u0430\u0441\u0448\u0438\u0444\u0440\u043e\u0432\u043a\u0430_{esc_xml(dd_val)}')
lines.append('\t\t\t')
lines.append('\t\t\t\t\u0418\u043c\u044f\u0420\u0435\u0441\u0443\u0440\u0441\u0430')
lines.append(f'\t\t\t\t"{esc_xml(dd_val)}"')
lines.append('\t\t\t')
lines.append('\t\t\tDrillDown')
lines.append('\t\t')
lines.append('\t')
# === 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')
lines.append(f'\t\t{esc_xml(str(t["name"]))}')
if t.get('template'):
lines.append(f'\t\t{t["template"]}')
if t.get('parameters'):
for tp in t['parameters']:
lines.append('\t\t')
lines.append(f'\t\t\t{esc_xml(str(tp["name"]))}')
lines.append(f'\t\t\t{esc_xml(str(tp["expression"]))}')
lines.append('\t\t')
# Drilldown parameter
if tp.get('drilldown'):
dd_val = str(tp['drilldown'])
lines.append('\t\t')
lines.append(f'\t\t\t\u0420\u0430\u0441\u0448\u0438\u0444\u0440\u043e\u0432\u043a\u0430_{esc_xml(dd_val)}')
lines.append('\t\t\t')
lines.append('\t\t\t\t\u0418\u043c\u044f\u0420\u0435\u0441\u0443\u0440\u0441\u0430')
lines.append(f'\t\t\t\t"{esc_xml(dd_val)}"')
lines.append('\t\t\t')
lines.append('\t\t\tDrillDown')
lines.append('\t\t')
lines.append('\t')
# === 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{esc_xml(str(gt["groupName"]))}')
elif gt.get('groupField'):
lines.append(f'\t\t{esc_xml(str(gt["groupField"]))}')
lines.append(f'\t\t{esc_xml(xml_ttype)}')
lines.append(f'\t\t{esc_xml(str(gt["template"]))}')
lines.append(f'\t{tag}>')
# === Settings Variants ===
def emit_selection(lines, items, indent, skip_auto=False):
if not items or len(items) == 0:
return
lines.append(f'{indent}')
for item in items:
if isinstance(item, str):
if item == 'Auto':
if not skip_auto:
lines.append(f'{indent}\t')
else:
lines.append(f'{indent}\t')
lines.append(f'{indent}\t\t{esc_xml(item)}')
lines.append(f'{indent}\t')
elif item.get('folder'):
lines.append(f'{indent}\t')
lines.append(f'{indent}\t\t')
lines.append(f'{indent}\t\t\t')
lines.append(f'{indent}\t\t\t\tru')
lines.append(f'{indent}\t\t\t\t{esc_xml(str(item["folder"]))}')
lines.append(f'{indent}\t\t\t')
lines.append(f'{indent}\t\t')
for sub in (item.get('items') or []):
sub_name = str(sub.get('field', sub)) if isinstance(sub, dict) else str(sub)
lines.append(f'{indent}\t\t')
lines.append(f'{indent}\t\t\t{esc_xml(sub_name)}')
lines.append(f'{indent}\t\t')
lines.append(f'{indent}\t\tAuto')
lines.append(f'{indent}\t')
else:
lines.append(f'{indent}\t')
lines.append(f'{indent}\t\t{esc_xml(str(item["field"]))}')
if item.get('title'):
lines.append(f'{indent}\t\t')
lines.append(f'{indent}\t\t\t')
lines.append(f'{indent}\t\t\t\tru')
lines.append(f'{indent}\t\t\t\t{esc_xml(str(item["title"]))}')
lines.append(f'{indent}\t\t\t')
lines.append(f'{indent}\t\t')
lines.append(f'{indent}\t')
lines.append(f'{indent}')
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}')
lines.append(f'{indent}\t{group_type}')
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')
lines.append(f'{indent}')
return
# FilterItemComparison
lines.append(f'{indent}')
if item.get('use') is False:
lines.append(f'{indent}\tfalse')
lines.append(f'{indent}\t{esc_xml(str(item["field"]))}')
comp_type = COMPARISON_TYPES.get(str(item.get('op', '')), str(item.get('op', '')))
lines.append(f'{indent}\t{esc_xml(comp_type)}')
# Right value
if item.get('value') is not None:
vt = str(item.get('valueType', '')) if item.get('valueType') else ''
if not vt:
v = item['value']
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'
else:
vt = 'xs:string'
if isinstance(item['value'], bool):
v_str = str(item['value']).lower()
else:
v_str = esc_xml(str(item['value']))
lines.append(f'{indent}\t{v_str}')
if item.get('presentation'):
lines.append(f'{indent}\t')
lines.append(f'{indent}\t\t')
lines.append(f'{indent}\t\t\tru')
lines.append(f'{indent}\t\t\t{esc_xml(str(item["presentation"]))}')
lines.append(f'{indent}\t\t')
lines.append(f'{indent}\t')
if item.get('viewMode'):
lines.append(f'{indent}\t{esc_xml(str(item["viewMode"]))}')
if item.get('userSettingID'):
uid = new_uuid() if str(item['userSettingID']) == 'auto' else str(item['userSettingID'])
lines.append(f'{indent}\t{esc_xml(uid)}')
if item.get('userSettingPresentation'):
lines.append(f'{indent}\t')
lines.append(f'{indent}\t\t')
lines.append(f'{indent}\t\t\tru')
lines.append(f'{indent}\t\t\t{esc_xml(str(item["userSettingPresentation"]))}')
lines.append(f'{indent}\t\t')
lines.append(f'{indent}\t')
lines.append(f'{indent}')
def emit_filter(lines, items, indent):
if not items or len(items) == 0:
return
lines.append(f'{indent}')
for item in items:
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')
lines.append(f'{indent}')
def emit_order(lines, items, indent, skip_auto=False):
if not items or len(items) == 0:
return
lines.append(f'{indent}')
for item in items:
if isinstance(item, str):
if item == 'Auto':
if not skip_auto:
lines.append(f'{indent}\t')
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')
lines.append(f'{indent}\t\t{esc_xml(field)}')
lines.append(f'{indent}\t\t{direction}')
lines.append(f'{indent}\t')
lines.append(f'{indent}')
def emit_appearance_value(lines, key, val, indent):
lines.append(f'{indent}')
if isinstance(val, dict) and val.get('use') is False:
lines.append(f'{indent}\tfalse')
lines.append(f'{indent}\t{esc_xml(key)}')
actual_val = str(val.get('value', ''))
else:
lines.append(f'{indent}\t{esc_xml(key)}')
actual_val = str(val)
# Auto-detect value type
if re.match(r'^(style|web|win):', actual_val):
lines.append(f'{indent}\t{esc_xml(actual_val)}')
elif actual_val == 'true' or actual_val == 'false':
lines.append(f'{indent}\t{actual_val}')
elif key in ('\u0422\u0435\u043a\u0441\u0442', '\u0417\u0430\u0433\u043e\u043b\u043e\u0432\u043e\u043a', '\u0424\u043e\u0440\u043c\u0430\u0442'):
lines.append(f'{indent}\t')
lines.append(f'{indent}\t\t')
lines.append(f'{indent}\t\t\tru')
lines.append(f'{indent}\t\t\t{esc_xml(actual_val)}')
lines.append(f'{indent}\t\t')
lines.append(f'{indent}\t')
else:
lines.append(f'{indent}\t{esc_xml(actual_val)}')
lines.append(f'{indent}')
def emit_conditional_appearance(lines, items, indent):
if not items or len(items) == 0:
return
lines.append(f'{indent}')
for ca in items:
lines.append(f'{indent}\t')
# Selection
if ca.get('selection') and len(ca['selection']) > 0:
lines.append(f'{indent}\t\t')
for sel in ca['selection']:
lines.append(f'{indent}\t\t\t')
lines.append(f'{indent}\t\t\t\t{esc_xml(str(sel))}')
lines.append(f'{indent}\t\t\t')
lines.append(f'{indent}\t\t')
else:
lines.append(f'{indent}\t\t')
# Filter
if ca.get('filter'):
emit_filter(lines, ca['filter'], f'{indent}\t\t')
# Appearance
if ca.get('appearance'):
lines.append(f'{indent}\t\t')
for k, v in ca['appearance'].items():
emit_appearance_value(lines, k, v, f'{indent}\t\t\t')
lines.append(f'{indent}\t\t')
# Presentation
if ca.get('presentation'):
lines.append(f'{indent}\t\t{esc_xml(str(ca["presentation"]))}')
# ViewMode
if ca.get('viewMode'):
lines.append(f'{indent}\t\t{esc_xml(str(ca["viewMode"]))}')
# UserSettingID
if ca.get('userSettingID'):
uid = new_uuid() if str(ca['userSettingID']) == 'auto' else str(ca['userSettingID'])
lines.append(f'{indent}\t\t{esc_xml(uid)}')
lines.append(f'{indent}\t')
lines.append(f'{indent}')
def emit_output_parameters(lines, params, indent):
if not params:
return
lines.append(f'{indent}')
for key, val in params.items():
val_str = str(val)
ptype = OUTPUT_PARAM_TYPES.get(key, 'xs:string')
lines.append(f'{indent}\t')
lines.append(f'{indent}\t\t{esc_xml(key)}')
if ptype == 'mltext':
lines.append(f'{indent}\t\t')
lines.append(f'{indent}\t\t\t')
lines.append(f'{indent}\t\t\t\tru')
lines.append(f'{indent}\t\t\t\t{esc_xml(val_str)}')
lines.append(f'{indent}\t\t\t')
lines.append(f'{indent}\t\t')
else:
lines.append(f'{indent}\t\t{esc_xml(val_str)}')
lines.append(f'{indent}\t')
lines.append(f'{indent}')
def emit_data_parameters(lines, items, indent):
if not items or len(items) == 0:
return
lines.append(f'{indent}')
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')
if dp.get('use') is False:
lines.append(f'{indent}\t\tfalse')
lines.append(f'{indent}\t\t{esc_xml(str(dp["parameter"]))}')
# Value
if dp.get('value') is not None:
val = dp['value']
if isinstance(val, dict) and val.get('variant'):
# StandardPeriod
lines.append(f'{indent}\t\t')
lines.append(f'{indent}\t\t\t{esc_xml(str(val["variant"]))}')
lines.append(f'{indent}\t\t\t0001-01-01T00:00:00')
lines.append(f'{indent}\t\t\t0001-01-01T00:00:00')
lines.append(f'{indent}\t\t')
elif isinstance(val, bool):
bv = str(val).lower()
lines.append(f'{indent}\t\t{esc_xml(bv)}')
elif re.match(r'^\d{4}-\d{2}-\d{2}T', str(val)):
lines.append(f'{indent}\t\t{esc_xml(str(val))}')
else:
lines.append(f'{indent}\t\t{esc_xml(str(val))}')
if dp.get('viewMode'):
lines.append(f'{indent}\t\t{esc_xml(str(dp["viewMode"]))}')
if dp.get('userSettingID'):
uid = new_uuid() if str(dp['userSettingID']) == 'auto' else str(dp['userSettingID'])
lines.append(f'{indent}\t\t{esc_xml(uid)}')
if dp.get('userSettingPresentation'):
lines.append(f'{indent}\t\t')
lines.append(f'{indent}\t\t\t')
lines.append(f'{indent}\t\t\t\tru')
lines.append(f'{indent}\t\t\t\t{esc_xml(str(dp["userSettingPresentation"]))}')
lines.append(f'{indent}\t\t\t')
lines.append(f'{indent}\t\t')
lines.append(f'{indent}\t')
lines.append(f'{indent}')
# === 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}')
for field in group_by:
if isinstance(field, str):
lines.append(f'{indent}\t')
lines.append(f'{indent}\t\t{esc_xml(field)}')
lines.append(f'{indent}\t\tItems')
lines.append(f'{indent}\t\tNone')
lines.append(f'{indent}\t\t0001-01-01T00:00:00')
lines.append(f'{indent}\t\t0001-01-01T00:00:00')
lines.append(f'{indent}\t')
else:
lines.append(f'{indent}\t')
lines.append(f'{indent}\t\t{esc_xml(str(field["field"]))}')
gt = str(field.get('groupType', 'Items'))
lines.append(f'{indent}\t\t{esc_xml(gt)}')
pat = str(field.get('periodAdditionType', 'None'))
lines.append(f'{indent}\t\t{esc_xml(pat)}')
lines.append(f'{indent}\t\t0001-01-01T00:00:00')
lines.append(f'{indent}\t\t0001-01-01T00:00:00')
lines.append(f'{indent}\t')
lines.append(f'{indent}')
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]
if innermost is not None:
group['children'] = [innermost]
innermost = group
if innermost:
return [innermost]
return []
def emit_structure_item(lines, item, indent):
item_type = str(item.get('type', 'group'))
if item_type == 'group':
lines.append(f'{indent}')
if item.get('name'):
lines.append(f'{indent}\t{esc_xml(str(item["name"]))}')
emit_group_items(lines, item.get('groupBy') or item.get('groupFields'), f'{indent}\t')
# Default order to ["Auto"] if not specified
order_items = item.get('order') or ['Auto']
emit_order(lines, order_items, f'{indent}\t')
# Default selection to ["Auto"] if not specified
sel_items = item.get('selection') or ['Auto']
emit_selection(lines, sel_items, f'{indent}\t')
emit_filter(lines, item.get('filter'), f'{indent}\t')
if item.get('outputParameters'):
emit_output_parameters(lines, item['outputParameters'], f'{indent}\t')
# Nested children
if item.get('children'):
for child in item['children']:
emit_structure_item(lines, child, f'{indent}\t')
lines.append(f'{indent}')
elif item_type == 'table':
lines.append(f'{indent}')
if item.get('name'):
lines.append(f'{indent}\t{esc_xml(str(item["name"]))}')
# Columns
if item.get('columns'):
for col in item['columns']:
lines.append(f'{indent}\t')
emit_group_items(lines, col.get('groupBy') or col.get('groupFields'), f'{indent}\t\t')
col_order = col.get('order') or ['Auto']
emit_order(lines, col_order, f'{indent}\t\t')
col_sel = col.get('selection') or ['Auto']
emit_selection(lines, col_sel, f'{indent}\t\t')
lines.append(f'{indent}\t')
# Rows
if item.get('rows'):
for row in item['rows']:
lines.append(f'{indent}\t')
if row.get('name'):
lines.append(f'{indent}\t\t{esc_xml(str(row["name"]))}')
emit_group_items(lines, row.get('groupBy') or row.get('groupFields'), f'{indent}\t\t')
row_order = row.get('order') or ['Auto']
emit_order(lines, row_order, f'{indent}\t\t')
row_sel = row.get('selection') or ['Auto']
emit_selection(lines, row_sel, f'{indent}\t\t')
lines.append(f'{indent}\t')
lines.append(f'{indent}')
elif item_type == 'chart':
lines.append(f'{indent}')
if item.get('name'):
lines.append(f'{indent}\t{esc_xml(str(item["name"]))}')
# Points
if item.get('points'):
lines.append(f'{indent}\t')
emit_group_items(lines, item['points'].get('groupBy') or item['points'].get('groupFields'), f'{indent}\t\t')
pt_order = item['points'].get('order') or ['Auto']
emit_order(lines, pt_order, f'{indent}\t\t')
pt_sel = item['points'].get('selection') or ['Auto']
emit_selection(lines, pt_sel, f'{indent}\t\t')
lines.append(f'{indent}\t')
# Series
if item.get('series'):
lines.append(f'{indent}\t')
emit_group_items(lines, item['series'].get('groupBy') or item['series'].get('groupFields'), f'{indent}\t\t')
sr_order = item['series'].get('order') or ['Auto']
emit_order(lines, sr_order, f'{indent}\t\t')
sr_sel = item['series'].get('selection') or ['Auto']
emit_selection(lines, sr_sel, f'{indent}\t\t')
lines.append(f'{indent}\t')
# 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')
lines.append(f'{indent}')
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')
lines.append(f'\t\t{esc_xml(str(v["name"]))}')
pres = str(v.get('presentation', '')) or str(v.get('title', '')) or str(v['name'])
lines.append('\t\t')
lines.append('\t\t\t')
lines.append('\t\t\t\tru')
lines.append(f'\t\t\t\t{esc_xml(pres)}')
lines.append('\t\t\t')
lines.append('\t\t')
lines.append('\t\t')
s = v.get('settings', {})
# Selection
if s.get('selection'):
emit_selection(lines, s['selection'], '\t\t\t', skip_auto=True)
# Filter
if s.get('filter'):
emit_filter(lines, s['filter'], '\t\t\t')
# Order
if s.get('order'):
emit_order(lines, s['order'], '\t\t\t', skip_auto=True)
# ConditionalAppearance
if s.get('conditionalAppearance'):
emit_conditional_appearance(lines, s['conditionalAppearance'], '\t\t\t')
# OutputParameters
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
auto_dp = []
for ap in _all_params:
if not ap['hidden']:
auto_dp.append({
'parameter': ap['name'],
'use': False,
'userSettingID': 'auto',
})
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')
lines.append('\t\t')
lines.append('\t')
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
load_user_styles(query_base_dir)
# --- 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('')
lines.append('')
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_group_templates(lines, defn)
emit_settings_variants(lines, defn)
lines.append('')
# --- 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()