#!/usr/bin/env python3 # skd-compile v1.0 — 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 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}") 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() if re.match(r'^\w+\(', func_part): return {'dataPath': data_path, 'expression': func_part} else: return {'dataPath': data_path, 'expression': f'{func_part}({data_path})'} # --- Parameter shorthand parser --- def parse_param_shorthand(s): result = {'name': '', 'type': '', 'value': None, 'autoDates': False} # Extract @autoDates flag if '@autoDates' in s: result['autoDates'] = True s = re.sub(r'\s*@autoDates', '', s) # 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' 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', '')), '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') 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') 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': lines.append(f'{indent}\t{esc_xml(str(ds.get("query", "")))}') 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') lines.append(f'\t\t{esc_xml(str(link["source"]))}') lines.append(f'\t\t{esc_xml(str(link["dest"]))}') lines.append(f'\t\t{esc_xml(str(link["sourceExpr"]))}') lines.append(f'\t\t{esc_xml(str(link["destExpr"]))}') 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', '')), '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') if cf.get('restrict'): restrict_map = { 'noField': 'field', 'noFilter': 'condition', 'noCondition': 'condition', 'noGroup': 'group', 'noOrder': 'order', } lines.append('\t\t') for r in cf['restrict']: xml_name = restrict_map.get(str(r)) if xml_name: lines.append(f'\t\t\t<{xml_name}>true') 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': lines.append(f'{indent}') lines.append(f'{indent}\t{esc_xml(val_str)}') 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)}') 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 title = '' if 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') # UseRestriction if 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"])}') # AvailableAsField if parsed.get('availableAsField') is False: lines.append('\t\tfalse') # Use if p is not None and not isinstance(p, str) and p.get('use'): lines.append(f'\t\t{esc_xml(str(p["use"]))}') lines.append('\t') def emit_parameters(lines, defn): 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('autoDates') is True: parsed['autoDates'] = True emit_single_param(lines, p, parsed) # @autoDates: auto-generate ДатаНачала and ДатаОкончания if parsed.get('autoDates'): param_name = parsed['name'] begin_parsed = { 'name': '\u0414\u0430\u0442\u0430\u041d\u0430\u0447\u0430\u043b\u0430', 'type': 'date', 'value': None, 'expression': f'&{param_name}.\u0414\u0430\u0442\u0430\u041d\u0430\u0447\u0430\u043b\u0430', 'availableAsField': False, } emit_single_param(lines, None, begin_parsed) end_parsed = { 'name': '\u0414\u0430\u0442\u0430\u041e\u043a\u043e\u043d\u0447\u0430\u043d\u0438\u044f', 'type': 'date', 'value': None, 'expression': f'&{param_name}.\u0414\u0430\u0442\u0430\u041e\u043a\u043e\u043d\u0447\u0430\u043d\u0438\u044f', 'availableAsField': False, } emit_single_param(lines, None, end_parsed) # === Templates === def emit_templates(lines, defn): if not defn.get('templates'): return for t in defn['templates']: lines.append('\t') # === GroupTemplates === def emit_group_templates(lines, defn): if not defn.get('groupTemplates'): return for gt in defn['groupTemplates']: lines.append('\t') lines.append(f'\t\t{esc_xml(str(gt["groupField"]))}') lines.append(f'\t\t{esc_xml(str(gt["templateType"]))}') lines.append(f'\t\t') lines.append('\t') # === 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') 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']: 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 == '\u0422\u0435\u043a\u0441\u0442' or key == '\u0417\u0430\u0433\u043e\u043b\u043e\u0432\u043e\u043a': 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') 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: 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', '')) 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'), 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'), 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'), 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'), 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'), 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['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'): 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) 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) # --- 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()