#!/usr/bin/env python3 # form-compile v1.174 — Compile 1C managed form from JSON or object metadata # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills import argparse import copy import json import os import re import sys import uuid import xml.etree.ElementTree as ET from collections import OrderedDict from lxml import etree # ============================================================ # Support guard (Ext/ParentConfigurations.bin) — see docs/1c-support-state-spec.md # Blocks edits of vendor objects "на замке" / read-only configs. Trigger = bin # present; reaction from .v8-project.json editingAllowedCheck (deny|warn|off, # default deny). Never throws (except sys.exit on deny) — errors degrade to allow. # ============================================================ def _sg_root_uuid(xml_path): if not os.path.isfile(xml_path): return None try: mx = etree.parse(xml_path).getroot() for child in mx: if isinstance(child.tag, str) and child.get("uuid"): return child.get("uuid") except Exception: return None return None def _sg_find_v8project(start_dir): d = start_dir for _ in range(20): if not d: break pj = os.path.join(d, ".v8-project.json") if os.path.isfile(pj): return pj parent = os.path.dirname(d) if parent == d: break d = parent return None def _sg_get_edit_mode(cfg_dir): try: pj = _sg_find_v8project(os.getcwd()) or _sg_find_v8project(cfg_dir) if not pj: return "deny" proj = json.loads(open(pj, encoding="utf-8-sig").read()) cfg_full = os.path.normcase(os.path.abspath(cfg_dir)).rstrip("\\/") for db in proj.get("databases", []): src = db.get("configSrc") if src: src_full = os.path.normcase(os.path.abspath(src)).rstrip("\\/") if cfg_full == src_full or cfg_full.startswith(src_full + os.sep): if db.get("editingAllowedCheck"): return db["editingAllowedCheck"] if proj.get("editingAllowedCheck"): return proj["editingAllowedCheck"] return "deny" except Exception: return "deny" def assert_edit_allowed(target_path, require): try: rp = os.path.abspath(target_path) elem_uuid = _sg_root_uuid(rp) cfg_dir = None bin_path = None d = rp if os.path.isdir(rp) else os.path.dirname(rp) for _ in range(12): if not d: break if not elem_uuid: elem_uuid = _sg_root_uuid(d + ".xml") if not cfg_dir: cand = os.path.join(d, "Ext", "ParentConfigurations.bin") if os.path.exists(cand) or os.path.exists(os.path.join(d, "Configuration.xml")): cfg_dir = d bin_path = cand if elem_uuid and cfg_dir: break parent = os.path.dirname(d) if parent == d: break d = parent if not elem_uuid and cfg_dir: elem_uuid = _sg_root_uuid(os.path.join(cfg_dir, "Configuration.xml")) if not bin_path or not os.path.exists(bin_path): return data = open(bin_path, "rb").read() if len(data) <= 32: return if data[:3] == b"\xef\xbb\xbf": data = data[3:] text = data.decode("utf-8", "replace") h = re.match(r"\{6,(\d+),(\d+),", text) if not h: return g = int(h.group(1)) k = int(h.group(2)) if k == 0: return best = None if elem_uuid: for m in re.finditer(r"([0-2]),0," + re.escape(elem_uuid.lower()), text): f1 = int(m.group(1)) if best is None or f1 < best: best = f1 blocked = False code = "" reason = "" if g == 1: blocked = True code = "capability-off" reason = "возможность изменения конфигурации выключена (вся конфигурация read-only)" elif require == "removed": if best is not None and best != 2: blocked = True code = "not-removed" reason = "объект не снят с поддержки — удаление сломает обновления" else: if best is not None and best == 0: blocked = True code = "locked" reason = "объект на замке — редактирование сломает обновления" if not blocked: return mode = _sg_get_edit_mode(cfg_dir) if mode == "off": return if mode == "warn": sys.stderr.write(f"[support-guard] ПРЕДУПРЕЖДЕНИЕ: {reason}. Цель: {rp}\n") return head = "[support-guard] Редактирование отклонено: это объект типовой конфигурации на поддержке поставщика, прямое редактирование молча сломает будущие обновления." cfe = "Рекомендуемый путь: внести доработку в расширение (навыки cfe-borrow / cfe-patch-method) — состояние поддержки менять не нужно, обновления вендора сохраняются." off_note = "Снять проверку для этой базы: editingAllowedCheck = warn|off в .v8-project.json." if code == "capability-off": state = f"Состояние: у всей конфигурации выключена возможность изменения (режим read-only «из коробки») — поэтому объект «{rp}» редактировать нельзя." fix = ( "Либо снять защиту явно (навык support-edit, два шага):\n" f' 1. support-edit -Path "{cfg_dir}" -Capability on — включить возможность изменения (объекты пока остаются на замке);\n' f' 2. support-edit -Path "{rp}" -Set editable — открыть этот объект для редактирования.\n' " Изменение применяется в базу полной загрузкой выгрузки и обходит механизм обновлений вендора." ) elif code == "not-removed": state = f"Состояние: объект «{rp}» на поддержке (не снят с поддержки) — его удаление разорвёт обновления вендора." fix = ( "Либо сначала снять объект с поддержки, затем удалять:\n" f' support-edit -Path "{rp}" -Set off-support — объект уходит из-под обновлений, после этого удаление безопасно.' ) else: state = f"Состояние: объект «{rp}» на замке (возможность изменения конфигурации включена, но сам объект не редактируется)." fix = ( "Либо разрешить редактирование этого объекта (навык support-edit, выбрать одно):\n" f' support-edit -Path "{rp}" -Set editable — редактировать и дальше получать обновления вендора (возможны конфликты слияния);\n' f' support-edit -Path "{rp}" -Set off-support — снять с поддержки: обновления по объекту больше не приходят.' ) sys.stderr.write(head + "\n" + state + "\n" + cfe + "\n" + fix + "\n" + off_note + "\n") sys.exit(1) except SystemExit: raise except Exception: return # ═══════════════════════════════════════════════════════════════════════════ # FROM-OBJECT MODE: functions for metadata parsing, presets, DSL generation # ═══════════════════════════════════════════════════════════════════════════ NS = { 'md': 'http://v8.1c.ru/8.3/MDClasses', 'xr': 'http://v8.1c.ru/8.3/xcf/readable', 'v8': 'http://v8.1c.ru/8.1/data/core', } def _et_find(node, path): """Find with namespace map.""" return node.find(path, NS) def _et_findall(node, path): """Findall with namespace map.""" return node.findall(path, NS) def _et_text(node, path, default=''): """Get text of a sub-element, or default.""" el = node.find(path, NS) return el.text if el is not None and el.text else default def parse_object_meta(object_path): """Parse 1C metadata XML and return dict with Type, Name, Synonym, Attributes, TabularSections, etc.""" tree = ET.parse(object_path) root = tree.getroot() # Detect object type from root child meta_root = _et_find(root, '.') # Root is MetaDataObject; first child is the type node type_node = None for child in root: type_node = child break if type_node is None: print("Not a 1C metadata XML: " + object_path, file=sys.stderr) sys.exit(1) # Extract local name (strip namespace) obj_type = type_node.tag.split('}')[-1] if '}' in type_node.tag else type_node.tag props_node = _et_find(type_node, 'md:Properties') child_objs = _et_find(type_node, 'md:ChildObjects') # Name obj_name = _et_text(props_node, 'md:Name') # Synonym (Russian) synonym = obj_name syn_node = _et_find(props_node, "md:Synonym/v8:item[v8:lang='ru']/v8:content") if syn_node is not None and syn_node.text: synonym = syn_node.text def extract_type(type_parent): """Extract type string from md:Type element.""" if type_parent is None: return 'string' types = [] for t in _et_findall(type_parent, 'v8:Type'): if t.text: types.append(t.text) if not types: return 'string' return ' | '.join(types) def is_ref_type(t): return bool(re.search(r'Ref\.', t) or re.search(r'\u0441\u0441\u044b\u043b\u043a\u0430\.', t)) def extract_fields(parent_node, tag_name='Attribute'): """Extract field list from ChildObjects by tag name (Attribute, Dimension, Resource, AccountingFlag, ExtDimensionAccountingFlag).""" result = [] if parent_node is None: return result for field_node in _et_findall(parent_node, f'md:{tag_name}'): fp = _et_find(field_node, 'md:Properties') f_name = _et_text(fp, 'md:Name') f_syn_node = _et_find(fp, "md:Synonym/v8:item[v8:lang='ru']/v8:content") f_syn = f_syn_node.text if f_syn_node is not None and f_syn_node.text else f_name f_type_node = _et_find(fp, 'md:Type') f_type = extract_type(f_type_node) result.append({ 'Name': f_name, 'Synonym': f_syn, 'Type': f_type, 'IsRef': is_ref_type(f_type), }) return result # Attributes attributes = extract_fields(child_objs, 'Attribute') # Tabular sections tabular_sections = [] if child_objs is not None: for ts_node in _et_findall(child_objs, 'md:TabularSection'): tsp = _et_find(ts_node, 'md:Properties') ts_name = _et_text(tsp, 'md:Name') ts_syn_node = _et_find(tsp, "md:Synonym/v8:item[v8:lang='ru']/v8:content") ts_syn = ts_syn_node.text if ts_syn_node is not None and ts_syn_node.text else ts_name ts_co = _et_find(ts_node, 'md:ChildObjects') ts_cols = extract_fields(ts_co, 'Attribute') tabular_sections.append({ 'Name': ts_name, 'Synonym': ts_syn, 'Columns': ts_cols, }) meta = { 'Type': obj_type, 'Name': obj_name, 'Synonym': synonym, 'Attributes': attributes, 'TabularSections': tabular_sections, } # Type-specific properties if obj_type == 'Document': nt_node = _et_find(props_node, 'md:NumberType') meta['NumberType'] = nt_node.text if nt_node is not None and nt_node.text else 'String' elif obj_type == 'Catalog': cl_node = _et_find(props_node, 'md:CodeLength') meta['CodeLength'] = int(cl_node.text) if cl_node is not None and cl_node.text else 0 dl_node = _et_find(props_node, 'md:DescriptionLength') meta['DescriptionLength'] = int(dl_node.text) if dl_node is not None and dl_node.text else 0 hi_node = _et_find(props_node, 'md:Hierarchical') meta['Hierarchical'] = (hi_node is not None and hi_node.text == 'true') ht_node = _et_find(props_node, 'md:HierarchyType') meta['HierarchyType'] = ht_node.text if ht_node is not None and ht_node.text else 'HierarchyFoldersAndItems' owners = [] for ow in _et_findall(props_node, 'md:Owners/xr:Item'): if ow.text: owners.append(ow.text) meta['Owners'] = owners elif obj_type == 'InformationRegister': meta['Dimensions'] = extract_fields(child_objs, 'Dimension') meta['Resources'] = extract_fields(child_objs, 'Resource') prd_node = _et_find(props_node, 'md:InformationRegisterPeriodicity') meta['Periodicity'] = prd_node.text if prd_node is not None and prd_node.text else 'Nonperiodical' wm_node = _et_find(props_node, 'md:WriteMode') meta['WriteMode'] = wm_node.text if wm_node is not None and wm_node.text else 'Independent' elif obj_type == 'AccumulationRegister': meta['Dimensions'] = extract_fields(child_objs, 'Dimension') meta['Resources'] = extract_fields(child_objs, 'Resource') rt_node = _et_find(props_node, 'md:RegisterType') meta['RegisterType'] = rt_node.text if rt_node is not None and rt_node.text else 'Balances' elif obj_type == 'ChartOfCharacteristicTypes': cl_node = _et_find(props_node, 'md:CodeLength') meta['CodeLength'] = int(cl_node.text) if cl_node is not None and cl_node.text else 0 dl_node = _et_find(props_node, 'md:DescriptionLength') meta['DescriptionLength'] = int(dl_node.text) if dl_node is not None and dl_node.text else 0 hi_node = _et_find(props_node, 'md:Hierarchical') meta['Hierarchical'] = (hi_node is not None and hi_node.text == 'true') ht_node = _et_find(props_node, 'md:HierarchyType') meta['HierarchyType'] = ht_node.text if ht_node is not None and ht_node.text else 'HierarchyFoldersAndItems' owners = [] for ow in _et_findall(props_node, 'md:Owners/xr:Item'): if ow.text: owners.append(ow.text) meta['Owners'] = owners meta['HasValueType'] = True elif obj_type == 'ExchangePlan': cl_node = _et_find(props_node, 'md:CodeLength') meta['CodeLength'] = int(cl_node.text) if cl_node is not None and cl_node.text else 0 dl_node = _et_find(props_node, 'md:DescriptionLength') meta['DescriptionLength'] = int(dl_node.text) if dl_node is not None and dl_node.text else 0 meta['Hierarchical'] = False meta['HierarchyType'] = None meta['Owners'] = [] elif obj_type == 'ChartOfAccounts': cl_node = _et_find(props_node, 'md:CodeLength') meta['CodeLength'] = int(cl_node.text) if cl_node is not None and cl_node.text else 0 dl_node = _et_find(props_node, 'md:DescriptionLength') meta['DescriptionLength'] = int(dl_node.text) if dl_node is not None and dl_node.text else 0 meta['Hierarchical'] = True ht_node = _et_find(props_node, 'md:HierarchyType') meta['HierarchyType'] = ht_node.text if ht_node is not None and ht_node.text else 'HierarchyFoldersAndItems' meta['Owners'] = [] max_ed_node = _et_find(props_node, 'md:MaxExtDimensionCount') meta['MaxExtDimensionCount'] = int(max_ed_node.text) if max_ed_node is not None and max_ed_node.text else 0 meta['AccountingFlags'] = extract_fields(child_objs, 'AccountingFlag') meta['ExtDimensionAccountingFlags'] = extract_fields(child_objs, 'ExtDimensionAccountingFlag') return meta def _deep_merge(base, overlay): """Deep merge two dicts. overlay wins on conflicts.""" if not overlay: return base if not base: return overlay result = {} for k in base: result[k] = base[k] for k in overlay: if k in result and isinstance(result[k], dict) and isinstance(overlay[k], dict): result[k] = _deep_merge(result[k], overlay[k]) else: result[k] = overlay[k] return result def load_preset(preset_name, script_dir, out_path_resolved): """Load preset: hardcoded defaults -> built-in JSON -> project-level JSON, with deep merge.""" defaults = { 'document.item': { 'header': {'position': 'insidePage', 'layout': '2col', 'distribute': 'even', 'dateTitle': '\u043e\u0442'}, 'footer': {'fields': ['\u041a\u043e\u043c\u043c\u0435\u043d\u0442\u0430\u0440\u0438\u0439'], 'position': 'insidePage'}, 'tabularSections': {'container': 'pages', 'exclude': ['\u0414\u043e\u043f\u043e\u043b\u043d\u0438\u0442\u0435\u043b\u044c\u043d\u044b\u0435\u0420\u0435\u043a\u0432\u0438\u0437\u0438\u0442\u044b'], 'lineNumber': True}, 'additional': {'position': 'page', 'layout': '2col', 'bspGroup': True}, 'fieldDefaults': {'ref': {'choiceButton': True}, 'boolean': {'element': 'check'}}, 'commandBar': 'auto', 'properties': {'autoTitle': False}, }, 'document.list': { 'columns': 'all', 'columnType': 'labelField', 'hiddenRef': True, 'tableCommandBar': 'none', 'commandBar': 'auto', 'properties': {}, }, 'document.choice': { 'basedOn': 'document.list', 'properties': {'windowOpeningMode': 'LockOwnerWindow'}, }, 'catalog.item': { 'header': {'layout': '1col', 'distribute': 'left'}, 'codeDescription': {'layout': 'horizontal', 'order': 'descriptionFirst'}, 'parent': {'title': '\u0412\u0445\u043e\u0434\u0438\u0442 \u0432 \u0433\u0440\u0443\u043f\u043f\u0443', 'position': 'afterCodeDescription'}, 'owner': {'readOnly': True, 'position': 'first'}, 'tabularSections': {'container': 'inline', 'exclude': ['\u0414\u043e\u043f\u043e\u043b\u043d\u0438\u0442\u0435\u043b\u044c\u043d\u044b\u0435\u0420\u0435\u043a\u0432\u0438\u0437\u0438\u0442\u044b', '\u041f\u0440\u0435\u0434\u0441\u0442\u0430\u0432\u043b\u0435\u043d\u0438\u044f'], 'lineNumber': True}, 'footer': {'fields': [], 'position': 'none'}, 'additional': {'position': 'none', 'bspGroup': True}, 'fieldDefaults': {'ref': {'choiceButton': True}, 'boolean': {'element': 'check'}}, 'commandBar': 'auto', 'properties': {}, }, 'catalog.folder': { 'parent': {'title': '\u0412\u0445\u043e\u0434\u0438\u0442 \u0432 \u0433\u0440\u0443\u043f\u043f\u0443'}, 'properties': {'windowOpeningMode': 'LockOwnerWindow'}, }, 'catalog.list': { 'columns': 'all', 'columnType': 'labelField', 'hiddenRef': True, 'tableCommandBar': 'none', 'commandBar': 'auto', 'properties': {}, }, 'catalog.choice': { 'basedOn': 'catalog.list', 'choiceMode': True, 'properties': {'windowOpeningMode': 'LockOwnerWindow'}, }, # --- Register defaults --- 'informationRegister.record': { 'fieldDefaults': {'ref': {'choiceButton': True}, 'boolean': {'element': 'check'}}, 'properties': {'windowOpeningMode': 'LockOwnerWindow'}, }, 'informationRegister.list': { 'columns': 'all', 'columnType': 'labelField', 'tableCommandBar': 'none', 'commandBar': 'auto', 'properties': {}, }, 'accumulationRegister.list': { 'columns': 'all', 'columnType': 'labelField', 'tableCommandBar': 'none', 'commandBar': 'auto', 'properties': {}, }, # --- Catalog-like type defaults --- 'chartOfCharacteristicTypes.item': {'basedOn': 'catalog.item'}, 'chartOfCharacteristicTypes.folder': {'basedOn': 'catalog.folder'}, 'chartOfCharacteristicTypes.list': {'basedOn': 'catalog.list'}, 'chartOfCharacteristicTypes.choice': {'basedOn': 'catalog.choice'}, 'exchangePlan.item': {'basedOn': 'catalog.item'}, 'exchangePlan.list': {'basedOn': 'catalog.list'}, 'exchangePlan.choice': {'basedOn': 'catalog.choice'}, # --- ChartOfAccounts defaults --- 'chartOfAccounts.item': { 'parent': {'title': '\u041f\u043e\u0434\u0447\u0438\u043d\u0435\u043d \u0441\u0447\u0435\u0442\u0443'}, 'fieldDefaults': {'ref': {'choiceButton': True}, 'boolean': {'element': 'check'}}, 'properties': {}, }, 'chartOfAccounts.folder': { 'parent': {'title': '\u041f\u043e\u0434\u0447\u0438\u043d\u0435\u043d \u0441\u0447\u0435\u0442\u0443'}, 'properties': {'windowOpeningMode': 'LockOwnerWindow'}, }, 'chartOfAccounts.list': {'basedOn': 'catalog.list'}, 'chartOfAccounts.choice': {'basedOn': 'catalog.choice'}, } # Try built-in preset preset_dir = os.path.join(os.path.dirname(script_dir), 'presets') built_in_path = os.path.join(preset_dir, f'{preset_name}.json') if os.path.isfile(built_in_path): with open(built_in_path, 'r', encoding='utf-8-sig') as f: preset_data = json.load(f) for k in list(preset_data.keys()): defaults[k] = _deep_merge(defaults.get(k), preset_data[k]) # Try project-level preset (scan up from output path) scan_dir = os.path.dirname(out_path_resolved) while scan_dir: proj_preset = os.path.join(scan_dir, 'presets', 'skills', 'form', f'{preset_name}.json') if os.path.isfile(proj_preset): with open(proj_preset, 'r', encoding='utf-8-sig') as f: proj_data = json.load(f) for k in list(proj_data.keys()): defaults[k] = _deep_merge(defaults.get(k), proj_data[k]) break parent_dir = os.path.dirname(scan_dir) if parent_dir == scan_dir: break scan_dir = parent_dir # Resolve basedOn references for k in list(defaults.keys()): sect = defaults[k] if isinstance(sect, dict) and 'basedOn' in sect: base_name = sect['basedOn'] if base_name in defaults: merged = _deep_merge(defaults[base_name], sect) merged.pop('basedOn', None) defaults[k] = merged return defaults # Non-displayable types — cannot be bound to form elements NON_DISPLAYABLE_TYPES = ('ValueStorage', 'v8:ValueStorage', 'ХранилищеЗначения') def is_displayable_type(type_str): return not any(nd in type_str for nd in NON_DISPLAYABLE_TYPES) def new_field_element(attr_name, data_path, attr_type, field_defaults, extra_props=None): """Build a field element DSL entry.""" is_ref = bool(re.search(r'Ref\.', attr_type)) is_bool = bool(re.match(r'^\s*xs:boolean\s*$', attr_type) or attr_type == 'boolean' or re.search(r'Boolean', attr_type)) el_type = 'input' if is_bool and field_defaults and field_defaults.get('boolean') and field_defaults['boolean'].get('element') == 'check': el_type = 'check' el = OrderedDict() el[el_type] = attr_name el['path'] = data_path # (ChoiceButton у ref-полей платформа выводит сама; компилятор эмитит true по StartChoice-эвристике. # Явный choiceButton из декомпиляции эмитится verbatim. Дефолт-«true» здесь НЕ ставим, чтобы # from-object вывод совпадал с сертифицированным и не плодил ChoiceButton на каждом ref-поле.) # Extra props if extra_props: for k in extra_props: el[k] = extra_props[k] return el # --- Catalog DSL generators --- def generate_catalog_dsl(meta, preset_data, purpose): purpose_key = f"catalog.{purpose.lower()}" p = preset_data.get(purpose_key, {}) fd = p.get('fieldDefaults', {}) dispatch = { 'Folder': lambda: generate_catalog_folder_dsl(meta, p), 'List': lambda: generate_catalog_list_dsl(meta, p), 'Choice': lambda: generate_catalog_choice_dsl(meta, p, preset_data), 'Item': lambda: generate_catalog_item_dsl(meta, p, fd), } return dispatch[purpose]() def generate_catalog_folder_dsl(meta, p): elements = [] # Code (if CodeLength > 0) if meta.get('CodeLength', 0) > 0: elements.append(OrderedDict([('input', '\u041a\u043e\u0434'), ('path', '\u041e\u0431\u044a\u0435\u043a\u0442.Code')])) # Description elements.append(OrderedDict([('input', '\u041d\u0430\u0438\u043c\u0435\u043d\u043e\u0432\u0430\u043d\u0438\u0435'), ('path', '\u041e\u0431\u044a\u0435\u043a\u0442.Description')])) # Parent parent_title = p.get('parent', {}).get('title') parent_el = OrderedDict([('input', '\u0420\u043e\u0434\u0438\u0442\u0435\u043b\u044c'), ('path', '\u041e\u0431\u044a\u0435\u043a\u0442.Parent')]) if parent_title: parent_el['title'] = parent_title elements.append(parent_el) props = OrderedDict([('windowOpeningMode', 'LockOwnerWindow')]) if p.get('properties'): for k in p['properties']: props[k] = p['properties'][k] form_props = OrderedDict([('useForFoldersAndItems', 'Folders')]) for k in props: form_props[k] = props[k] return OrderedDict([ ('title', meta['Synonym']), ('properties', form_props), ('elements', elements), ('attributes', [ OrderedDict([('name', '\u041e\u0431\u044a\u0435\u043a\u0442'), ('type', f"CatalogObject.{meta['Name']}"), ('main', True)]) ]), ]) def generate_catalog_list_dsl(meta, p): columns = [] # Description always first columns.append(OrderedDict([('labelField', '\u041d\u0430\u0438\u043c\u0435\u043d\u043e\u0432\u0430\u043d\u0438\u0435'), ('path', '\u0421\u043f\u0438\u0441\u043e\u043a.Description')])) # Code if present if meta.get('CodeLength', 0) > 0: columns.append(OrderedDict([('labelField', '\u041a\u043e\u0434'), ('path', '\u0421\u043f\u0438\u0441\u043e\u043a.Code')])) # Custom attributes for attr in meta['Attributes']: if not is_displayable_type(attr['Type']): continue columns.append(OrderedDict([('labelField', attr['Name']), ('path', f"\u0421\u043f\u0438\u0441\u043e\u043a.{attr['Name']}")])) # Hidden ref if p.get('hiddenRef', True) is not False: columns.append(OrderedDict([('labelField', '\u0421\u0441\u044b\u043b\u043a\u0430'), ('path', '\u0421\u043f\u0438\u0441\u043e\u043a.Ref'), ('userVisible', False)])) table_el = OrderedDict([ ('table', '\u0421\u043f\u0438\u0441\u043e\u043a'), ('path', '\u0421\u043f\u0438\u0441\u043e\u043a'), ('rowPictureDataPath', '\u0421\u043f\u0438\u0441\u043e\u043a.DefaultPicture'), ('commandBarLocation', 'None'), ('tableAutofill', False), ('columns', columns), ]) # Hierarchical properties if meta.get('Hierarchical'): table_el['initialTreeView'] = 'ExpandTopLevel' table_el['enableStartDrag'] = True table_el['enableDrag'] = True form_props = OrderedDict() if p.get('properties'): for k in p['properties']: form_props[k] = p['properties'][k] return OrderedDict([ ('title', meta['Synonym']), ('properties', form_props), ('elements', [table_el]), ('attributes', [ OrderedDict([ ('name', '\u0421\u043f\u0438\u0441\u043e\u043a'), ('type', 'DynamicList'), ('main', True), ('settings', OrderedDict([('mainTable', f"Catalog.{meta['Name']}"), ('dynamicDataRead', True)])), ]) ]), ]) def generate_catalog_choice_dsl(meta, p, preset_data): # Start from list list_key = 'catalog.list' lp = preset_data.get(list_key, {}) dsl = generate_catalog_list_dsl(meta, lp) # Add choice-specific properties dsl['properties']['windowOpeningMode'] = 'LockOwnerWindow' if p.get('properties'): for k in p['properties']: dsl['properties'][k] = p['properties'][k] # Set ChoiceMode on table dsl['elements'][0]['choiceMode'] = True return dsl def generate_catalog_item_dsl(meta, p, fd): header_children = [] # Owner (if subordinate) if meta.get('Owners') and len(meta['Owners']) > 0: owner_el = OrderedDict([('input', '\u0412\u043b\u0430\u0434\u0435\u043b\u0435\u0446'), ('path', '\u041e\u0431\u044a\u0435\u043a\u0442.Owner'), ('readOnly', True)]) header_children.append(owner_el) # Code + Description cd_layout = (p.get('codeDescription') or {}).get('layout', 'horizontal') cd_order = (p.get('codeDescription') or {}).get('order', 'descriptionFirst') has_code = meta.get('CodeLength', 0) > 0 if cd_layout == 'horizontal' and has_code: cd_children = [] desc_el = OrderedDict([('input', '\u041d\u0430\u0438\u043c\u0435\u043d\u043e\u0432\u0430\u043d\u0438\u0435'), ('path', '\u041e\u0431\u044a\u0435\u043a\u0442.Description')]) code_el = OrderedDict([('input', '\u041a\u043e\u0434'), ('path', '\u041e\u0431\u044a\u0435\u043a\u0442.Code')]) if cd_order == 'descriptionFirst': cd_children = [desc_el, code_el] else: cd_children = [code_el, desc_el] header_children.append(OrderedDict([ ('group', 'horizontal'), ('name', '\u0413\u0440\u0443\u043f\u043f\u0430\u041a\u043e\u0434\u041d\u0430\u0438\u043c\u0435\u043d\u043e\u0432\u0430\u043d\u0438\u0435'), ('showTitle', False), ('representation', 'none'), ('children', cd_children), ])) else: # Vertical or no code header_children.append(OrderedDict([('input', '\u041d\u0430\u0438\u043c\u0435\u043d\u043e\u0432\u0430\u043d\u0438\u0435'), ('path', '\u041e\u0431\u044a\u0435\u043a\u0442.Description')])) if has_code: header_children.append(OrderedDict([('input', '\u041a\u043e\u0434'), ('path', '\u041e\u0431\u044a\u0435\u043a\u0442.Code')])) # Parent (for hierarchical catalogs) parent_pos = (p.get('parent') or {}).get('position', 'afterCodeDescription') parent_title = (p.get('parent') or {}).get('title') if meta.get('Hierarchical'): parent_el = OrderedDict([('input', '\u0420\u043e\u0434\u0438\u0442\u0435\u043b\u044c'), ('path', '\u041e\u0431\u044a\u0435\u043a\u0442.Parent')]) if parent_title: parent_el['title'] = parent_title if parent_pos == 'beforeCodeDescription': insert_idx = 1 if (meta.get('Owners') and len(meta['Owners']) > 0) else 0 header_children.insert(insert_idx, parent_el) else: # afterCodeDescription (default) header_children.append(parent_el) # Custom attributes -> header footer_field_names = (p.get('footer') or {}).get('fields', []) for attr in meta['Attributes']: if attr['Name'] in footer_field_names: continue if not is_displayable_type(attr['Type']): continue header_children.append(new_field_element(attr['Name'], f"\u041e\u0431\u044a\u0435\u043a\u0442.{attr['Name']}", attr['Type'], fd)) # Build root elements root_elements = [] # ГруппаШапка root_elements.append(OrderedDict([ ('group', 'vertical'), ('name', '\u0413\u0440\u0443\u043f\u043f\u0430\u0428\u0430\u043f\u043a\u0430'), ('showTitle', False), ('representation', 'none'), ('children', header_children), ])) # Tabular sections ts_exclude = ['\u0414\u043e\u043f\u043e\u043b\u043d\u0438\u0442\u0435\u043b\u044c\u043d\u044b\u0435\u0420\u0435\u043a\u0432\u0438\u0437\u0438\u0442\u044b', '\u041f\u0440\u0435\u0434\u0441\u0442\u0430\u0432\u043b\u0435\u043d\u0438\u044f'] if (p.get('tabularSections') or {}).get('exclude'): ts_exclude = p['tabularSections']['exclude'] ts_line_number = (p.get('tabularSections') or {}).get('lineNumber', True) visible_ts = [ts for ts in meta['TabularSections'] if ts['Name'] not in ts_exclude] for ts in visible_ts: ts_cols = [] if ts_line_number: ts_cols.append(OrderedDict([('labelField', f"{ts['Name']}\u041d\u043e\u043c\u0435\u0440\u0421\u0442\u0440\u043e\u043a\u0438"), ('path', f"\u041e\u0431\u044a\u0435\u043a\u0442.{ts['Name']}.LineNumber")])) for col in ts['Columns']: ts_cols.append(new_field_element(f"{ts['Name']}{col['Name']}", f"\u041e\u0431\u044a\u0435\u043a\u0442.{ts['Name']}.{col['Name']}", col['Type'], fd)) root_elements.append(OrderedDict([('table', ts['Name']), ('path', f"\u041e\u0431\u044a\u0435\u043a\u0442.{ts['Name']}"), ('columns', ts_cols)])) # Footer fields for fn in footer_field_names: f_attr = next((a for a in meta['Attributes'] if a['Name'] == fn), None) if f_attr: root_elements.append(new_field_element(f_attr['Name'], f"\u041e\u0431\u044a\u0435\u043a\u0442.{f_attr['Name']}", f_attr['Type'], fd)) # BSP group bsp_group = (p.get('additional') or {}).get('bspGroup', True) if bsp_group: root_elements.append(OrderedDict([('group', 'vertical'), ('name', '\u0413\u0440\u0443\u043f\u043f\u0430\u0414\u043e\u043f\u043e\u043b\u043d\u0438\u0442\u0435\u043b\u044c\u043d\u044b\u0435\u0420\u0435\u043a\u0432\u0438\u0437\u0438\u0442\u044b')])) # Properties form_props = OrderedDict() if p.get('properties'): for k in p['properties']: form_props[k] = p['properties'][k] # UseForFoldersAndItems if meta.get('Hierarchical') and meta.get('HierarchyType') == 'HierarchyFoldersAndItems': form_props['useForFoldersAndItems'] = 'Items' return OrderedDict([ ('title', meta['Synonym']), ('properties', form_props), ('elements', root_elements), ('attributes', [ OrderedDict([('name', '\u041e\u0431\u044a\u0435\u043a\u0442'), ('type', f"CatalogObject.{meta['Name']}"), ('main', True)]) ]), ]) # --- Document DSL generators --- def generate_document_dsl(meta, preset_data, purpose): purpose_key = f"document.{purpose.lower()}" p = preset_data.get(purpose_key, {}) fd = p.get('fieldDefaults', {}) dispatch = { 'List': lambda: generate_document_list_dsl(meta, p), 'Choice': lambda: generate_document_choice_dsl(meta, p, preset_data), 'Item': lambda: generate_document_item_dsl(meta, p, fd), } return dispatch[purpose]() def generate_document_list_dsl(meta, p): columns = [] # Standard columns: Number + Date columns.append(OrderedDict([('labelField', 'Номер'), ('path', 'Список.Number')])) columns.append(OrderedDict([('labelField', 'Дата'), ('path', 'Список.Date')])) # All custom attributes as labelField for attr in meta['Attributes']: if not is_displayable_type(attr['Type']): continue columns.append(OrderedDict([('labelField', attr['Name']), ('path', f"\u0421\u043f\u0438\u0441\u043e\u043a.{attr['Name']}")])) # Hidden ref if p.get('hiddenRef', True): columns.append(OrderedDict([('labelField', '\u0421\u0441\u044b\u043b\u043a\u0430'), ('path', '\u0421\u043f\u0438\u0441\u043e\u043a.Ref'), ('userVisible', False)])) table_el = OrderedDict([ ('table', '\u0421\u043f\u0438\u0441\u043e\u043a'), ('path', '\u0421\u043f\u0438\u0441\u043e\u043a'), ('rowPictureDataPath', '\u0421\u043f\u0438\u0441\u043e\u043a.DefaultPicture'), ('commandBarLocation', 'None'), ('tableAutofill', False), ('columns', columns), ]) form_props = OrderedDict() if p.get('properties'): for k in p['properties']: form_props[k] = p['properties'][k] return OrderedDict([ ('title', meta['Synonym']), ('properties', form_props), ('elements', [table_el]), ('attributes', [ OrderedDict([ ('name', '\u0421\u043f\u0438\u0441\u043e\u043a'), ('type', 'DynamicList'), ('main', True), ('settings', OrderedDict([('mainTable', f"Document.{meta['Name']}"), ('dynamicDataRead', True)])), ]) ]), ]) def generate_document_choice_dsl(meta, p, preset_data): list_key = 'document.list' lp = preset_data.get(list_key, {}) dsl = generate_document_list_dsl(meta, lp) dsl['properties']['windowOpeningMode'] = 'LockOwnerWindow' if p.get('properties'): for k in p['properties']: dsl['properties'][k] = p['properties'][k] return dsl def generate_document_item_dsl(meta, p, fd): header_pos = (p.get('header') or {}).get('position', 'insidePage') header_layout = (p.get('header') or {}).get('layout', '2col') header_distribute = (p.get('header') or {}).get('distribute', 'even') date_title = (p.get('header') or {}).get('dateTitle', '\u043e\u0442') footer_fields = (p.get('footer') or {}).get('fields', []) footer_pos = (p.get('footer') or {}).get('position', 'insidePage') add_pos = (p.get('additional') or {}).get('position', 'page') add_layout = (p.get('additional') or {}).get('layout', '2col') add_bsp_group = (p.get('additional') or {}).get('bspGroup', True) add_left = (p.get('additional') or {}).get('left', []) add_right = (p.get('additional') or {}).get('right', []) header_right = (p.get('header') or {}).get('right', []) ts_exclude = ['\u0414\u043e\u043f\u043e\u043b\u043d\u0438\u0442\u0435\u043b\u044c\u043d\u044b\u0435\u0420\u0435\u043a\u0432\u0438\u0437\u0438\u0442\u044b'] if (p.get('tabularSections') or {}).get('exclude'): ts_exclude = p['tabularSections']['exclude'] ts_line_number = (p.get('tabularSections') or {}).get('lineNumber', True) # Classify attributes claimed = {} for fn in footer_fields: claimed[fn] = 'footer' for fn in header_right: claimed[fn] = 'header.right' for fn in add_left: claimed[fn] = 'additional.left' for fn in add_right: claimed[fn] = 'additional.right' unclaimed = [attr for attr in meta['Attributes'] if attr['Name'] not in claimed and is_displayable_type(attr['Type'])] # Distribute unclaimed left_attrs = [] right_extra_attrs = [] if header_distribute == 'left': left_attrs = unclaimed elif header_distribute == 'right': right_extra_attrs = unclaimed else: # "even" import math half = math.ceil(len(unclaimed) / 2) if unclaimed else 0 left_attrs = unclaimed[:half] right_extra_attrs = unclaimed[half:] # Build ГруппаНомерДата num_date_children = [ OrderedDict([('input', '\u041d\u043e\u043c\u0435\u0440'), ('path', '\u041e\u0431\u044a\u0435\u043a\u0442.Number'), ('autoMaxWidth', False), ('width', 9)]), OrderedDict([('input', '\u0414\u0430\u0442\u0430'), ('path', '\u041e\u0431\u044a\u0435\u043a\u0442.Date'), ('title', date_title)]), ] num_date_group = OrderedDict([ ('group', 'horizontal'), ('name', '\u0413\u0440\u0443\u043f\u043f\u0430\u041d\u043e\u043c\u0435\u0440\u0414\u0430\u0442\u0430'), ('showTitle', False), ('children', num_date_children), ]) # Build left column left_children = [num_date_group] for attr in left_attrs: left_children.append(new_field_element(attr['Name'], f"\u041e\u0431\u044a\u0435\u043a\u0442.{attr['Name']}", attr['Type'], fd)) # Build right column right_children = [] for rn in header_right: r_attr = next((a for a in meta['Attributes'] if a['Name'] == rn), None) if r_attr: right_children.append(new_field_element(r_attr['Name'], f"\u041e\u0431\u044a\u0435\u043a\u0442.{r_attr['Name']}", r_attr['Type'], fd)) for attr in right_extra_attrs: right_children.append(new_field_element(attr['Name'], f"\u041e\u0431\u044a\u0435\u043a\u0442.{attr['Name']}", attr['Type'], fd)) # Header group if header_layout == '2col' and len(right_children) > 0: header_group = OrderedDict([ ('group', 'horizontal'), ('name', '\u0413\u0440\u0443\u043f\u043f\u0430\u0428\u0430\u043f\u043a\u0430'), ('showTitle', False), ('representation', 'none'), ('children', [ OrderedDict([('group', 'vertical'), ('name', '\u0413\u0440\u0443\u043f\u043f\u0430\u0428\u0430\u043f\u043a\u0430\u041b\u0435\u0432\u043e'), ('showTitle', False), ('children', left_children)]), OrderedDict([('group', 'vertical'), ('name', '\u0413\u0440\u0443\u043f\u043f\u0430\u0428\u0430\u043f\u043a\u0430\u041f\u0440\u0430\u0432\u043e'), ('showTitle', False), ('children', right_children)]), ]), ]) else: # 1col or no right items all_header_fields = left_children + right_children header_group = OrderedDict([ ('group', 'horizontal'), ('name', '\u0413\u0440\u0443\u043f\u043f\u0430\u0428\u0430\u043f\u043a\u0430'), ('showTitle', False), ('representation', 'none'), ('children', [ OrderedDict([('group', 'vertical'), ('name', '\u0413\u0440\u0443\u043f\u043f\u0430\u0428\u0430\u043f\u043a\u0430\u041b\u0435\u0432\u043e'), ('showTitle', False), ('children', all_header_fields)]), ]), ]) # Footer elements footer_elements = [] for fn in footer_fields: f_attr = next((a for a in meta['Attributes'] if a['Name'] == fn), None) if f_attr: footer_elements.append(new_field_element(f_attr['Name'], f"\u041e\u0431\u044a\u0435\u043a\u0442.{f_attr['Name']}", f_attr['Type'], fd)) # Visible tabular sections visible_ts = [ts for ts in meta['TabularSections'] if ts['Name'] not in ts_exclude] # Additional page content additional_page = None if add_pos == 'page': add_left_els = [] add_right_els = [] for aln in add_left: al_attr = next((a for a in meta['Attributes'] if a['Name'] == aln), None) if al_attr: add_left_els.append(new_field_element(al_attr['Name'], f"\u041e\u0431\u044a\u0435\u043a\u0442.{al_attr['Name']}", al_attr['Type'], fd)) for arn in add_right: ar_attr = next((a for a in meta['Attributes'] if a['Name'] == arn), None) if ar_attr: add_right_els.append(new_field_element(ar_attr['Name'], f"\u041e\u0431\u044a\u0435\u043a\u0442.{ar_attr['Name']}", ar_attr['Type'], fd)) add_page_children = [] if add_layout == '2col': add_page_children.append(OrderedDict([ ('group', 'horizontal'), ('name', '\u0413\u0440\u0443\u043f\u043f\u0430\u041f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b'), ('showTitle', False), ('children', [ OrderedDict([('group', 'vertical'), ('name', '\u0413\u0440\u0443\u043f\u043f\u0430\u041f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b\u041b\u0435\u0432\u043e'), ('showTitle', False), ('children', add_left_els)]), OrderedDict([('group', 'vertical'), ('name', '\u0413\u0440\u0443\u043f\u043f\u0430\u041f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b\u041f\u0440\u0430\u0432\u043e'), ('showTitle', False), ('children', add_right_els)]), ]), ])) else: add_page_children.extend(add_left_els + add_right_els) if add_bsp_group: add_page_children.append(OrderedDict([('group', 'vertical'), ('name', '\u0413\u0440\u0443\u043f\u043f\u0430\u0414\u043e\u043f\u043e\u043b\u043d\u0438\u0442\u0435\u043b\u044c\u043d\u044b\u0435\u0420\u0435\u043a\u0432\u0438\u0437\u0438\u0442\u044b')])) additional_page = OrderedDict([('page', '\u0413\u0440\u0443\u043f\u043f\u0430\u0414\u043e\u043f\u043e\u043b\u043d\u0438\u0442\u0435\u043b\u044c\u043d\u043e'), ('title', '\u0414\u043e\u043f\u043e\u043b\u043d\u0438\u0442\u0435\u043b\u044c\u043d\u043e'), ('children', add_page_children)]) # Build TS page elements ts_pages = [] for ts in visible_ts: ts_cols = [] if ts_line_number: ts_cols.append(OrderedDict([('labelField', f"{ts['Name']}\u041d\u043e\u043c\u0435\u0440\u0421\u0442\u0440\u043e\u043a\u0438"), ('path', f"\u041e\u0431\u044a\u0435\u043a\u0442.{ts['Name']}.LineNumber")])) for col in ts['Columns']: ts_cols.append(new_field_element(f"{ts['Name']}{col['Name']}", f"\u041e\u0431\u044a\u0435\u043a\u0442.{ts['Name']}.{col['Name']}", col['Type'], fd)) ts_pages.append(OrderedDict([ ('page', f"\u0413\u0440\u0443\u043f\u043f\u0430{ts['Name']}"), ('title', ts['Synonym']), ('children', [ OrderedDict([('table', ts['Name']), ('path', f"\u041e\u0431\u044a\u0435\u043a\u0442.{ts['Name']}"), ('columns', ts_cols)]) ]), ])) # Assemble root elements root_elements = [] if len(visible_ts) == 0: # Simple form - no Pages root_elements.append(header_group) if footer_elements: root_elements.extend(footer_elements) if add_bsp_group and add_pos != 'none': root_elements.append(OrderedDict([('group', 'vertical'), ('name', '\u0413\u0440\u0443\u043f\u043f\u0430\u0414\u043e\u043f\u043e\u043b\u043d\u0438\u0442\u0435\u043b\u044c\u043d\u044b\u0435\u0420\u0435\u043a\u0432\u0438\u0437\u0438\u0442\u044b')])) else: # Pages form if header_pos == 'abovePages': root_elements.append(header_group) pages_children = list(ts_pages) if additional_page: pages_children.append(additional_page) root_elements.append(OrderedDict([('pages', '\u0413\u0440\u0443\u043f\u043f\u0430\u0421\u0442\u0440\u0430\u043d\u0438\u0446\u044b'), ('children', pages_children)])) else: # insidePage (default) osnovnoe_children = [header_group] if footer_pos == 'insidePage' and footer_elements: osnovnoe_children.extend(footer_elements) pages_children = [] pages_children.append(OrderedDict([('page', '\u0413\u0440\u0443\u043f\u043f\u0430\u041e\u0441\u043d\u043e\u0432\u043d\u043e\u0435'), ('title', '\u041e\u0441\u043d\u043e\u0432\u043d\u043e\u0435'), ('children', osnovnoe_children)])) pages_children.extend(ts_pages) if additional_page: pages_children.append(additional_page) root_elements.append(OrderedDict([('pages', '\u0413\u0440\u0443\u043f\u043f\u0430\u0421\u0442\u0440\u0430\u043d\u0438\u0446\u044b'), ('children', pages_children)])) # Footer below pages if footer_pos == 'belowPages' and footer_elements: root_elements.extend(footer_elements) # Properties form_props = OrderedDict([('autoTitle', False)]) if p.get('properties'): for k in p['properties']: form_props[k] = p['properties'][k] return OrderedDict([ ('title', meta['Synonym']), ('properties', form_props), ('elements', root_elements), ('attributes', [ OrderedDict([('name', '\u041e\u0431\u044a\u0435\u043a\u0442'), ('type', f"DocumentObject.{meta['Name']}"), ('main', True)]) ]), ]) # --- InformationRegister DSL generators --- def generate_information_register_dsl(meta, preset_data, purpose): p_key = f"informationRegister.{purpose.lower()}" p = preset_data.get(p_key, {}) fd = p.get('fieldDefaults') or {'ref': {'choiceButton': True}, 'boolean': {'element': 'check'}} dispatch = { 'Record': lambda: generate_information_register_record_dsl(meta, p, fd), 'List': lambda: generate_information_register_list_dsl(meta, p), } return dispatch[purpose]() def generate_information_register_record_dsl(meta, p, fd): elements = OrderedDict() is_periodic = meta.get('Periodicity') and meta['Periodicity'] != 'Nonperiodical' # Period first (if periodic) if is_periodic: elements['\u041f\u0435\u0440\u0438\u043e\u0434'] = {'element': 'input', 'path': '\u0417\u0430\u043f\u0438\u0441\u044c.Period'} # Dimensions for dim in meta.get('Dimensions', []): if not is_displayable_type(dim['Type']): continue elements[dim['Name']] = new_field_element(dim['Name'], f"\u0417\u0430\u043f\u0438\u0441\u044c.{dim['Name']}", dim['Type'], fd) # Resources for res in meta.get('Resources', []): if not is_displayable_type(res['Type']): continue elements[res['Name']] = new_field_element(res['Name'], f"\u0417\u0430\u043f\u0438\u0441\u044c.{res['Name']}", res['Type'], fd) # Attributes for attr in meta['Attributes']: if not is_displayable_type(attr['Type']): continue elements[attr['Name']] = new_field_element(attr['Name'], f"\u0417\u0430\u043f\u0438\u0441\u044c.{attr['Name']}", attr['Type'], fd) props = OrderedDict([('windowOpeningMode', 'LockOwnerWindow')]) if p.get('properties'): for k in p['properties']: props[k] = p['properties'][k] return OrderedDict([ ('title', meta['Synonym']), ('properties', props), ('elements', elements), ('attributes', [ {'name': '\u0417\u0430\u043f\u0438\u0441\u044c', 'type': f"InformationRegisterRecordManager.{meta['Name']}", 'main': True, 'savedData': True} ]), ]) def generate_information_register_list_dsl(meta, p): is_periodic = meta.get('Periodicity') and meta['Periodicity'] != 'Nonperiodical' is_recorder_subordinate = meta.get('WriteMode') == 'RecorderSubordinate' columns_list = [] # Period if is_periodic: columns_list.append(OrderedDict([('labelField', '\u041f\u0435\u0440\u0438\u043e\u0434'), ('path', '\u0421\u043f\u0438\u0441\u043e\u043a.Period')])) # Recorder/LineNumber for subordinate registers if is_recorder_subordinate: columns_list.append(OrderedDict([('labelField', '\u0420\u0435\u0433\u0438\u0441\u0442\u0440\u0430\u0442\u043e\u0440'), ('path', '\u0421\u043f\u0438\u0441\u043e\u043a.Recorder')])) columns_list.append(OrderedDict([('labelField', '\u041d\u043e\u043c\u0435\u0440\u0421\u0442\u0440\u043e\u043a\u0438'), ('path', '\u0421\u043f\u0438\u0441\u043e\u043a.LineNumber')])) # Dimensions for dim in meta.get('Dimensions', []): if not is_displayable_type(dim['Type']): continue columns_list.append(OrderedDict([('labelField', dim['Name']), ('path', f"\u0421\u043f\u0438\u0441\u043e\u043a.{dim['Name']}")])) # Resources for res in meta.get('Resources', []): if not is_displayable_type(res['Type']): continue el_key = 'check' if re.match(r'^xs:boolean$|^Boolean$', res['Type']) else 'labelField' columns_list.append(OrderedDict([(el_key, res['Name']), ('path', f"\u0421\u043f\u0438\u0441\u043e\u043a.{res['Name']}")])) # Attributes for attr in meta['Attributes']: if not is_displayable_type(attr['Type']): continue el_key = 'check' if re.match(r'^xs:boolean$|^Boolean$', attr['Type']) else 'labelField' columns_list.append(OrderedDict([(el_key, attr['Name']), ('path', f"\u0421\u043f\u0438\u0441\u043e\u043a.{attr['Name']}")])) table_el = OrderedDict([ ('table', '\u0421\u043f\u0438\u0441\u043e\u043a'), ('path', '\u0421\u043f\u0438\u0441\u043e\u043a'), ('rowPictureDataPath', '\u0421\u043f\u0438\u0441\u043e\u043a.DefaultPicture'), ('commandBarLocation', 'None'), ('tableAutofill', False), ('columns', columns_list), ]) props = OrderedDict() if p.get('properties'): for k in p['properties']: props[k] = p['properties'][k] return OrderedDict([ ('title', meta['Synonym']), ('properties', props), ('elements', [table_el]), ('attributes', [ {'name': '\u0421\u043f\u0438\u0441\u043e\u043a', 'type': 'DynamicList', 'main': True, 'settings': {'mainTable': f"InformationRegister.{meta['Name']}", 'dynamicDataRead': True}} ]), ]) # --- AccumulationRegister DSL generators --- def generate_accumulation_register_dsl(meta, preset_data, purpose): p_key = f"accumulationRegister.{purpose.lower()}" p = preset_data.get(p_key, {}) dispatch = { 'List': lambda: generate_accumulation_register_list_dsl(meta, p), } return dispatch[purpose]() def generate_accumulation_register_list_dsl(meta, p): columns_list = [] # AccumulationRegisters always have Period, Recorder, LineNumber columns_list.append(OrderedDict([('labelField', '\u041f\u0435\u0440\u0438\u043e\u0434'), ('path', '\u0421\u043f\u0438\u0441\u043e\u043a.Period')])) columns_list.append(OrderedDict([('labelField', '\u0420\u0435\u0433\u0438\u0441\u0442\u0440\u0430\u0442\u043e\u0440'), ('path', '\u0421\u043f\u0438\u0441\u043e\u043a.Recorder')])) columns_list.append(OrderedDict([('labelField', '\u041d\u043e\u043c\u0435\u0440\u0421\u0442\u0440\u043e\u043a\u0438'), ('path', '\u0421\u043f\u0438\u0441\u043e\u043a.LineNumber')])) # Dimensions for dim in meta.get('Dimensions', []): if not is_displayable_type(dim['Type']): continue columns_list.append(OrderedDict([('labelField', dim['Name']), ('path', f"\u0421\u043f\u0438\u0441\u043e\u043a.{dim['Name']}")])) # Resources for res in meta.get('Resources', []): if not is_displayable_type(res['Type']): continue el_key = 'check' if re.match(r'^xs:boolean$|^Boolean$', res['Type']) else 'labelField' columns_list.append(OrderedDict([(el_key, res['Name']), ('path', f"\u0421\u043f\u0438\u0441\u043e\u043a.{res['Name']}")])) # Attributes for attr in meta['Attributes']: if not is_displayable_type(attr['Type']): continue el_key = 'check' if re.match(r'^xs:boolean$|^Boolean$', attr['Type']) else 'labelField' columns_list.append(OrderedDict([(el_key, attr['Name']), ('path', f"\u0421\u043f\u0438\u0441\u043e\u043a.{attr['Name']}")])) table_el = OrderedDict([ ('table', '\u0421\u043f\u0438\u0441\u043e\u043a'), ('path', '\u0421\u043f\u0438\u0441\u043e\u043a'), ('rowPictureDataPath', '\u0421\u043f\u0438\u0441\u043e\u043a.DefaultPicture'), ('commandBarLocation', 'None'), ('tableAutofill', False), ('columns', columns_list), ]) props = OrderedDict() if p.get('properties'): for k in p['properties']: props[k] = p['properties'][k] return OrderedDict([ ('title', meta['Synonym']), ('properties', props), ('elements', [table_el]), ('attributes', [ {'name': '\u0421\u043f\u0438\u0441\u043e\u043a', 'type': 'DynamicList', 'main': True, 'settings': {'mainTable': f"AccumulationRegister.{meta['Name']}", 'dynamicDataRead': True}} ]), ]) # --- ChartOfCharacteristicTypes (delegates to Catalog) --- def generate_chart_of_characteristic_types_dsl(meta, preset_data, purpose): # Delegate to Catalog generators -- meta already has CodeLength, DescriptionLength, etc. dsl = generate_catalog_dsl(meta, preset_data, purpose) # Post-patch: replace Catalog types with ChartOfCharacteristicTypes types cat_obj_type = f"CatalogObject.{meta['Name']}" ccoct_obj_type = f"ChartOfCharacteristicTypesObject.{meta['Name']}" cat_list_type = f"Catalog.{meta['Name']}" ccoct_list_type = f"ChartOfCharacteristicTypes.{meta['Name']}" for a in dsl['attributes']: if a.get('type') == cat_obj_type: a['type'] = ccoct_obj_type if a.get('type') == 'DynamicList' and a.get('settings') and a['settings'].get('mainTable') == cat_list_type: a['settings']['mainTable'] = ccoct_list_type # For Item forms: inject ValueType field after Description/ГруппаКодНаименование if purpose == 'Item' and dsl.get('elements'): vt_el = OrderedDict([('input', '\u0422\u0438\u043f\u0417\u043d\u0430\u0447\u0435\u043d\u0438\u044f'), ('path', '\u041e\u0431\u044a\u0435\u043a\u0442.ValueType')]) els = dsl['elements'] if isinstance(els, list): inserted = False new_els = [] for el in els: new_els.append(el) if not inserted and isinstance(el, dict): name = el.get('input') or el.get('group') or '' if name in ('\u041d\u0430\u0438\u043c\u0435\u043d\u043e\u0432\u0430\u043d\u0438\u0435', '\u0413\u0440\u0443\u043f\u043f\u0430\u041a\u043e\u0434\u041d\u0430\u0438\u043c\u0435\u043d\u043e\u0432\u0430\u043d\u0438\u0435'): new_els.append(vt_el) inserted = True if not inserted: new_els.append(vt_el) dsl['elements'] = new_els return dsl # --- ExchangePlan (delegates to Catalog) --- def generate_exchange_plan_dsl(meta, preset_data, purpose): # ExchangePlans are not hierarchical and have no Folder form dsl = generate_catalog_dsl(meta, preset_data, purpose) # Post-patch: replace Catalog types with ExchangePlan types cat_obj_type = f"CatalogObject.{meta['Name']}" ep_obj_type = f"ExchangePlanObject.{meta['Name']}" cat_list_type = f"Catalog.{meta['Name']}" ep_list_type = f"ExchangePlan.{meta['Name']}" for a in dsl['attributes']: if a.get('type') == cat_obj_type: a['type'] = ep_obj_type if a.get('type') == 'DynamicList' and a.get('settings') and a['settings'].get('mainTable') == cat_list_type: a['settings']['mainTable'] = ep_list_type # For Item forms: inject SentNo, ReceivedNo after Code/Description if purpose == 'Item' and dsl.get('elements'): sent_el = OrderedDict([('input', '\u041d\u043e\u043c\u0435\u0440\u041e\u0442\u043f\u0440\u0430\u0432\u043b\u0435\u043d\u043d\u043e\u0433\u043e'), ('path', '\u041e\u0431\u044a\u0435\u043a\u0442.SentNo'), ('readOnly', True)]) recv_el = OrderedDict([('input', '\u041d\u043e\u043c\u0435\u0440\u041f\u0440\u0438\u043d\u044f\u0442\u043e\u0433\u043e'), ('path', '\u041e\u0431\u044a\u0435\u043a\u0442.ReceivedNo'), ('readOnly', True)]) els = dsl['elements'] if isinstance(els, list): inserted = False new_els = [] for el in els: new_els.append(el) if not inserted and isinstance(el, dict): name = el.get('input') or el.get('group') or '' if name in ('\u041d\u0430\u0438\u043c\u0435\u043d\u043e\u0432\u0430\u043d\u0438\u0435', '\u0413\u0440\u0443\u043f\u043f\u0430\u041a\u043e\u0434\u041d\u0430\u0438\u043c\u0435\u043d\u043e\u0432\u0430\u043d\u0438\u0435'): new_els.append(sent_el) new_els.append(recv_el) inserted = True if not inserted: new_els.append(sent_el) new_els.append(recv_el) dsl['elements'] = new_els return dsl # --- ChartOfAccounts DSL generators --- def generate_chart_of_accounts_dsl(meta, preset_data, purpose): p_key = f"chartOfAccounts.{purpose.lower()}" p = preset_data.get(p_key, {}) fd = p.get('fieldDefaults') or {'ref': {'choiceButton': True}, 'boolean': {'element': 'check'}} dispatch = { 'Item': lambda: generate_chart_of_accounts_item_dsl(meta, p, fd, preset_data), 'Folder': lambda: generate_chart_of_accounts_folder_dsl(meta, p), 'List': lambda: generate_chart_of_accounts_list_dsl(meta, preset_data), 'Choice': lambda: generate_chart_of_accounts_choice_dsl(meta, preset_data), } return dispatch[purpose]() def generate_chart_of_accounts_item_dsl(meta, p, fd, preset_data): elements = [] # Header: Code + Parent header_left_children = [] if meta.get('CodeLength', 0) > 0: header_left_children.append(OrderedDict([('input', '\u041a\u043e\u0434'), ('path', '\u041e\u0431\u044a\u0435\u043a\u0442.Code')])) header_right_children = [] if meta.get('Hierarchical'): parent_title = (p.get('parent') or {}).get('title', '\u041f\u043e\u0434\u0447\u0438\u043d\u0435\u043d \u0441\u0447\u0435\u0442\u0443') header_right_children.append(OrderedDict([('input', '\u0420\u043e\u0434\u0438\u0442\u0435\u043b\u044c'), ('path', '\u041e\u0431\u044a\u0435\u043a\u0442.Parent'), ('title', parent_title)])) if len(header_right_children) > 0: elements.append(OrderedDict([ ('group', 'horizontal'), ('name', '\u0413\u0440\u0443\u043f\u043f\u0430\u0428\u0430\u043f\u043a\u0430'), ('showTitle', False), ('representation', 'none'), ('children', [ OrderedDict([('group', 'vertical'), ('name', '\u0413\u0440\u0443\u043f\u043f\u0430\u0428\u0430\u043f\u043a\u0430\u041b\u0435\u0432\u043e'), ('showTitle', False), ('children', header_left_children)]), OrderedDict([('group', 'vertical'), ('name', '\u0413\u0440\u0443\u043f\u043f\u0430\u0428\u0430\u043f\u043a\u0430\u041f\u0440\u0430\u0432\u043e'), ('showTitle', False), ('children', header_right_children)]), ]), ])) elif len(header_left_children) > 0: elements.extend(header_left_children) # Description if meta.get('DescriptionLength', 0) > 0: elements.append(OrderedDict([('input', '\u041d\u0430\u0438\u043c\u0435\u043d\u043e\u0432\u0430\u043d\u0438\u0435'), ('path', '\u041e\u0431\u044a\u0435\u043a\u0442.Description')])) # OffBalance elements.append(OrderedDict([('check', '\u0417\u0430\u0431\u0430\u043b\u0430\u043d\u0441\u043e\u0432\u044b\u0439'), ('path', '\u041e\u0431\u044a\u0435\u043a\u0442.OffBalance')])) # AccountingFlags as checkboxes if meta.get('AccountingFlags') and len(meta['AccountingFlags']) > 0: flag_children = [] for flag in meta['AccountingFlags']: flag_children.append(OrderedDict([('check', flag['Name']), ('path', f"\u041e\u0431\u044a\u0435\u043a\u0442.{flag['Name']}")])) elements.append(OrderedDict([ ('group', 'vertical'), ('name', '\u0413\u0440\u0443\u043f\u043f\u0430\u041f\u0440\u0438\u0437\u043d\u0430\u043a\u0438\u0423\u0447\u0435\u0442\u0430'), ('title', '\u041f\u0440\u0438\u0437\u043d\u0430\u043a\u0438 \u0443\u0447\u0435\u0442\u0430'), ('children', flag_children), ])) # ExtDimensionTypes table if meta.get('MaxExtDimensionCount', 0) > 0: # Column names are prefixed with the table name (like the generic TS path and stock 1C), # else a subconto flag column collides with a same-named account accounting-flag checkbox. ed_table = '\u0412\u0438\u0434\u044b\u0421\u0443\u0431\u043a\u043e\u043d\u0442\u043e' ed_cols = [] ed_cols.append(OrderedDict([('input', f"{ed_table}\u0412\u0438\u0434\u0421\u0443\u0431\u043a\u043e\u043d\u0442\u043e"), ('path', '\u041e\u0431\u044a\u0435\u043a\u0442.ExtDimensionTypes.ExtDimensionType')])) ed_cols.append(OrderedDict([('check', f"{ed_table}\u0422\u043e\u043b\u044c\u043a\u043e\u041e\u0431\u043e\u0440\u043e\u0442\u044b"), ('path', '\u041e\u0431\u044a\u0435\u043a\u0442.ExtDimensionTypes.TurnoversOnly')])) if meta.get('ExtDimensionAccountingFlags'): for ed_flag in meta['ExtDimensionAccountingFlags']: ed_cols.append(OrderedDict([('check', f"{ed_table}{ed_flag['Name']}"), ('path', f"\u041e\u0431\u044a\u0435\u043a\u0442.ExtDimensionTypes.{ed_flag['Name']}")])) elements.append(OrderedDict([ ('table', ed_table), ('path', '\u041e\u0431\u044a\u0435\u043a\u0442.ExtDimensionTypes'), ('columns', ed_cols), ])) # Custom attributes for attr in meta['Attributes']: if not is_displayable_type(attr['Type']): continue elements.append(new_field_element(attr['Name'], f"\u041e\u0431\u044a\u0435\u043a\u0442.{attr['Name']}", attr['Type'], fd)) # Tabular sections ts_exclude = ['\u0414\u043e\u043f\u043e\u043b\u043d\u0438\u0442\u0435\u043b\u044c\u043d\u044b\u0435\u0420\u0435\u043a\u0432\u0438\u0437\u0438\u0442\u044b', '\u041f\u0440\u0435\u0434\u0441\u0442\u0430\u0432\u043b\u0435\u043d\u0438\u044f'] for ts in meta['TabularSections']: if ts['Name'] in ts_exclude: continue ts_cols = [] for col in ts['Columns']: if not is_displayable_type(col['Type']): continue ts_cols.append(new_field_element(f"{ts['Name']}{col['Name']}", f"\u041e\u0431\u044a\u0435\u043a\u0442.{ts['Name']}.{col['Name']}", col['Type'], fd)) elements.append(OrderedDict([('table', ts['Name']), ('path', f"\u041e\u0431\u044a\u0435\u043a\u0442.{ts['Name']}"), ('columns', ts_cols)])) props = OrderedDict() if p.get('properties'): for k in p['properties']: props[k] = p['properties'][k] return OrderedDict([ ('title', meta['Synonym']), ('properties', props), ('elements', elements), ('attributes', [ {'name': '\u041e\u0431\u044a\u0435\u043a\u0442', 'type': f"ChartOfAccountsObject.{meta['Name']}", 'main': True, 'savedData': True} ]), ]) def generate_chart_of_accounts_folder_dsl(meta, p): elements = [] if meta.get('CodeLength', 0) > 0: elements.append(OrderedDict([('input', '\u041a\u043e\u0434'), ('path', '\u041e\u0431\u044a\u0435\u043a\u0442.Code')])) if meta.get('DescriptionLength', 0) > 0: elements.append(OrderedDict([('input', '\u041d\u0430\u0438\u043c\u0435\u043d\u043e\u0432\u0430\u043d\u0438\u0435'), ('path', '\u041e\u0431\u044a\u0435\u043a\u0442.Description')])) if meta.get('Hierarchical'): parent_title = (p.get('parent') or {}).get('title', '\u041f\u043e\u0434\u0447\u0438\u043d\u0435\u043d \u0441\u0447\u0435\u0442\u0443') elements.append(OrderedDict([('input', '\u0420\u043e\u0434\u0438\u0442\u0435\u043b\u044c'), ('path', '\u041e\u0431\u044a\u0435\u043a\u0442.Parent'), ('title', parent_title)])) props = OrderedDict([('windowOpeningMode', 'LockOwnerWindow')]) if p.get('properties'): for k in p['properties']: props[k] = p['properties'][k] return OrderedDict([ ('title', meta['Synonym']), ('useForFoldersAndItems', 'Folders'), ('properties', props), ('elements', elements), ('attributes', [ {'name': '\u041e\u0431\u044a\u0435\u043a\u0442', 'type': f"ChartOfAccountsObject.{meta['Name']}", 'main': True, 'savedData': True} ]), ]) def generate_chart_of_accounts_list_dsl(meta, preset_data): # Delegate to Catalog List and patch types dsl = generate_catalog_dsl(meta, preset_data, 'List') for a in dsl['attributes']: if a.get('type') == 'DynamicList' and a.get('settings') and a['settings'].get('mainTable') == f"Catalog.{meta['Name']}": a['settings']['mainTable'] = f"ChartOfAccounts.{meta['Name']}" return dsl def generate_chart_of_accounts_choice_dsl(meta, preset_data): dsl = generate_catalog_dsl(meta, preset_data, 'Choice') for a in dsl['attributes']: if a.get('type') == 'DynamicList' and a.get('settings') and a['settings'].get('mainTable') == f"Catalog.{meta['Name']}": a['settings']['mainTable'] = f"ChartOfAccounts.{meta['Name']}" return dsl # ═══════════════════════════════════════════════════════════════════════════ # END OF FROM-OBJECT MODE FUNCTIONS # ═══════════════════════════════════════════════════════════════════════════ def esc_xml(s): # Экранирование ТЕКСТА элемента (, ): только & < > . # Кавычки/апострофы в тексте 1С не экранирует (пишет литерально) — " ломал бы раундтрип. return s.replace('&', '&').replace('<', '<').replace('>', '>') def di_attr(el): # DisplayImportance — атрибут открывающего тега элемента (адаптивная важность). "" если нет. if isinstance(el, dict) and el.get('displayImportance'): return f' DisplayImportance="{esc_xml(str(el["displayImportance"]))}"' return '' # Базовая директория для @file-ссылок в query динсписка (устанавливается в main) QUERY_BASE_DIR = None 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 or os.getcwd(), 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_ml_items(lines, indent, val): # строка → один ru-элемент; объект {lang: text} → по элементу на язык if isinstance(val, dict): for k, v in val.items(): lines.append(f"{indent}") lines.append(f"{indent}\t{k}") lines.append(f"{indent}\t{esc_xml(str(v))}") lines.append(f"{indent}") else: lines.append(f"{indent}") lines.append(f"{indent}\tru") lines.append(f"{indent}\t{esc_xml(str(val))}") lines.append(f"{indent}") def emit_mltext(lines, indent, tag, text, xsi_type=None): attr = f' xsi:type="{xsi_type}"' if xsi_type else '' if not text: lines.append(f"{indent}<{tag}{attr}/>") return lines.append(f"{indent}<{tag}{attr}>") emit_ml_items(lines, f"{indent}\t", text) lines.append(f"{indent}") def emit_us_presentation(lines, indent, tag, val): # : плоская строка → xsi:type="xs:string"; мультиязычный → v8:LocalStringType if val is None: return if isinstance(val, str): lines.append(f'{indent}<{tag} xsi:type="xs:string">{esc_xml(val)}') else: emit_mltext(lines, indent, tag, val, xsi_type='v8:LocalStringType') # Каноничные GUID пустых контейнеров ListSettings (умолчание платформы, ~90% форм). CANON_FILTER_ID = 'dfcece9d-5077-440b-b6b3-45a5cb4538eb' CANON_ORDER_ID = '88619765-ccb3-46c6-ac52-38e9c992ebd4' CANON_CA_ID = 'b75fecce-942b-4aed-abc9-e6a02e460fb3' CANON_ITEMS_ID = '911b6018-f537-43e8-a417-da56b22f9aec' def new_uuid(): return str(uuid.uuid4()) # ───────────────────────────────────────────────────────────────────────────── # Настройки компоновщика ListSettings: filter/order/conditionalAppearance. # Грамматика DSL и эмиссия dcsset скопированы из skd-compile (навыки автономны). # ───────────────────────────────────────────────────────────────────────────── COMPARISON_TYPES = { '=': 'Equal', '<>': 'NotEqual', '>': 'Greater', '>=': 'GreaterOrEqual', '<': 'Less', '<=': 'LessOrEqual', 'in': 'InList', 'notIn': 'NotInList', 'inHierarchy': 'InHierarchy', 'inListByHierarchy': 'InListByHierarchy', 'contains': 'Contains', 'notContains': 'NotContains', 'beginsWith': 'BeginsWith', 'notBeginsWith': 'NotBeginsWith', 'like': 'Like', 'notLike': 'NotLike', 'подобно': 'Like', 'неподобно': 'NotLike', # рус. синоним 'filled': 'Filled', 'notFilled': 'NotFilled', } # Регистронезависимый лукап (зеркало PS-хэша): Like/LIKE/ПОДОБНО → канон _COMPARISON_TYPES_CI = {k.lower(): v for k, v in COMPARISON_TYPES.items()} _REF_TYPE_RE = re.compile( r'^(Перечисление|Справочник|ПланСчетов|Документ|ПланВидовХарактеристик|ПланВидовРасчета|' r'БизнесПроцесс|Задача|РегистрСведений|ПланОбмена|Catalog|Enum|Document|ChartOfAccounts|' r'ChartOfCharacteristicTypes|ChartOfCalculationTypes|BusinessProcess|Task|' r'InformationRegister|ExchangePlan)\.') def parse_filter_shorthand(s): result = {'field': '', 'op': 'Equal', 'value': None, 'use': True, 'userSettingID': None, 'viewMode': None, 'presentation': None} if re.search(r'@user', s): result['userSettingID'] = 'auto' s = re.sub(r'\s*@user', '', s) if re.search(r'@off', s): result['use'] = False s = re.sub(r'\s*@off', '', s) if re.search(r'@quickAccess', s): result['viewMode'] = 'QuickAccess' s = re.sub(r'\s*@quickAccess', '', s) if re.search(r'@normal', s): result['viewMode'] = 'Normal' s = re.sub(r'\s*@normal', '', s) if re.search(r'@inaccessible', s): result['viewMode'] = 'Inaccessible' s = re.sub(r'\s*@inaccessible', '', s) s = s.strip() 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'notLike\b', r'like\b', r'неподобно\b', r'подобно\b', r'notFilled\b', r'filled\b'] op_joined = '|'.join(op_patterns) m = re.match(r'^(.+?)\s+(' + op_joined + r')\s*(.*)?$', s, re.IGNORECASE) if m: result['field'] = m.group(1).strip() result['op'] = m.group(2).strip() val_part = m.group(3).strip() if m.group(3) else '' 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): # дата без valueType → emit_filter_item выведет StandardBeginningDate Custom (дефолт даты в фильтре) result['value'] = val_part 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' else: result['field'] = s return result def _value_type_for(v, explicit=None): if explicit: return explicit if isinstance(v, bool): return 'xs:boolean' if isinstance(v, (int, float)): return 'xs:decimal' vs = str(v) if re.match(r'^\d{4}-\d{2}-\d{2}T', vs): return 'xs:dateTime' if re.match(r'^-?\d+(\.\d+)?$', vs): return 'xs:decimal' if _REF_TYPE_RE.match(vs): return 'dcscor:DesignTimeValue' return 'xs:string' # Значение типа v8:Type (напр. тип «Неопределено» = :Undefined) ссылается на тип # платформы из namespace http://v8.1c.ru/8.2/data/types — платформа объявляет его ЛОКАЛЬНО # на теге значения (префикс авто: d6p1/d8p1/dN…). Без объявления QName битый. def _value_type_ns_attr(value_type, value): if value_type == 'v8:Type': m = re.match(r'^([A-Za-z]\w*):', str(value)) if m and m.group(1) not in ('xs', 'cfg', 'v8', 'v8ui', 'ent', 'dcscor', 'dcsset', 'dcssch'): return f' xmlns:{m.group(1)}="http://v8.1c.ru/8.2/data/types"' return '' def emit_filter_item(lines, item, indent): if item.get('group'): g = str(item['group']) group_type = {'And': 'AndGroup', 'Or': 'OrGroup', 'Not': 'NotGroup'}.get(g, g + 'Group') lines.append(f'{indent}') if item.get('use') is False: lines.append(f'{indent}\tfalse') # группа отключена (перед groupType) 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) obj = {'field': parsed['field'], 'op': parsed['op']} if parsed['use'] is False: obj['use'] = False if parsed['value'] is not None: obj['value'] = parsed['value'] if parsed.get('valueType'): obj['valueType'] = parsed['valueType'] if parsed.get('userSettingID'): obj['userSettingID'] = parsed['userSettingID'] if parsed.get('viewMode'): obj['viewMode'] = parsed['viewMode'] sub = obj emit_filter_item(lines, sub, f'{indent}\t') if item.get('presentation'): emit_us_presentation(lines, f'{indent}\t', 'dcsset:presentation', item['presentation']) if item.get('viewMode'): lines.append(f'{indent}\t{esc_xml(str(item["viewMode"]))}') if item.get('userSettingID'): guid = new_uuid() if str(item['userSettingID']) == 'auto' else str(item['userSettingID']) lines.append(f'{indent}\t{esc_xml(guid)}') if item.get('userSettingPresentation'): emit_us_presentation(lines, f'{indent}\t', 'dcsset:userSettingPresentation', item['userSettingPresentation']) lines.append(f'{indent}') return lines.append(f'{indent}') if item.get('use') is False: lines.append(f'{indent}\tfalse') lines.append(f'{indent}\t{esc_xml(str(item.get("field", "")))}') # Регистронезависимый лукап (зеркало PS): Like/LIKE/ПОДОБНО → канон; иначе — как есть comp_type = _COMPARISON_TYPES_CI.get(str(item.get('op')).lower()) if not comp_type: comp_type = str(item.get('op')) lines.append(f'{indent}\t{esc_xml(comp_type)}') val = item.get('value') if isinstance(val, list): if len(val) == 0: lines.append(f'{indent}\t') lines.append(f'{indent}\t\t') lines.append(f'{indent}\t\t-1') lines.append(f'{indent}\t') else: for v in val: vt = _value_type_for(v, item.get('valueType')) v_str = str(v).lower() if isinstance(v, bool) else esc_xml(str(v)) ns_attr = _value_type_ns_attr(vt, v) lines.append(f'{indent}\t{v_str}') elif val is not None and ( re.search(r'Standard(Beginning|End)Date$', str(item.get('valueType') or '')) or (not item.get('valueType') and isinstance(val, str) and re.match(r'^\d{4}-\d{2}-\d{2}T', val))): # Стандартная дата начала/окончания. Формы: объект {variant, date?} (Custom несёт ); # строка-вариант "BeginningOfThisDay" (именованный без даты); голая ISO-дата без valueType — # шорткат для Custom+date (дата в фильтре почти всегда SBD Custom, корпус 268 vs 2 xs:dateTime). sd_type = re.sub(r'^v8:', '', str(item['valueType'])) if item.get('valueType') else 'StandardBeginningDate' if isinstance(val, dict): variant = str(val.get('variant', '')); date_v = val.get('date') elif isinstance(val, str) and re.match(r'^\d{4}-\d{2}-\d{2}T', val): variant = 'Custom'; date_v = val else: variant = str(val); date_v = None lines.append(f'{indent}\t') lines.append(f'{indent}\t\t{esc_xml(variant)}') if date_v is not None: lines.append(f'{indent}\t\t{esc_xml(str(date_v))}') lines.append(f'{indent}\t') elif str(val) == '_': # "_" — маркер пустого значения: платформа эмитит пустой self-closing # (напр. — сравнение с незаданным полем). vt = str(item['valueType']) if item.get('valueType') else 'xs:string' lines.append(f'{indent}\t') elif val is not None: vt = _value_type_for(val, item.get('valueType')) v_str = str(val).lower() if isinstance(val, bool) else esc_xml(str(val)) ns_attr = _value_type_ns_attr(vt, val) lines.append(f'{indent}\t{v_str}') if item.get('presentation'): emit_us_presentation(lines, f'{indent}\t', 'dcsset:presentation', item['presentation']) 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'): emit_us_presentation(lines, f'{indent}\t', 'dcsset:userSettingPresentation', item['userSettingPresentation']) lines.append(f'{indent}') def emit_filter(lines, items, indent, block_view_mode=None, block_user_setting_id=None, block_user_setting_presentation=None): has_items = bool(items) and len(items) > 0 has_block_meta = (block_view_mode is not None) or (block_user_setting_id is not None) or (block_user_setting_presentation is not None) if not has_items and not has_block_meta: return lines.append(f'{indent}') for item in (items or []): if isinstance(item, str): parsed = parse_filter_shorthand(item) obj = {'field': parsed['field'], 'op': parsed['op']} if parsed['use'] is False: obj['use'] = False if parsed['value'] is not None: obj['value'] = parsed['value'] if parsed.get('valueType'): obj['valueType'] = parsed['valueType'] if parsed.get('userSettingID'): obj['userSettingID'] = parsed['userSettingID'] if parsed.get('viewMode'): obj['viewMode'] = parsed['viewMode'] emit_filter_item(lines, obj, f'{indent}\t') else: emit_filter_item(lines, item, f'{indent}\t') if block_view_mode is not None: lines.append(f'{indent}\t{esc_xml(str(block_view_mode))}') if block_user_setting_id is not None: uid = new_uuid() if str(block_user_setting_id) == 'auto' else str(block_user_setting_id) lines.append(f'{indent}\t{esc_xml(uid)}') if block_user_setting_presentation is not None: emit_us_presentation(lines, f'{indent}\t', 'dcsset:userSettingPresentation', block_user_setting_presentation) lines.append(f'{indent}') def emit_order(lines, items, indent, skip_auto=False, block_view_mode=None, block_user_setting_id=None, block_user_setting_presentation=None): has_items = bool(items) and len(items) > 0 has_block_meta = (block_view_mode is not None) or (block_user_setting_id is not None) or (block_user_setting_presentation is not None) if not has_items and not has_block_meta: return lines.append(f'{indent}') for item in (items or []): if isinstance(item, str): if item == 'Auto': if not skip_auto: lines.append(f'{indent}\t') else: parts = re.split(r'\s+', item) 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') else: if item.get('field') == 'Auto' or item.get('type') == 'auto': if not skip_auto: lines.append(f'{indent}\t') continue direction = str(item['direction']) if item.get('direction') else 'Asc' if re.match(r'(?i)^(desc|убыв)', direction): direction = 'Desc' elif re.match(r'(?i)^(asc|возр)', direction): direction = 'Asc' lines.append(f'{indent}\t') if item.get('use') is False: lines.append(f'{indent}\t\tfalse') lines.append(f'{indent}\t\t{esc_xml(str(item.get("field", "")))}') lines.append(f'{indent}\t\t{direction}') if item.get('viewMode'): lines.append(f'{indent}\t\t{esc_xml(str(item["viewMode"]))}') lines.append(f'{indent}\t') if block_view_mode is not None: lines.append(f'{indent}\t{esc_xml(str(block_view_mode))}') if block_user_setting_id is not None: uid = new_uuid() if str(block_user_setting_id) == 'auto' else str(block_user_setting_id) lines.append(f'{indent}\t{esc_xml(uid)}') if block_user_setting_presentation is not None: emit_us_presentation(lines, f'{indent}\t', 'dcsset:userSettingPresentation', block_user_setting_presentation) lines.append(f'{indent}') def emit_appearance_value(lines, key, val, indent): lines.append(f'{indent}') def _has_key(o, k): return isinstance(o, dict) and (k in o) def _get(o, k): return o.get(k) if isinstance(o, dict) else None is_top_level_line = _has_key(val, '@type') and (str(_get(val, '@type')) == 'Line') use_wrapper = False inner_val = val nested_items = None if is_top_level_line: if _has_key(val, 'use') and (_get(val, 'use') is False): use_wrapper = True if _has_key(val, 'items'): nested_items = _get(val, 'items') elif _has_key(val, 'value') and isinstance(val, dict): inner_val = _get(val, 'value') if _has_key(val, 'use') and (_get(val, 'use') is False): use_wrapper = True if _has_key(val, 'items'): nested_items = _get(val, 'items') if use_wrapper: lines.append(f'{indent}\tfalse') lines.append(f'{indent}\t{esc_xml(key)}') is_font_dict = isinstance(inner_val, dict) and inner_val.get('@type') is not None and str(inner_val.get('@type')) == 'Font' is_line_dict = _has_key(inner_val, '@type') and (str(_get(inner_val, '@type')) == 'Line') is_dict = isinstance(inner_val, dict) if is_line_dict: lw = _get(inner_val, 'width') if _has_key(inner_val, 'width') else 0 lg = ('true' if _get(inner_val, 'gap') else 'false') if _has_key(inner_val, 'gap') else 'false' ls = str(_get(inner_val, 'style')) if _has_key(inner_val, 'style') else 'None' lines.append(f'{indent}\t') lines.append(f'{indent}\t\t{esc_xml(ls)}') lines.append(f'{indent}\t') elif is_font_dict: attr_parts = [] for attr_name in ('ref', 'faceName', 'height', 'bold', 'italic', 'underline', 'strikeout', 'kind', 'scale'): if attr_name in inner_val: av = inner_val[attr_name] if av is not None: attr_parts.append(f'{attr_name}="{esc_xml(str(av))}"') lines.append(f'{indent}\t') elif is_dict and _has_key(inner_val, 'field'): # Ссылка на поле (dcscor:Field) — значение параметра оформления = поле компоновки lines.append(f'{indent}\t{esc_xml(str(_get(inner_val, "field")))}') elif is_dict: # Локализуемый текст параметра оформления: платформа объявляет xsi:type на dcscor:value emit_mltext(lines, f'{indent}\t', 'dcscor:value', inner_val, xsi_type='v8:LocalStringType') else: actual_val = str(inner_val) key_type_map = { 'Размещение': 'dcscor:DataCompositionTextPlacementType', 'ГоризонтальноеПоложение': 'v8ui:HorizontalAlign', 'ВертикальноеПоложение': 'v8ui:VerticalAlign', 'ОриентацияТекста': 'xs:decimal', 'РасположениеИтогов': 'dcscor:DataCompositionTotalPlacement', 'ТипМакета': 'dcsset:DataCompositionGroupTemplateType', } key_type = key_type_map.get(key) if key_type: lines.append(f'{indent}\t{esc_xml(actual_val)}') elif 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 == 'Текст' or key == 'Заголовок' or key == 'Формат': # Голая строка = плоский xs:string (нелокализованный литерал). Локализуемый → объект {ru,en}. # Пустая строка → самозакрывающийся тег (как у платформы). if actual_val == '': lines.append(f'{indent}\t') else: lines.append(f'{indent}\t{esc_xml(actual_val)}') elif re.match(r'^-?\d+(\.\d+)?$', actual_val): lines.append(f'{indent}\t{actual_val}') elif key == 'ЦветТекста' or key == 'ЦветФона' or key == 'ЦветГраницы': lines.append(f'{indent}\t{esc_xml(actual_val)}') else: lines.append(f'{indent}\t{esc_xml(actual_val)}') if nested_items: if isinstance(nested_items, dict): for nk, nv in nested_items.items(): emit_appearance_value(lines, nk, nv, f'{indent}\t') lines.append(f'{indent}') # === Группировка строк динамического списка (DCS-структура ListSettings) === # Линейная цепочка (каждый уровень = одно поле в groupItems; # вложенность — через дочерний ). Плоская модель уровней (список всегда линеен). def get_list_grouping_value(s): for k in ('grouping', 'structure', 'группировка'): if s.get(k): return s[k] return None def parse_list_grouping(grouping): # Шорткат "A > B > C" → массив имён; массив строк/объектов → как есть. if not grouping: return [] if isinstance(grouping, str): return [p.strip() for p in re.split(r'\s*>\s*', grouping) if p.strip()] return list(grouping) def emit_group_item_field(lines, level, indent): if isinstance(level, str): field, gt, pat = level, 'Items', 'None' pab = pae = '0001-01-01T00:00:00' else: field = str(level.get('field', '')) gt = str(level.get('groupType') or 'Items') pat = str(level.get('periodAdditionType') or 'None') pab = str(level.get('periodAdditionBegin') or '0001-01-01T00:00:00') pae = str(level.get('periodAdditionEnd') or '0001-01-01T00:00:00') lines.append(f'{indent}') lines.append(f'{indent}\t{esc_xml(field)}') lines.append(f'{indent}\t{esc_xml(gt)}') lines.append(f'{indent}\t{esc_xml(pat)}') # Авто-детект: ISO-дата → xs:dateTime, иначе путь → dcscor:Field. pab_t = 'xs:dateTime' if re.match(r'^\d{4}-\d{2}-\d{2}T', pab) else 'dcscor:Field' pae_t = 'xs:dateTime' if re.match(r'^\d{4}-\d{2}-\d{2}T', pae) else 'dcscor:Field' lines.append(f'{indent}\t{esc_xml(pab)}') lines.append(f'{indent}\t{esc_xml(pae)}') lines.append(f'{indent}') def emit_list_grouping_levels(lines, levels, i, indent): lines.append(f'{indent}') lines.append(f'{indent}\t') emit_group_item_field(lines, levels[i], f'{indent}\t\t') lines.append(f'{indent}\t') if i < len(levels) - 1: emit_list_grouping_levels(lines, levels, i + 1, f'{indent}\t') lines.append(f'{indent}') def emit_list_grouping(lines, grouping, indent): levels = parse_list_grouping(grouping) if not levels: return emit_list_grouping_levels(lines, levels, 0, indent) # === Вычисляемые поля DataSet динамического списка () === # Зеркало skd calculatedFields: shorthand "Имя [Заголовок]: тип = Выражение #noField #noFilter # #noGroup #noOrder" или объект. Форм-специфика: dcssch:-теги + presentationExpression/orderExpression. _CALC_RESTRICT_MAP = {'noField': 'field', 'noFilter': 'condition', 'noCondition': 'condition', 'noGroup': 'group', 'noOrder': 'order'} _DCS_COMMON_NS = 'http://v8.1c.ru/8.1/data-composition-system/common' def parse_calc_shorthand(s): restrict = re.findall(r'#(noField|noFilter|noCondition|noGroup|noOrder)\b', s) s = re.sub(r'\s*#(?:noField|noFilter|noCondition|noGroup|noOrder)\b', '', s) eq = s.find('=') lhs, rhs = (s[:eq], s[eq + 1:].strip()) if eq > 0 else (s, '') title = '' m = re.search(r'\[([^\]]+)\]', lhs) if m: title = m.group(1) lhs = re.sub(r'\s*\[[^\]]+\]', '', lhs) lhs = lhs.strip() typ, data_path = '', lhs if ':' in lhs: data_path, t = lhs.split(':', 1) data_path, typ = data_path.strip(), resolve_type_str(t.strip()) return {'dataPath': data_path, 'expression': rhs, 'type': typ, 'title': title, 'restrict': restrict} def emit_calc_fields(lines, calc_fields, indent): if not calc_fields: return for cf in calc_fields: if isinstance(cf, str): p = parse_calc_shorthand(cf) data_path, expression, title = p['dataPath'], p['expression'], p['title'] type_str = p['type'] restrict = [_CALC_RESTRICT_MAP[r] for r in p['restrict'] if r in _CALC_RESTRICT_MAP] pres_expr = order_expr = None else: data_path = str(cf.get('dataPath') or cf.get('field') or cf.get('name', '')) expression = str(cf.get('expression', '')) title = cf.get('title') type_str = cf.get('valueType') or cf.get('type') type_str = str(type_str) if type_str else None ur = cf.get('useRestriction') or cf.get('restrict') if isinstance(ur, dict): restrict = [k for k in ('field', 'condition', 'group', 'order') if ur.get(k)] elif isinstance(ur, str): restrict = [_CALC_RESTRICT_MAP.get(t.strip().lstrip('#'), t.strip().lstrip('#')) for t in ur.split() if t.strip()] elif isinstance(ur, list): restrict = [_CALC_RESTRICT_MAP.get(str(r), str(r)) for r in ur] else: restrict = [] pres_expr = cf.get('presentationExpression') order_expr = cf.get('orderExpression') ci = f'{indent}\t' lines.append(f'{indent}') lines.append(f'{ci}{esc_xml(data_path)}') lines.append(f'{ci}{esc_xml(expression)}') if title: emit_mltext(lines, ci, 'dcssch:title', title, xsi_type='v8:LocalStringType') if restrict: lines.append(f'{ci}') for r in ('field', 'condition', 'group', 'order'): if r in restrict: lines.append(f'{ci}\ttrue') lines.append(f'{ci}') if pres_expr: lines.append(f'{ci}{esc_xml(str(pres_expr))}') if order_expr: for oe in (order_expr if isinstance(order_expr, list) else [order_expr]): if isinstance(oe, str): expr_v, otype, auto = oe, 'Asc', 'false' else: expr_v = str(oe.get('expression', '')) otype = str(oe.get('orderType', 'Asc')) auto = 'true' if oe.get('autoOrder') else 'false' lines.append(f'{ci}') lines.append(f'{ci}\t{esc_xml(expr_v)}') lines.append(f'{ci}\t{otype}') lines.append(f'{ci}\t{auto}') lines.append(f'{ci}') if type_str: emit_dl_value_type(lines, type_str, ci) lines.append(f'{indent}') # Ограничения использования поля/вычисляемого поля (useRestriction / attributeUseRestriction). # Значение: объект {field?,condition?,group?,order?} | флаг-строка "#noField …" | массив. def parse_restrict(ur): if not ur: return [] if isinstance(ur, dict): return [k for k in ('field', 'condition', 'group', 'order') if ur.get(k)] if isinstance(ur, str): return [_CALC_RESTRICT_MAP.get(t.strip().lstrip('#'), t.strip().lstrip('#')) for t in ur.split() if t.strip()] if isinstance(ur, list): return [_CALC_RESTRICT_MAP.get(str(r), str(r)) for r in ur] return [] def emit_restrict_block(lines, tag, ur, indent): r = parse_restrict(ur) if not r: return lines.append(f'{indent}') for k in ('field', 'condition', 'group', 'order'): if k in r: lines.append(f'{indent}\ttrue') lines.append(f'{indent}') def emit_conditional_appearance(lines, items, indent, block_view_mode=None, block_user_setting_id=None, wrap_tag='dcsset:conditionalAppearance', block_user_setting_presentation=None): has_items = bool(items) and len(items) > 0 has_block_meta = (block_view_mode is not None) or (block_user_setting_id is not None) or (block_user_setting_presentation is not None) if not has_items and not has_block_meta: return lines.append(f'{indent}<{wrap_tag}>') for ca in (items or []): lines.append(f'{indent}\t') if ca.get('use') is False: lines.append(f'{indent}\t\tfalse') 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') if ca.get('filter') and len(ca['filter']) > 0: emit_filter(lines, ca['filter'], f'{indent}\t\t') else: lines.append(f'{indent}\t\t') 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') if ca.get('presentation'): if isinstance(ca['presentation'], dict): # Мультиязык → LocalStringType (платформа объявляет тип у локализованного presentation) lines.append(f'{indent}\t\t') emit_ml_items(lines, f'{indent}\t\t\t', ca['presentation']) lines.append(f'{indent}\t\t') else: lines.append(f'{indent}\t\t{esc_xml(str(ca["presentation"]))}') if ca.get('viewMode'): lines.append(f'{indent}\t\t{esc_xml(str(ca["viewMode"]))}') 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)}') if ca.get('userSettingPresentation'): emit_us_presentation(lines, f'{indent}\t\t', 'dcsset:userSettingPresentation', ca['userSettingPresentation']) if ca.get('useInDontUse') and len(ca['useInDontUse']) > 0: use_in_order = ['group', 'hierarchicalGroup', 'overall', 'fieldsHeader', 'header', 'parameters', 'filter', 'resourceFieldsHeader', 'overallHeader', 'overallResourceFieldsHeader'] sset = {str(n): True for n in ca['useInDontUse']} for n in use_in_order: if n in sset: tag = 'useIn' + n[0].upper() + n[1:] lines.append(f'{indent}\t\tDontUse') lines.append(f'{indent}\t') if block_view_mode is not None: lines.append(f'{indent}\t{esc_xml(str(block_view_mode))}') if block_user_setting_id is not None: uid = new_uuid() if str(block_user_setting_id) == 'auto' else str(block_user_setting_id) lines.append(f'{indent}\t{esc_xml(uid)}') if block_user_setting_presentation is not None: emit_us_presentation(lines, f'{indent}\t', 'dcsset:userSettingPresentation', block_user_setting_presentation) lines.append(f'{indent}') def write_utf8_bom(path, content): with open(path, 'w', encoding='utf-8-sig', newline='') as f: f.write(content) # --- ID allocator --- _next_id = 0 def new_id(): global _next_id _next_id += 1 return _next_id # Уникальность имён внутри коллекции (1С: элементы/реквизиты/команды/параметры/колонки — каждое своё # пространство имён). Дубль → битый XML, форма не открывается, поэтому fail-fast. _seen_element_names = set() # пул имён элементов (глобально по всей форме) def _ensure_unique(name, seen, kind): if name in seen: print(f"[ERROR] Duplicate {kind} name '{name}' — names must be unique within their collection in a 1C form (set a unique 'name')", file=sys.stderr) sys.exit(1) seen.add(name) # --- Event handler name generator --- EVENT_SUFFIX_MAP = { "OnChange": "\u041f\u0440\u0438\u0418\u0437\u043c\u0435\u043d\u0435\u043d\u0438\u0438", "StartChoice": "\u041d\u0430\u0447\u0430\u043b\u043e\u0412\u044b\u0431\u043e\u0440\u0430", "ChoiceProcessing": "\u041e\u0431\u0440\u0430\u0431\u043e\u0442\u043a\u0430\u0412\u044b\u0431\u043e\u0440\u0430", "AutoComplete": "\u0410\u0432\u0442\u043e\u041f\u043e\u0434\u0431\u043e\u0440", "Clearing": "\u041e\u0447\u0438\u0441\u0442\u043a\u0430", "Opening": "\u041e\u0442\u043a\u0440\u044b\u0442\u0438\u0435", "Click": "\u041d\u0430\u0436\u0430\u0442\u0438\u0435", "OnActivateRow": "\u041f\u0440\u0438\u0410\u043a\u0442\u0438\u0432\u0438\u0437\u0430\u0446\u0438\u0438\u0421\u0442\u0440\u043e\u043a\u0438", "BeforeAddRow": "\u041f\u0435\u0440\u0435\u0434\u041d\u0430\u0447\u0430\u043b\u043e\u043c\u0414\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0438\u044f", "BeforeDeleteRow": "\u041f\u0435\u0440\u0435\u0434\u0423\u0434\u0430\u043b\u0435\u043d\u0438\u0435\u043c", "BeforeRowChange": "\u041f\u0435\u0440\u0435\u0434\u041d\u0430\u0447\u0430\u043b\u043e\u043c\u0418\u0437\u043c\u0435\u043d\u0435\u043d\u0438\u044f", "OnStartEdit": "\u041f\u0440\u0438\u041d\u0430\u0447\u0430\u043b\u0435\u0420\u0435\u0434\u0430\u043a\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044f", "OnEndEdit": "\u041f\u0440\u0438\u041e\u043a\u043e\u043d\u0447\u0430\u043d\u0438\u0438\u0420\u0435\u0434\u0430\u043a\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044f", "Selection": "\u0412\u044b\u0431\u043e\u0440\u0421\u0442\u0440\u043e\u043a\u0438", "OnCurrentPageChange": "\u041f\u0440\u0438\u0421\u043c\u0435\u043d\u0435\u0421\u0442\u0440\u0430\u043d\u0438\u0446\u044b", "TextEditEnd": "\u041e\u043a\u043e\u043d\u0447\u0430\u043d\u0438\u0435\u0412\u0432\u043e\u0434\u0430\u0422\u0435\u043a\u0441\u0442\u0430", "URLProcessing": "\u041e\u0431\u0440\u0430\u0431\u043e\u0442\u043a\u0430\u041d\u0430\u0432\u0438\u0433\u0430\u0446\u0438\u043e\u043d\u043d\u043e\u0439\u0421\u0441\u044b\u043b\u043a\u0438", "DragStart": "\u041d\u0430\u0447\u0430\u043b\u043e\u041f\u0435\u0440\u0435\u0442\u0430\u0441\u043a\u0438\u0432\u0430\u043d\u0438\u044f", "Drag": "\u041f\u0435\u0440\u0435\u0442\u0430\u0441\u043a\u0438\u0432\u0430\u043d\u0438\u0435", "DragCheck": "\u041f\u0440\u043e\u0432\u0435\u0440\u043a\u0430\u041f\u0435\u0440\u0435\u0442\u0430\u0441\u043a\u0438\u0432\u0430\u043d\u0438\u044f", "Drop": "\u041f\u043e\u043c\u0435\u0449\u0435\u043d\u0438\u0435", "AfterDeleteRow": "\u041f\u043e\u0441\u043b\u0435\u0423\u0434\u0430\u043b\u0435\u043d\u0438\u044f", } KNOWN_EVENTS = { "input": ["OnChange", "StartChoice", "ChoiceProcessing", "AutoComplete", "TextEditEnd", "Clearing", "Creating", "EditTextChange"], "check": ["OnChange"], "radio": ["OnChange"], "label": ["Click", "URLProcessing"], "labelField": ["OnChange", "StartChoice", "ChoiceProcessing", "Click", "URLProcessing", "Clearing"], "table": ["Selection", "BeforeAddRow", "AfterDeleteRow", "BeforeDeleteRow", "OnActivateRow", "OnEditEnd", "OnStartEdit", "BeforeRowChange", "BeforeEditEnd", "ValueChoice", "OnActivateCell", "OnActivateField", "Drag", "DragStart", "DragCheck", "DragEnd", "OnGetDataAtServer", "BeforeLoadUserSettingsAtServer", "OnUpdateUserSettingSetAtServer", "OnChange"], "pages": ["OnCurrentPageChange"], "page": ["OnCurrentPageChange"], "button": ["Click"], "picField": ["OnChange", "StartChoice", "ChoiceProcessing", "Click", "Clearing"], "calendar": ["OnChange", "OnActivate"], "picture": ["Click"], "cmdBar": [], "popup": [], "group": [], } KNOWN_FORM_EVENTS = [ "OnCreateAtServer", "OnOpen", "BeforeClose", "OnClose", "NotificationProcessing", "ChoiceProcessing", "OnReadAtServer", "AfterWriteAtServer", "BeforeWriteAtServer", "AfterWrite", "BeforeWrite", "OnWriteAtServer", "FillCheckProcessingAtServer", "OnLoadDataFromSettingsAtServer", "BeforeLoadDataFromSettingsAtServer", "OnSaveDataInSettingsAtServer", "ExternalEvent", "OnReopen", "Opening", ] KNOWN_KEYS = { "group", "columnGroup", "buttonGroup", "input", "check", "radio", "label", "labelField", "table", "pages", "page", "button", "picture", "picField", "calendar", "cmdBar", "popup", "showInHeader", "radioButtonType", "choiceList", "columnsCount", "checkBoxType", "editMode", "name", "path", "title", "tooltip", "tooltipRepresentation", "extendedTooltip", "visible", "hidden", "enabled", "disabled", "readOnly", "userVisible", "events", "on", "handlers", "selectionMode", "showCurrentDate", "widthInMonths", "heightInMonths", "showMonthsPanel", "titleLocation", "representation", "width", "height", "horizontalStretch", "verticalStretch", "autoMaxWidth", "autoMaxHeight", "maxWidth", "maxHeight", "groupHorizontalAlign", "groupVerticalAlign", "horizontalAlign", "multiLine", "passwordMode", "choiceButton", "clearButton", "spinButton", "dropListButton", "markIncomplete", "skipOnInput", "inputHint", "textEdit", "choiceList", "wrap", "openButton", "listChoiceMode", "showInHeader", "showInFooter", "extendedEditMultipleValues", "chooseType", "autoCellHeight", "choiceButtonRepresentation", "footerHorizontalAlign", "headerHorizontalAlign", "headerDataPath", "headerFormat", "currentRowUse", "format", "editFormat", "choiceParameters", "choiceParameterLinks", "typeLink", "hyperlink", "formatted", "collapsedTitle", "showTitle", "united", "collapsed", "behavior", "children", "columns", "changeRowSet", "changeRowOrder", "autoInsertNewRow", "rowFilter", "header", "footer", "commandBarLocation", "searchStringLocation", "viewStatusLocation", "searchControlLocation", "excludedCommands", "pagesRepresentation", "type", "command", "commandName", "stdCommand", "parameter", "defaultButton", "locationInCommandBar", "displayImportance", "commandBar", "contextMenu", "commandSource", "src", "valuesPicture", "loadTransparent", "headerPicture", "footerPicture", "autofill", "choiceMode", "initialTreeView", "enableDrag", "enableStartDrag", "rowSelectionMode", "verticalLines", "horizontalLines", "rowPictureDataPath", "tableAutofill", "heightInTableRows", "multipleChoice", "searchOnInput", "shortcut", # dynamic-list table block "defaultItem", "useAlternationRowColor", "fileDragMode", "autoRefresh", "autoRefreshPeriod", "choiceFoldersAndItems", "restoreCurrentRow", "showRoot", "allowRootChoice", "updateOnDataChange", "allowGettingCurrentRowURL", "userSettingsGroup", "rowsPicture", # AutoCommandBar-маркер (autofill heuristic) на элементе/таблице "autoCmdBar", # дополнения командной панели таблицы (тип-ключи + свойства) "searchString", "viewStatus", "searchControl", "source", "horizontalLocation", "additions", # generic-скаляры (pass-through) "verticalAlign", "throughAlign", "enableContentChange", "pictureSize", "titleHeight", "childItemsWidth", "showLeftMargin", "cellHyperlink", "viewMode", "verticalScrollBar", "rowInputMode", "mask", "createButton", "fixingInTable", "verticalSpacing", # InputField choice-скаляры "choiceListButton", "quickChoice", "autoChoiceIncomplete", "choiceForm", "choiceHistoryOnInput", "footerDataPath", "minValue", "maxValue", # Button — пометка toggle-кнопки "checked", # спец-поля (документ/датчик/диаграмма) — тип-ключи + типоспец. скаляры "spreadsheet", "html", "textDoc", "formattedDoc", "progressBar", "trackBar", "chart", "ganttChart", "graphicalSchema", "planner", "periodField", "dendrogram", "ganttTable", "showPercent", "largeStep", "markingStep", "step", "horizontalScrollBar", "viewScalingMode", "output", "selectionShowMode", "protection", "edit", "showGrid", "showGroups", "showHeaders", "showRowAndColumnNames", "showCellNames", "pointerType", "drawingSelectionShowMode", "warningOnEditRepresentation", "markingAppearance", # report-form контекст (generic-скаляры элементов) "horizontalSpacing", "representationInContextMenu", "settingsNamedItemDetailedRepresentation", # хвост: высота элемента списка / ширина выпадающего списка / картинка кнопки выбора / прозрачный пиксель "itemHeight", "dropListWidth", "choiceButtonPicture", "transparentPixel", # хвост CI-форм: динамический заголовок / расширенное редактирование / высота таблицы "titleDataPath", "extendedEdit", "maxRowsCount", "autoMaxRowsCount", "heightControlVariant", "warningOnEdit", "nonselectedPictureText", "editTextUpdate", "footerText", } # picture/picField — НИЗКИЙ приоритет: 'picture' это и тип (PictureDecoration), и свойство-иконка # у popup/button/cmdBar. Тип-ключ владельца (popup/button/…) должен выиграть. # pages/page ПЕРЕД group: у Page/Pages ключ 'group' — это направление раскладки детей # (Horizontal), а не тип UsualGroup. Реальная UsualGroup ключа page/pages не несёт. TYPE_KEYS = ["columnGroup", "buttonGroup", "pages", "page", "group", "input", "check", "radio", "label", "labelField", "table", "button", "calendar", "cmdBar", "popup", "searchString", "viewStatus", "searchControl", "picField", "picture", "spreadsheet", "html", "textDoc", "formattedDoc", "progressBar", "trackBar", "chart", "ganttChart", "graphicalSchema", "planner", "periodField", "dendrogram"] # Synonyms: model often writes XML name or Russian (ПолеПереключателя/RadioButtonField → radio) ELEMENT_TYPE_SYNONYMS = { "commandBar": "cmdBar", "autoCommandBar": "autoCmdBar", "КоманднаяПанель": "cmdBar", "InputField": "input", "ПолеВвода": "input", "CheckBoxField": "check", "ПолеФлажка": "check", "RadioButtonField": "radio", "ПолеПереключателя": "radio", "radioButton": "radio", "PictureField": "picField", "ПолеКартинки": "picField", "LabelField": "labelField", "ПолеНадписи": "labelField", "CalendarField": "calendar", "ПолеКалендаря": "calendar", "LabelDecoration": "label", "Надпись": "label", "PictureDecoration": "picture", "Картинка": "picture", "UsualGroup": "group", "Группа": "group", "ОбычнаяГруппа": "group", "ColumnGroup": "columnGroup", "ГруппаКолонок": "columnGroup", "Pages": "pages", "ГруппаСтраниц": "pages", "Page": "page", "Страница": "page", "Table": "table", "Таблица": "table", "Button": "button", "Кнопка": "button", "Popup": "popup", "ВсплывающееМеню": "popup", # дополнения командной панели таблицы — forgiving: XML-тег/Type/рус.имя → канон "SearchStringAddition": "searchString", "SearchStringRepresentation": "searchString", "строкаПоиска": "searchString", "отображениеСтрокиПоиска": "searchString", "Отображение строки поиска": "searchString", "ViewStatusAddition": "viewStatus", "ViewStatusRepresentation": "viewStatus", "состояниеПросмотра": "viewStatus", "Состояние просмотра": "viewStatus", "SearchControlAddition": "searchControl", "SearchControl": "searchControl", "управлениеПоиском": "searchControl", "Управление поиском": "searchControl", # Спец-поля (документ/датчик) — XML-имя/рус. → канон "SpreadSheetDocumentField": "spreadsheet", "ПолеТабличногоДокумента": "spreadsheet", "HTMLDocumentField": "html", "ПолеHTMLДокумента": "html", "TextDocumentField": "textDoc", "ПолеТекстовогоДокумента": "textDoc", "FormattedDocumentField": "formattedDoc", "ПолеФорматированногоДокумента": "formattedDoc", "ProgressBarField": "progressBar", "ПолеИндикатора": "progressBar", "TrackBarField": "trackBar", "ПолеПолосыРегулирования": "trackBar", "ChartField": "chart", "ПолеДиаграммы": "chart", "GanttChartField": "ganttChart", "ПолеДиаграммыГанта": "ganttChart", "GraphicalSchemaField": "graphicalSchema", "ПолеГрафическойСхемы": "graphicalSchema", "PlannerField": "planner", "ПолеПланировщика": "planner", "PeriodField": "periodField", "ПолеПериода": "periodField", "DendrogramField": "dendrogram", "ПолеДендрограммы": "dendrogram", } # Тип-синонимы, применяемые ТОЛЬКО к строковому значению (имя элемента); объект/массив # у того же слова — companion-панель (свойство), см. normalize_panel_synonyms. STR_ONLY_TYPE_SYNONYMS = {"commandBar", "autoCommandBar", "КоманднаяПанель"} # Companion-панели как СВОЙСТВА (значение объект/массив): синоним → каноника. PANEL_SYNONYMS = { 'commandBar': ['commandBar', 'autoCommandBar', 'AutoCommandBar', 'autoCmdBar', 'cmdBar', 'КоманднаяПанель'], 'contextMenu': ['contextMenu', 'ContextMenu', 'КонтекстноеМеню'], } def normalize_panel_synonyms(el): if not isinstance(el, dict): return for canon, syns in PANEL_SYNONYMS.items(): for syn in syns: if syn in el and isinstance(el[syn], (list, dict)): if syn != canon and canon not in el: el[canon] = el.pop(syn) break # Maps Russian/English root of typed reference path to canonical English root REF_ROOT_SYNONYMS = { "Перечисление": "Enum", "Справочник": "Catalog", "Документ": "Document", "ПланСчетов": "ChartOfAccounts", "ПланВидовХарактеристик": "ChartOfCharacteristicTypes", "ПланВидовРасчета": "ChartOfCalculationTypes", "ПланВидовРасчёта": "ChartOfCalculationTypes", "ПланОбмена": "ExchangePlan", "БизнесПроцесс": "BusinessProcess", "Задача": "Task", "РегистрСведений": "InformationRegister", "РегистрНакопления": "AccumulationRegister", "РегистрБухгалтерии": "AccountingRegister", "РегистрРасчета": "CalculationRegister", "РегистрРасчёта": "CalculationRegister", "ЖурналДокументов": "DocumentJournal", "КритерийОтбора": "FilterCriterion", } ENUM_VALUE_SYNONYMS = {"EnumValue", "ЗначениеПеречисления"} def normalize_meta_type_ref(ref): # "Справочник.Контрагенты" → "Catalog.Контрагенты"; уже англ — без изменений if not ref: return ref dot = ref.find('.') if dot < 1: return ref root = ref[:dot] if root in REF_ROOT_SYNONYMS: return REF_ROOT_SYNONYMS[root] + ref[dot:] return ref def normalize_choice_value(value): """Returns dict {xsi_type, text} for a choiceList item value.""" if isinstance(value, bool): return {"xsi_type": "xs:boolean", "text": "true" if value else "false"} if isinstance(value, (int, float)): return {"xsi_type": "xs:decimal", "text": str(value)} s = "" if value is None else str(value) if not s: return {"xsi_type": "xs:string", "text": ""} # ISO datetime ("2020-01-01T00:00:00") → xs:dateTime if re.fullmatch(r'\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}', s): return {"xsi_type": "xs:dateTime", "text": s} # Raw-ссылка по GUID (метаданные.значение) "GUID.GUID" → xr:DesignTimeRef (всегда ссылка, не строка) if re.fullmatch(r'[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}\.[0-9a-fA-F]{8}-[0-9a-fA-F-]+', s): return {"xsi_type": "xr:DesignTimeRef", "text": s} parts = s.split(".") if len(parts) >= 2: root = parts[0] canon_root = None if root in REF_ROOT_SYNONYMS: canon_root = REF_ROOT_SYNONYMS[root] elif root in REF_ROOT_SYNONYMS.values(): canon_root = root if canon_root: type_name = parts[1] normalized = None if canon_root == "Enum": if len(parts) == 3 and parts[2] == 'EmptyRef': # "Enum.X.EmptyRef" — пустая ссылка, НЕ значение перечисления (без .EnumValue.) normalized = f"Enum.{type_name}.EmptyRef" elif len(parts) == 3: normalized = f"Enum.{type_name}.EnumValue.{parts[2]}" elif len(parts) >= 4: member = parts[2] if member in ENUM_VALUE_SYNONYMS: rest = ".".join(parts[3:]) else: rest = ".".join(parts[2:]) normalized = f"Enum.{type_name}.EnumValue.{rest}" else: if len(parts) >= 3: tail = ".".join(parts[1:]) normalized = f"{canon_root}.{tail}" if normalized: return {"xsi_type": "xr:DesignTimeRef", "text": normalized} return {"xsi_type": "xs:string", "text": s} def emit_choice_presentation(lines, pres, indent): """Accepts None/empty → ; str → ru only; dict → multi-lang.""" if pres is None or (isinstance(pres, str) and pres == ""): lines.append(f"{indent}") return if isinstance(pres, str): pairs = [("ru", pres)] elif isinstance(pres, dict): pairs = [(str(k), str(v)) for k, v in pres.items()] else: pairs = [("ru", str(pres))] lines.append(f"{indent}") for lang, content in pairs: lines.append(f"{indent}\t") lines.append(f"{indent}\t\t{lang}") lines.append(f"{indent}\t\t{esc_xml(content)}") lines.append(f"{indent}\t") lines.append(f"{indent}") def choice_value_tag(norm): # для choiceList/choiceParameters: пустой текст → самозакрывающийся тег (зеркало платформы). if not norm["text"]: return f'' return f'{esc_xml(norm["text"])}' def emit_choice_list(lines, el, indent): # — у RadioButtonField и InputField. Элемент: { value, presentation?/title? }. choice_list = el.get('choiceList') or [] if not choice_list: return lines.append(f'{indent}') item_indent = f'{indent}\t' for item in choice_list: if not isinstance(item, dict): continue val_raw = item.get('value', item.get('значение')) has_pres = any(k in item for k in ('presentation', 'представление', 'title')) pres_raw = item.get('presentation', item.get('представление', item.get('title'))) # valueType: явный xsi:type значения (системное перечисление ent:*, иной не-примитив) — # переопределяет авто-детект (normalize_choice_value вывела бы xs:string). vt_raw = item.get('valueType') if vt_raw == 'nil': norm = {'xsi_type': None, 'text': None, 'nil': True} elif vt_raw: norm = {'xsi_type': str(vt_raw), 'text': '' if val_raw is None else str(val_raw)} else: norm = normalize_choice_value(val_raw) if not has_pres: if norm.get('xsi_type') == 'xr:DesignTimeRef': tail = norm['text'].split('.')[-1] pres_raw = title_from_name(tail) else: pres_raw = norm.get('text') lines.append(f'{item_indent}') val_indent = f'{item_indent}\t' lines.append(f'{val_indent}') lines.append(f'{val_indent}0') lines.append(f'{val_indent}') emit_choice_presentation(lines, pres_raw, f'{val_indent}\t') val_tag = '' if norm.get('nil') else choice_value_tag(norm) lines.append(f'{val_indent}\t{val_tag}') lines.append(f'{val_indent}') lines.append(f'{item_indent}') lines.append(f'{indent}') def get_el_prop(obj, names): # Читает свойство из dict по списку синонимов (первый найденный, иначе None). if not isinstance(obj, dict): return None for n in names: if n in obj: return obj[n] return None def to_scalar_literal(s): # Литерал shorthand → тип: true/false → bool, целое/дробное → число, иначе строка. t = str(s).strip() if t.lower() == 'true': return True if t.lower() == 'false': return False if re.fullmatch(r'-?\d+', t): return int(t) if re.fullmatch(r'-?\d+\.\d+', t): return float(t) return t def from_choice_param_shorthand(s): # "name=value" либо "name=v1, v2, …" (запятые → массив). → {name, value}. eq = s.find('=') if eq < 0: return {'name': s.strip()} name = s[:eq].strip() rest = s[eq + 1:] if ',' in rest: return {'name': name, 'value': [to_scalar_literal(p) for p in rest.split(',')]} return {'name': name, 'value': to_scalar_literal(rest)} def from_choice_param_link_shorthand(s): # "name=dataPath" либо "name=dataPath:DontChange". → {name, dataPath, valueChange?}. eq = s.find('=') if eq < 0: return {'name': s.strip()} o = {'name': s[:eq].strip()} rest = s[eq + 1:].strip() m = re.fullmatch(r'(.*):(Clear|DontChange|очистить|неизменять)', rest, re.IGNORECASE) if m: o['dataPath'] = m.group(1).strip() o['valueChange'] = m.group(2) else: o['dataPath'] = rest return o def from_type_link_shorthand(s): # "dataPath" либо "dataPath#linkItem". → {dataPath, linkItem}. m = re.fullmatch(r'(.*)#(\d+)', str(s)) if m: return {'dataPath': m.group(1).strip(), 'linkItem': int(m.group(2))} return {'dataPath': str(s).strip()} def emit_choice_param_value(lines, value, indent): # Внутреннее значение параметра выбора (FormChoiceListDesTimeValue): + . # Скаляр → один Value; массив → v8:FixedArray из вложенных FormChoiceListDesTimeValue. lines.append(f'{indent}') if isinstance(value, (list, tuple)): lines.append(f'{indent}') for v in value: norm = normalize_choice_value(v) lines.append(f'{indent}\t') lines.append(f'{indent}\t\t') lines.append(f'{indent}\t\t{choice_value_tag(norm)}') lines.append(f'{indent}\t') lines.append(f'{indent}') else: norm = normalize_choice_value(value) lines.append(f'{indent}{choice_value_tag(norm)}') def emit_choice_parameters(lines, el, indent): # (параметры выбора поля ввода) — [{name, value}]. value через # normalize_choice_value; массив значений → FixedArray. Рус. синонимы имя/значение. cp = el.get('choiceParameters') or [] if not cp: return lines.append(f'{indent}') for item in cp: if isinstance(item, str): item = from_choice_param_shorthand(item) name = get_el_prop(item, ('name', 'имя')) has_val = isinstance(item, dict) and ('value' in item or 'значение' in item) val = get_el_prop(item, ('value', 'значение')) name_s = '' if name is None else str(name) lines.append(f'{indent}\t') # Параметр выбора без значения → (платформа, 13 в корпусе); # со значением (в т.ч. пустой строкой) → FormChoiceListDesTimeValue. if not has_val: lines.append(f'{indent}\t\t') else: lines.append(f'{indent}\t\t') emit_choice_param_value(lines, val, f'{indent}\t\t\t') lines.append(f'{indent}\t\t') lines.append(f'{indent}\t') lines.append(f'{indent}') def emit_choice_parameter_links(lines, el, indent): # (связи параметров выбора) — [{name, dataPath, valueChange?}]. # valueChange всегда эмитится, дефолт Clear; forgiving Clear/DontChange + рус. синонимы. cpl = el.get('choiceParameterLinks') or [] if not cpl: return lines.append(f'{indent}') for lk in cpl: if isinstance(lk, str): lk = from_choice_param_link_shorthand(lk) name = get_el_prop(lk, ('name', 'имя')) dp = get_el_prop(lk, ('dataPath', 'path', 'путь')) vc_raw = get_el_prop(lk, ('valueChange', 'режимИзменения')) vc = 'Clear' if vc_raw: s = str(vc_raw).lower() if s in ('clear', 'очистить', 'очистка'): vc = 'Clear' elif s in ('dontchange', 'неизменять', 'неменять', 'нет'): vc = 'DontChange' else: vc = str(vc_raw) name_s = '' if name is None else str(name) dp_s = '' if dp is None else str(dp) lines.append(f'{indent}\t') lines.append(f'{indent}\t\t{esc_xml(name_s)}') lines.append(f'{indent}\t\t{esc_xml(dp_s)}') lines.append(f'{indent}\t\t{vc}') lines.append(f'{indent}\t') lines.append(f'{indent}') def emit_type_link(lines, el, indent): # (связь по типу) — {dataPath, linkItem}. linkItem дефолт 0. tl = el.get('typeLink') if not tl: return if isinstance(tl, str): tl = from_type_link_shorthand(tl) dp = get_el_prop(tl, ('dataPath', 'path', 'путь')) li = get_el_prop(tl, ('linkItem', 'элементСвязи')) if li is None: li = 0 dp_s = '' if dp is None else str(dp) lines.append(f'{indent}') lines.append(f'{indent}\t{esc_xml(dp_s)}') lines.append(f'{indent}\t{li}') lines.append(f'{indent}') def normalize_radio_button_type(raw): if not raw: return "Auto" s = str(raw).strip().lower() if s in ("auto", "авто"): return "Auto" if s in ("radiobutton", "radiobuttons", "переключатель", "радио"): return "RadioButtons" if s in ("tumbler", "тумблер"): return "Tumbler" return str(raw).strip() def get_handler_name(element_name, event_name): suffix = EVENT_SUFFIX_MAP.get(event_name) if suffix: return f"{element_name}{suffix}" return f"{element_name}{event_name}" def get_element_name(el, type_key): if el.get('name'): return str(el['name']) return str(el.get(type_key, '')) # Собрать упорядоченный список событий элемента (имя, обработчик) из DSL. # Основной формат: el['events'] = { Событие: ИмяОбработчика } (None/"" → авто-имя по конвенции). # Legacy (принимается ради совместимости): el['on'] (массив) + el['handlers'] (переопределение имён). def get_event_pairs(el, element_name): pairs = [] events = el.get('events') if events: for ev_name, val in events.items(): handler = '' if val is None else str(val) if not handler: handler = get_handler_name(element_name, ev_name) pairs.append((ev_name, handler)) elif el.get('on'): handlers = el.get('handlers') or {} for evt in el['on']: evt_name = str(evt) if handlers.get(evt_name): handler = str(handlers[evt_name]) else: handler = get_handler_name(element_name, evt_name) pairs.append((evt_name, handler)) return pairs # Проверить, подключено ли событие к элементу (в любом из форматов). def test_element_event(el, event_name): events = el.get('events') if events and event_name in events: return True return event_name in (el.get('on') or []) def emit_events(lines, el, element_name, indent, type_key): pairs = get_event_pairs(el, element_name) if not pairs: return # Validate event names if type_key and type_key in KNOWN_EVENTS: allowed = KNOWN_EVENTS[type_key] for ev_name, _ in pairs: if allowed and str(ev_name) not in allowed: print(f"[WARN] Unknown event '{ev_name}' for {type_key} '{element_name}'. Known: {', '.join(allowed)}") lines.append(f"{indent}") for ev_name, handler in pairs: lines.append(f'{indent}\t{handler}') lines.append(f"{indent}") # Детектор «настоящей» inline-разметки (1С: ///… и ). Должен быть # идентичен form-decompile/form-compile.ps1, иначе гибрид-раундтрип поедет. _FMT_MARKUP_RE = re.compile(r'|<\s*(?:link|b|i|u|s|color|colorStyle|bgColor|bgColorStyle|font|fontSize|fontStyle|img)(?:\s|>)', re.I) def _has_real_markup(text): if text is None: return False vals = list(text.values()) if isinstance(text, dict) else [text] return any(_FMT_MARKUP_RE.search(str(v)) for v in vals) def resolve_ml_formatted(val): # {text, formatted} = явный override; строка/мапа → авто-детект formatted if isinstance(val, dict) and 'text' in val: return val['text'], bool(val.get('formatted')) return val, _has_real_markup(val) # ExtendedTooltip — это LabelDecoration: own-content (layout/оформление/флаги/hyperlink) ±текст. # Признак структурированной формы: объект с любым НЕ-текстовым ключом ({text,formatted}/{ru,en} → текст). COMPANION_STRUCT_KEYS = { 'width', 'autoMaxWidth', 'maxWidth', 'height', 'autoMaxHeight', 'maxHeight', 'verticalAlign', 'titleHeight', 'horizontalStretch', 'verticalStretch', 'horizontalAlign', 'groupHorizontalAlign', 'groupVerticalAlign', 'visible', 'hidden', 'enabled', 'disabled', 'hyperlink', 'events', 'tooltip', 'textColor', 'backColor', 'borderColor', 'font', 'border', 'цветтекста', 'цветфона', 'цветрамки', 'шрифт', 'рамка', } def emit_companion_title(lines, content, indent): text, fmt = resolve_ml_formatted(content) lines.append(f'{indent}') emit_ml_items(lines, f'{indent}\t', text) lines.append(f'{indent}') def emit_companion(lines, tag, name, indent, content=None): cid = new_id() has_content = content is not None and not (isinstance(content, str) and content == '') if not has_content: lines.append(f'{indent}<{tag} name="{name}" id="{cid}"/>') return inner = f'{indent}\t' # DI-Attr от собственного объекта компаньона (не от владельца) — зеркало ps1 lines.append(f'{indent}<{tag} name="{name}" id="{cid}"{di_attr(content if isinstance(content, dict) else None)}>') if isinstance(content, dict) and any(k in content for k in COMPANION_STRUCT_KEYS): # own-content ПЕРЕД Title (в корпусе layout-first 582 vs 10). emit_common_flags(lines, content, inner) if content.get('hyperlink') is True: lines.append(f'{inner}true') emit_layout(lines, content, inner) emit_appearance(lines, content, inner, 'decoration') if 'text' in content: emit_companion_title(lines, content, inner) # ToolTip компаньона (подсказка самой расширенной подсказки) — после Title (порядок схемы LabelDecoration) if content.get('tooltip'): emit_mltext(lines, inner, 'ToolTip', content['tooltip']) # События компаньона (ExtendedTooltip = LabelDecoration: напр. URLProcessing у hyperlink-подсказки) emit_events(lines, content, name, inner, 'label') else: emit_companion_title(lines, content, inner) lines.append(f'{indent}') def emit_companion_panel(lines, tag, name, indent, panel): # Companion-командная-панель (ContextMenu/AutoCommandBar) с контентом: { autofill?, horizontalAlign?, children?[] } # или массив = shorthand для { children }. Пусто/нет → self-closing. cid = new_id() autofill = None halign = None children = None if isinstance(panel, list): children = panel elif panel is not None: if panel.get('autofill') is not None: autofill = bool(panel.get('autofill')) if panel.get('horizontalAlign'): halign = str(panel.get('horizontalAlign')) children = panel.get('children') has_children = bool(children) and len(children) > 0 # Платформа пишет только при false; true = дефолт (тег опускается). emit_af_false = (autofill is False) if not emit_af_false and not has_children and not halign: lines.append(f'{indent}<{tag} name="{name}" id="{cid}"/>') return lines.append(f'{indent}<{tag} name="{name}" id="{cid}"{di_attr(panel if isinstance(panel, dict) else None)}>') if halign: lines.append(f'{indent}\t{halign}') if emit_af_false: lines.append(f'{indent}\tfalse') if has_children: lines.append(f'{indent}\t') for c in children: emit_element(lines, c, f'{indent}\t\t', in_cmd_bar=True) lines.append(f'{indent}\t') lines.append(f'{indent}') # Дополнения командной панели таблицы: тип DSL → XML-тег + AdditionSource.Type + суффикс имени. ADDITION_TYPE_MAP = { 'searchString': {'tag': 'SearchStringAddition', 'type': 'SearchStringRepresentation', 'suffix': 'СтрокаПоиска'}, 'viewStatus': {'tag': 'ViewStatusAddition', 'type': 'ViewStatusRepresentation', 'suffix': 'СостояниеПросмотра'}, 'searchControl': {'tag': 'SearchControlAddition', 'type': 'SearchControl', 'suffix': 'УправлениеПоиском'}, } ADDITION_KEY_SYNONYMS = { 'searchString': ['SearchStringAddition', 'SearchStringRepresentation', 'строкаПоиска', 'отображениеСтрокиПоиска'], 'viewStatus': ['ViewStatusAddition', 'ViewStatusRepresentation', 'состояниеПросмотра'], 'searchControl': ['SearchControlAddition', 'SearchControl', 'управлениеПоиском'], } # Имя текущей таблицы — дефолт source для кастомных дополнений в commandBar. _current_table_name = {'name': None} def get_hlocation(el): # HorizontalLocation: auto (дефолт, опускаем) / left / right; forgiving + рус. if not isinstance(el, dict): return None v = el.get('horizontalLocation') if not v: return None s = str(v).lower() if s in ('auto', 'авто'): return None if s in ('left', 'слева', 'лево'): return 'Left' if s in ('right', 'справа', 'право'): return 'Right' if s in ('center', 'центр', 'по центру'): return 'Center' return str(v) def emit_addition_body(lines, props, source, src_type, add_name, indent): # Тело дополнения: AdditionSource + свойства (как у поля) + companions. props может быть None. inner = f'{indent}\t' lines.append(f'{inner}') lines.append(f'{inner}\t{source}') lines.append(f'{inner}\t{src_type}') lines.append(f'{inner}') if props: if props.get('title'): emit_mltext(lines, inner, 'Title', props['title']) emit_common_flags(lines, props, inner) if props.get('tooltip'): emit_mltext(lines, inner, 'ToolTip', props['tooltip']) if props.get('tooltipRepresentation'): lines.append(f'{inner}{props["tooltipRepresentation"]}') hl = get_hlocation(props) if hl: lines.append(f'{inner}{hl}') emit_layout(lines, props, inner) emit_appearance(lines, props, inner, 'field') emit_companion(lines, 'ContextMenu', f'{add_name}КонтекстноеМеню', inner) emit_companion(lines, 'ExtendedTooltip', f'{add_name}РасширеннаяПодсказка', inner) def emit_addition(lines, el, name, eid, type_key, indent): # Кастомное дополнение (тип-элемент в commandBar): source дефолтит в текущую таблицу. m = ADDITION_TYPE_MAP[type_key] source = el.get('source') or _current_table_name['name'] or '' lines.append(f'{indent}<{m["tag"]} name="{name}" id="{eid}"{di_attr(el)}>') emit_addition_body(lines, el, source, m['type'], name, indent) lines.append(f'{indent}') def emit_table_addition(lines, type_key, table_name, indent, override=None): # Стандартное табличное дополнение (авто-генерация). override — объект отклонений из карты additions. m = ADDITION_TYPE_MAP[type_key] add_name = f'{table_name}{m["suffix"]}' aid = new_id() lines.append(f'{indent}<{m["tag"]} name="{add_name}" id="{aid}">') emit_addition_body(lines, override, table_name, m['type'], add_name, indent) lines.append(f'{indent}') def get_addition_override(additions, type_key): # Прочитать override-объект для типа из per-table карты additions (с синонимами). if not isinstance(additions, dict): return None for k in [type_key] + ADDITION_KEY_SYNONYMS[type_key]: if k in additions: return additions[k] return None # Role-adjustable boolean (xr:Common + 0..N xr:Value name="Role.X"). # Единый механизм платформы: UserVisible (элементы), View/Edit (атрибуты), Use (команды/кнопки). # Значение DSL: скаляр bool → только ; объект { common, roles:{ Имя: bool } } → +пер-ролевые исключения. # Имя роли принимаем с/без префикса "Role." (forgiving); на выход всегда с префиксом. def emit_xr_flag(lines, tag, val, indent): if val is None: return if isinstance(val, bool): lines.append(f"{indent}<{tag}>") lines.append(f"{indent}\t{'true' if val else 'false'}") lines.append(f"{indent}") return # объектная форма { common, roles } common = bool(val.get('common')) if val.get('common') is not None else False lines.append(f"{indent}<{tag}>") lines.append(f"{indent}\t{'true' if common else 'false'}") roles = val.get('roles') if roles: for rname, rval in roles.items(): # Forgiving: имя без префикса, с "Role." или кириллическим "Роль." → нормализуем в "Role.". # Роль по GUID (заимствованная/расширение — name="" без префикса) эмитим как есть. rn = re.sub(r'^(Role|Роль)\.', '', rname) if not re.match(r'^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$', rn): rn = "Role." + rn lines.append(f"{indent}\t{'true' if rval else 'false'}") lines.append(f"{indent}") def emit_common_flags(lines, el, indent): if el.get('visible') is False or el.get('hidden') is True: lines.append(f"{indent}false") if el.get('userVisible') is not None: emit_xr_flag(lines, 'UserVisible', el.get('userVisible'), indent) if el.get('enabled') is False or el.get('disabled') is True: lines.append(f"{indent}false") if el.get('readOnly') is True: lines.append(f"{indent}true") # Общие свойства элемента (любой тип, включая Button/cmdBar): default/skip/drag. def emit_common_element_props(lines, el, indent): if el.get('defaultItem') is True: lines.append(f"{indent}true") if 'skipOnInput' in el and el['skipOnInput'] is not None: siv = 'true' if el['skipOnInput'] is True else 'false' lines.append(f"{indent}{siv}") # EnableStartDrag — фактическое значение (платформа эмитит и явный false, напр. SpreadSheet) if el.get('enableStartDrag') is not None: lines.append(f'{indent}{"true" if el["enableStartDrag"] else "false"}') if el.get('fileDragMode'): lines.append(f"{indent}{el['fileDragMode']}") # Cell-свойства поля в таблице (общие для Input/Label/Picture/CheckBox): захват «как есть» for key, tag in (('showInHeader', 'ShowInHeader'), ('showInFooter', 'ShowInFooter'), ('autoCellHeight', 'AutoCellHeight')): if el.get(key) is not None: lines.append(f'{indent}<{tag}>{"true" if el[key] else "false"}') # Динамический заголовок колонки-группы из данных (HeaderDataPath) — перед HeaderHorizontalAlign (порядок XSD) if el.get('headerDataPath'): lines.append(f"{indent}{esc_xml(str(el['headerDataPath']))}") if el.get('footerHorizontalAlign'): lines.append(f"{indent}{el['footerHorizontalAlign']}") if el.get('headerHorizontalAlign'): lines.append(f"{indent}{el['headerHorizontalAlign']}") # Формат заголовка колонки-группы (ML-текст) — после HeaderHorizontalAlign (порядок XSD) if el.get('headerFormat'): emit_mltext(lines, indent, 'HeaderFormat', el['headerFormat']) def emit_picture_ref(lines, val, pic_tag, indent): """Картинка-ссылка с прозрачностью (HeaderPicture/FooterPicture/ValuesPicture/Page Picture). Платформа ВСЕГДА эмитит → пишем всегда (false по умолчанию). Значение: скаляр (Ref) ИЛИ объект {src, loadTransparent, transparentPixel}. src с префиксом "abs:" → встроенная картинка ; иначе .""" if not val: return tpx = None if isinstance(val, str): src, lt = val, False else: src = val.get('src') lt = val.get('loadTransparent') is True tpx = val.get('transparentPixel') if not src: return src_str = str(src) lines.append(f"{indent}<{pic_tag}>") if src_str.startswith('abs:'): lines.append(f"{indent}\t{esc_xml(src_str[4:])}") else: lines.append(f"{indent}\t{esc_xml(src_str)}") lines.append(f'{indent}\t{"true" if lt else "false"}') if tpx: lines.append(f'{indent}\t') lines.append(f"{indent}") def emit_column_pics(lines, el, indent): """Картинки заголовка/подвала колонки поля — по схеме сразу после , перед тип-специфичными элементами и layout (порядок XDTO строгий именно здесь).""" emit_picture_ref(lines, el.get('headerPicture'), 'HeaderPicture', indent) emit_picture_ref(lines, el.get('footerPicture'), 'FooterPicture', indent) def emit_command_picture(lines, pic, elem_lt, indent): """ кнопки/попапа/команды. Дефолт LoadTransparent=true, отклонение false (обратная конвенция относительно header/values-картинок). Прощающий ввод: принимает скаляр (Ref) ИЛИ объект {src, loadTransparent} — на случай если модель опишет картинку объектно по аналогии с headerPicture. elem_lt — legacy элемент-уровневый ключ loadTransparent (если в объекте флаг не задан).""" if not pic: return lt = None tpx = None if isinstance(pic, str): src = pic else: src = pic.get('src') if pic.get('loadTransparent') is not None: lt = bool(pic.get('loadTransparent')) tpx = pic.get('transparentPixel') if not src: return if lt is None and elem_lt is not None: lt = bool(elem_lt) src_str = str(src) lines.append(f'{indent}') if src_str.startswith('abs:'): lines.append(f'{indent}\t{esc_xml(src_str[4:])}') else: lines.append(f'{indent}\t{esc_xml(src_str)}') lines.append(f'{indent}\t{"false" if lt is False else "true"}') if tpx: lines.append(f'{indent}\t') lines.append(f'{indent}') # --- Оформление элемента: цвета / шрифты / граница (зеркало form-compile.ps1 Emit-Appearance) --- # Прямые свойства элемента (// + header/footer у полей). Ключи англ. # camelCase 1:1 с тегами + приём рус. синонимов. Цвет — verbatim-строка (style:/web:/win:/#RRGGBB); # шрифт — строка-ref/объект-атрибуты; граница — строка-ref/ # объект {width,style}. Порядок тегов — XSD (профиль по базовому типу). APPEARANCE_SPEC = { 'titleTextColor': ('TitleTextColor', 'color'), 'titleBackColor': ('TitleBackColor', 'color'), 'titleFont': ('TitleFont', 'font'), 'footerTextColor': ('FooterTextColor', 'color'), 'footerBackColor': ('FooterBackColor', 'color'), 'footerFont': ('FooterFont', 'font'), 'textColor': ('TextColor', 'color'), 'backColor': ('BackColor', 'color'), 'borderColor': ('BorderColor', 'color'), 'border': ('Border', 'border'), 'font': ('Font', 'font'), } APPEARANCE_SYNONYMS = { 'цветтекста': 'textColor', 'цветфона': 'backColor', 'цветрамки': 'borderColor', 'цветтекстазаголовка': 'titleTextColor', 'цветфоназаголовка': 'titleBackColor', 'шрифтзаголовка': 'titleFont', 'цветтекстаподвала': 'footerTextColor', 'цветфонаподвала': 'footerBackColor', 'шрифтподвала': 'footerFont', 'шрифт': 'font', 'рамка': 'border', } # Синонимы ключей-свойств: русские имена свойств 1С (как в Конфигураторе) → канон. англ. ключ. # Ключи нормализованы (lowercase, без пробелов); сопоставление в emit_element тоже. Англ. ключ # работает всегда (доп. слой прощающего ввода). Видимость/Доступность НЕ включаем (hidden/disabled инвертирован). PROP_SYNONYMS = { 'пометка': 'checked', 'кнопкавыбора': 'choiceButton', 'кнопкаочистки': 'clearButton', 'кнопкарегулирования': 'spinButton', 'кнопкавыпадающегосписка': 'dropListButton', 'кнопкасписковоговыбора': 'choiceListButton', 'кнопкаоткрытия': 'openButton', 'кнопкапоумолчанию': 'defaultButton', 'быстрыйвыбор': 'quickChoice', 'формавыбора': 'choiceForm', 'историявыборапривводе': 'choiceHistoryOnInput', 'выборгруппиэлементов': 'choiceFoldersAndItems', 'фиксациявтаблице': 'fixingInTable', 'путькданнымподвала': 'footerDataPath', 'автоотметканезаполненного': 'markIncomplete', 'многострочныйрежим': 'multiLine', 'режимпароля': 'passwordMode', 'переноспословам': 'wrap', 'расположениезаголовка': 'titleLocation', 'пропускатьпривводе': 'skipOnInput', 'заголовок': 'title', 'ширина': 'width', 'высота': 'height', 'подсказкаввода': 'inputHint', } APP_ORDER_FIELD =['titleTextColor', 'titleBackColor', 'titleFont', 'footerTextColor', 'footerBackColor', 'footerFont', 'textColor', 'backColor', 'borderColor', 'border', 'font'] APP_ORDER_DECORATION = ['textColor', 'font', 'backColor', 'borderColor', 'border'] APP_ORDER_BUTTON = ['textColor', 'backColor', 'borderColor', 'font'] def get_appearance_value(el, canonical): if not isinstance(el, dict): return None if canonical in el: return el[canonical] lowmap = {k.lower(): k for k in el.keys()} if canonical.lower() in lowmap: return el[lowmap[canonical.lower()]] for syn, canon in APPEARANCE_SYNONYMS.items(): if canon == canonical and syn in lowmap: return el[lowmap[syn]] return None def emit_font_tag(lines, tag, val, indent): if isinstance(val, str): lines.append(f'{indent}<{tag} ref="{esc_xml(val)}" kind="StyleItem"/>') return attrs = [] for a in ('ref', 'faceName', 'height', 'bold', 'italic', 'underline', 'strikeout', 'kind', 'scale'): if a in val and val[a] is not None: v = val[a] if isinstance(v, bool): v = 'true' if v else 'false' attrs.append(f'{a}="{esc_xml(str(v))}"') lines.append(f'{indent}<{tag} {" ".join(attrs)}/>') def emit_border_tag(lines, val, indent): if isinstance(val, str): lines.append(f'{indent}') return if val.get('ref'): lines.append(f'{indent}') return width = val['width'] if val.get('width') is not None else 1 style = str(val['style']) if 'style' in val else None lines.append(f'{indent}') if style: lines.append(f'{indent}\t{esc_xml(style)}') lines.append(f'{indent}') # ───────────────────────────────────────────────────────────────────────────── # Planner design-time — зеркало Emit-PlannerSettings (ps1). PLANNER_NS = 'http://v8.1c.ru/8.3/data/planner' CHART_NS = 'http://v8.1c.ru/8.2/data/chart' def _pl_get(o, k, default=None): if isinstance(o, dict) and o.get(k) is not None: return o[k] return default def _pl_bool(v): if isinstance(v, bool): return 'true' if v else 'false' if str(v) == 'True': return 'true' if str(v) == 'False': return 'false' return str(v) def emit_planner_color(lines, tag, o, key, ind): lines.append(f'{ind}{esc_xml(str(_pl_get(o, key, "auto")))}') def emit_planner_text(lines, tag, v, ind): if v is None or str(v) == '': lines.append(f'{ind}') else: lines.append(f'{ind}{esc_xml(str(v))}') _PLANNER_REF_RE = re.compile( r'^(Enum|Catalog|Document|ChartOfAccounts|ChartOfCalculationTypes|ChartOfCharacteristicTypes|ExchangePlan|BusinessProcess|Task)\.' r'|\.EnumValue\.|EmptyRef$' r'|^(Перечисление|Справочник|Документ|ПланСчетов|ПланВидовХарактеристик|ПланВидовРасчета|ПланОбмена|БизнесПроцесс|Задача)\.') def test_planner_ref(v): return bool(_PLANNER_REF_RE.search(str(v))) def emit_planner_value(lines, v, ind): if v is None or str(v) == '': lines.append(f'{ind}') return t = 'xr:DesignTimeRef' if test_planner_ref(v) else 'xs:string' lines.append(f'{ind}{esc_xml(str(v))}') def emit_planner_font(lines, o, ind): f = _pl_get(o, 'font') if f is None: lines.append(f'{ind}') return emit_font_tag(lines, 'pl:font', f, ind) def emit_planner_border(lines, o, ind, key='border'): b = _pl_get(o, key) bw = _pl_get(b, 'width', 1) if b else 1 bs = _pl_get(b, 'style', 'Single') if b else 'Single' lines.append(f'{ind}') lines.append(f'{ind}\t{esc_xml(str(bs))}') lines.append(f'{ind}') def emit_planner_level(lines, lv, cns, ind): li = f'{ind}\t' lines.append(f'{ind}') lines.append(f'{li}{esc_xml(str(_pl_get(lv, "measure", "Hour")))}') lines.append(f'{li}{_pl_get(lv, "interval", 1)}') lines.append(f'{li}{_pl_bool(_pl_get(lv, "show", True))}') line = _pl_get(lv, 'line') lw = _pl_get(line, 'width', 1) if line else 1 lg = _pl_get(line, 'gap', False) if line else False lst = _pl_get(line, 'style', 'Solid') if line else 'Solid' lines.append(f'{li}') lines.append(f'{li}\t{esc_xml(str(lst))}') lines.append(f'{li}') lines.append(f'{li}{esc_xml(str(_pl_get(lv, "scaleColor", "auto")))}') lines.append(f'{li}{esc_xml(str(_pl_get(lv, "dayFormatRule", "MonthDayWeekDay")))}') fmt = _pl_get(lv, 'format') if fmt is None: fmt = {'#': 'DF="HH:mm"', 'ru': 'DF="HH:mm"'} lines.append(f'{li}') emit_ml_items(lines, f'{li}\t', fmt) lines.append(f'{li}') labels = _pl_get(lv, 'labels') ticks = _pl_get(labels, 'ticks', 0) if labels else 0 lines.append(f'{li}') lines.append(f'{li}\t{ticks}') lines.append(f'{li}') lines.append(f'{li}{esc_xml(str(_pl_get(lv, "backColor", "auto")))}') lines.append(f'{li}{esc_xml(str(_pl_get(lv, "textColor", "auto")))}') lines.append(f'{li}{_pl_bool(_pl_get(lv, "showPereodicalLabels", True))}') lines.append(f'{ind}') def emit_planner_timescale(lines, ts, ind): cns = CHART_NS ci = f'{ind}\t' lines.append(f'{ind}') placement = _pl_get(ts, 'placement', 'Left') if ts else 'Left' lines.append(f'{ci}{esc_xml(str(placement))}') levels = _pl_get(ts, 'levels', []) if ts else [] if not levels: levels = [None] for lv in levels: emit_planner_level(lines, lv, cns, ci) transp = _pl_get(ts, 'transparent', False) if ts else False lines.append(f'{ci}{_pl_bool(transp)}') tbc = _pl_get(ts, 'backColor', 'auto') if ts else 'auto' ttc = _pl_get(ts, 'textColor', 'auto') if ts else 'auto' tcl = _pl_get(ts, 'currentLevel', 0) if ts else 0 lines.append(f'{ci}{esc_xml(str(tbc))}') lines.append(f'{ci}{esc_xml(str(ttc))}') lines.append(f'{ci}{tcl}') lines.append(f'{ind}') def emit_planner_item(lines, it, ind): lines.append(f'{ind}') ii = f'{ind}\t' emit_planner_value(lines, _pl_get(it, 'value'), ii) emit_planner_text(lines, 'text', _pl_get(it, 'text', ''), ii) emit_planner_text(lines, 'tooltip', _pl_get(it, 'tooltip', ''), ii) lines.append(f'{ii}{_pl_get(it, "begin", "0001-01-01T00:00:00")}') lines.append(f'{ii}{_pl_get(it, "end", "0001-01-01T00:00:00")}') emit_planner_color(lines, 'borderColor', it, 'borderColor', ii) emit_planner_color(lines, 'backColor', it, 'backColor', ii) emit_planner_color(lines, 'textColor', it, 'textColor', ii) emit_planner_font(lines, it, ii) lines.append(f'{ii}') lines.append(f'{ii}{_pl_get(it, "replacementDate", "0001-01-01T00:00:00")}') lines.append(f'{ii}{_pl_bool(_pl_get(it, "deleted", False))}') iid = _pl_get(it, 'id') if iid is None: import uuid iid = str(uuid.uuid4()) lines.append(f'{ii}{iid}') lines.append(f'{ii}{_pl_bool(_pl_get(it, "textFormatted", False))}') emit_planner_border(lines, it, ii, 'border') lines.append(f'{ii}{esc_xml(str(_pl_get(it, "editMode", "EnableEdit")))}') lines.append(f'{ind}') def emit_planner_dim_element(lines, el, ind): lines.append(f'{ind}') ii = f'{ind}\t' emit_planner_value(lines, _pl_get(el, 'value'), ii) emit_planner_text(lines, 'text', _pl_get(el, 'text', ''), ii) emit_planner_color(lines, 'borderColor', el, 'borderColor', ii) emit_planner_color(lines, 'backColor', el, 'backColor', ii) emit_planner_color(lines, 'textColor', el, 'textColor', ii) emit_planner_font(lines, el, ii) for sub in _pl_get(el, 'elements', []): emit_planner_dim_element(lines, sub, ii) lines.append(f'{ii}{_pl_bool(_pl_get(el, "showOnlySubordinatesAreas", True))}') lines.append(f'{ii}{_pl_bool(_pl_get(el, "textFormatted", False))}') lines.append(f'{ind}') def emit_planner_dimension(lines, d, ind): lines.append(f'{ind}') di = f'{ind}\t' emit_planner_value(lines, _pl_get(d, 'value'), di) emit_planner_text(lines, 'text', _pl_get(d, 'text', ''), di) emit_planner_color(lines, 'borderColor', d, 'borderColor', di) emit_planner_color(lines, 'backColor', d, 'backColor', di) emit_planner_color(lines, 'textColor', d, 'textColor', di) emit_planner_font(lines, d, di) for el in _pl_get(d, 'elements', []): emit_planner_dim_element(lines, el, di) lines.append(f'{di}{_pl_bool(_pl_get(d, "textFormatted", False))}') lines.append(f'{ind}') def emit_planner_settings(lines, pl, ind): lines.append(f'{ind}') si = f'{ind}\t' for it in _pl_get(pl, 'items', []): emit_planner_item(lines, it, si) for d in _pl_get(pl, 'dimensions', []): emit_planner_dimension(lines, d, si) emit_planner_color(lines, 'borderColor', pl, 'borderColor', si) emit_planner_color(lines, 'backColor', pl, 'backColor', si) emit_planner_color(lines, 'textColor', pl, 'textColor', si) emit_planner_color(lines, 'lineColor', pl, 'lineColor', si) emit_planner_font(lines, pl, si) lines.append(f'{si}{_pl_get(pl, "beginOfRepresentationPeriod", "0001-01-01T00:00:00")}') lines.append(f'{si}{_pl_get(pl, "endOfRepresentationPeriod", "0001-01-01T00:00:00")}') lines.append(f'{si}{_pl_bool(_pl_get(pl, "alignElementsOfTimeScale", True))}') lines.append(f'{si}{_pl_bool(_pl_get(pl, "displayTimeScaleWrapHeaders", True))}') lines.append(f'{si}{_pl_bool(_pl_get(pl, "displayWrapHeaders", True))}') wfmt = _pl_get(pl, 'timeScaleWrapHeadersFormat') if wfmt is None: wfmt = {'#': 'DLF="DD"', 'ru': 'DLF="DD"'} emit_mltext(lines, si, 'pl:timeScaleWrapHeadersFormat', wfmt) lines.append(f'{si}{esc_xml(str(_pl_get(pl, "periodicVariantUnit", "Day")))}') lines.append(f'{si}{_pl_get(pl, "periodicVariantRepetition", 1)}') lines.append(f'{si}{_pl_get(pl, "timeScaleWrapBeginIndent", 0)}') lines.append(f'{si}{_pl_get(pl, "timeScaleWrapEndIndent", 0)}') emit_planner_timescale(lines, _pl_get(pl, 'timeScale'), si) period = _pl_get(pl, 'period') if period: lines.append(f'{si}') lines.append(f'{si}\t{_pl_get(period, "begin", "0001-01-01T00:00:00")}') lines.append(f'{si}\t{_pl_get(period, "end", "0001-01-01T00:00:00")}') lines.append(f'{si}') lines.append(f'{si}{_pl_bool(_pl_get(pl, "displayCurrentDate", True))}') lines.append(f'{si}{esc_xml(str(_pl_get(pl, "itemsTimeRepresentation", "BeginTime")))}') lines.append(f'{si}{esc_xml(str(_pl_get(pl, "itemsBehaviorWhenSpaceInsufficient", "CollapseItems")))}') lines.append(f'{si}{_pl_bool(_pl_get(pl, "autoMinColumnWidth", True))}') lines.append(f'{si}{_pl_bool(_pl_get(pl, "autoMinRowHeight", True))}') lines.append(f'{si}{_pl_get(pl, "minColumnWidth", 0)}') lines.append(f'{si}{_pl_get(pl, "minRowHeight", 0)}') lines.append(f'{si}{esc_xml(str(_pl_get(pl, "fixDimensionsHeader", "auto")))}') lines.append(f'{si}{esc_xml(str(_pl_get(pl, "fixTimeScaleHeader", "auto")))}') emit_planner_border(lines, pl, si, 'border') lines.append(f'{si}{esc_xml(str(_pl_get(pl, "newItemsTextType", "String")))}') lines.append(f'{ind}') # ───────────────────────────────────────────────────────────────────────────── # Chart design-time — генерик-эмиттер (зеркало # Build-ChartNode декомпилятора + Emit-ChartNode ps1). CHART_ML_FIELDS = {'title', 'lbFormat', 'lbpFormat', 'vsFormat', 'dtFormat', 'dataSourceDescription', 'labelFormat', 'text'} CHART_ATTR_FIELDS = {'gaugeQualityBands'} CHART_FONT_KEYS = ('ref', 'faceName', 'height', 'bold', 'italic', 'underline', 'strikeout', 'kind', 'scale') def emit_chart_node(lines, name, val, ind): if name in CHART_ML_FIELDS: if val is None or str(val) == '': lines.append(f'{ind}') return lines.append(f'{ind}') emit_ml_items(lines, f'{ind}\t', val) lines.append(f'{ind}') return if isinstance(val, list): for e in val: emit_chart_node(lines, name, e, ind) return if isinstance(val, dict): keys = list(val.keys()) if name in CHART_ATTR_FIELDS: attrs = ' '.join(f'{k}="{esc_xml(_pl_bool(val[k]) if isinstance(val[k], bool) else str(val[k]))}"' for k in keys) lines.append(f'{ind}') return if 'gap' in val: lines.append(f'{ind}') lines.append(f'{ind}\t{esc_xml(str(val.get("style")))}') lines.append(f'{ind}') return if 'style' in val and 'width' in val: lines.append(f'{ind}') lines.append(f'{ind}\t{esc_xml(str(val.get("style")))}') lines.append(f'{ind}') return if any(fk in val for fk in CHART_FONT_KEYS): attrs = ' '.join(f'{fk}="{esc_xml(_pl_bool(val[fk]) if isinstance(val[fk], bool) else str(val[fk]))}"' for fk in CHART_FONT_KEYS if fk in val) lines.append(f'{ind}') return if not keys: lines.append(f'{ind}') return lines.append(f'{ind}') for k in keys: emit_chart_node(lines, k, val[k], f'{ind}\t') lines.append(f'{ind}') return if val is None or str(val) == '': lines.append(f'{ind}') return if isinstance(val, bool): lines.append(f'{ind}{_pl_bool(val)}') return lines.append(f'{ind}{esc_xml(str(val))}') def emit_chart_settings(lines, chart, ind, ctype='d4p1:Chart'): lines.append(f'{ind}') for k in list(chart.keys()): emit_chart_node(lines, k, chart[k], f'{ind}\t') lines.append(f'{ind}') def emit_appearance(lines, el, indent, profile='field'): if not isinstance(el, dict): return order = {'decoration': APP_ORDER_DECORATION, 'button': APP_ORDER_BUTTON}.get(profile, APP_ORDER_FIELD) for key in order: val = get_appearance_value(el, key) if val is None or (isinstance(val, str) and val == ''): continue tag, kind = APPEARANCE_SPEC[key] if kind == 'color': lines.append(f'{indent}<{tag}>{esc_xml(str(val))}') elif kind == 'font': emit_font_tag(lines, tag, val, indent) else: emit_border_tag(lines, val, indent) # Простые скаляры элемента (pass-through, зеркало $script:genericScalars). kind bool/value. GENERIC_SCALARS = [ ('VerticalAlign', 'verticalAlign', 'value'), ('ThroughAlign', 'throughAlign', 'value'), ('EnableContentChange', 'enableContentChange', 'bool'), ('PictureSize', 'pictureSize', 'value'), ('TitleHeight', 'titleHeight', 'value'), ('ChildItemsWidth', 'childItemsWidth', 'value'), ('ShowLeftMargin', 'showLeftMargin', 'bool'), ('CellHyperlink', 'cellHyperlink', 'bool'), ('ViewMode', 'viewMode', 'value'), ('VerticalScrollBar', 'verticalScrollBar', 'value'), ('RowInputMode', 'rowInputMode', 'value'), ('Mask', 'mask', 'value'), ('CreateButton', 'createButton', 'bool'), ('FixingInTable', 'fixingInTable', 'value'), ('VerticalSpacing', 'verticalSpacing', 'value'), # Spec-fields (document/gauge) - type-specific enum/bool scalars pass-through ('HorizontalScrollBar', 'horizontalScrollBar', 'value'), ('ViewScalingMode', 'viewScalingMode', 'value'), ('Output', 'output', 'value'), ('SelectionShowMode', 'selectionShowMode', 'value'), ('PointerType', 'pointerType', 'value'), ('DrawingSelectionShowMode', 'drawingSelectionShowMode', 'value'), ('WarningOnEditRepresentation', 'warningOnEditRepresentation', 'value'), ('MarkingAppearance', 'markingAppearance', 'value'), ('Protection', 'protection', 'bool'), ('Edit', 'edit', 'bool'), ('ShowGrid', 'showGrid', 'bool'), ('ShowGroups', 'showGroups', 'bool'), ('ShowHeaders', 'showHeaders', 'bool'), ('ShowRowAndColumnNames', 'showRowAndColumnNames', 'bool'), ('ShowCellNames', 'showCellNames', 'bool'), ('ShowPercent', 'showPercent', 'bool'), # Report-form контекст: интервал группы / представление кнопки в контекстном меню / детальное представление настройки таблицы ('HorizontalSpacing', 'horizontalSpacing', 'value'), ('RepresentationInContextMenu', 'representationInContextMenu', 'value'), ('SettingsNamedItemDetailedRepresentation', 'settingsNamedItemDetailedRepresentation', 'bool'), # Хвост: высота элемента списка (radio) / ширина выпадающего списка (input) ('ItemHeight', 'itemHeight', 'value'), ('DropListWidth', 'dropListWidth', 'value'), # Хвост CI-форм: динамический заголовок (Page/Group) / расширенное ред. (input) / высота таблицы по строкам ('TitleDataPath', 'titleDataPath', 'value'), ('ExtendedEdit', 'extendedEdit', 'bool'), ('MaxRowsCount', 'maxRowsCount', 'value'), ('AutoMaxRowsCount', 'autoMaxRowsCount', 'bool'), ('HeightControlVariant', 'heightControlVariant', 'value'), ('EditTextUpdate', 'editTextUpdate', 'value'), # Корпусный хвост: свёртка группы / форма попапа / авто-добавление / выделение отрицательных / # нач. позиция списка / высота списка выбора / три состояния / прокрутка страницы при сжатии ('ControlRepresentation', 'controlRepresentation', 'value'), ('ShapeRepresentation', 'shapeRepresentation', 'value'), ('AutoAddIncomplete', 'autoAddIncomplete', 'bool'), ('MarkNegatives', 'markNegatives', 'bool'), ('InitialListView', 'initialListView', 'value'), ('ChoiceListHeight', 'choiceListHeight', 'value'), ('ThreeState', 'threeState', 'bool'), ('ScrollOnCompress', 'scrollOnCompress', 'bool'), # Сочетание клавиш — общее свойство (команда — отдельный путь) ('Shortcut', 'shortcut', 'value'), # Батч простых скаляров (input/radio/group/picDecoration/button; Table-специфичные — отдельно) ('IncompleteChoiceMode', 'incompleteChoiceMode', 'value'), ('EqualColumnsWidth', 'equalColumnsWidth', 'bool'), ('ChildrenAlign', 'childrenAlign', 'value'), ('ImageScale', 'imageScale', 'value'), ('Zoomable', 'zoomable', 'bool'), ('Shape', 'shape', 'value'), ('PictureLocation', 'pictureLocation', 'value'), # Равная ширина элементов (check/radio) / высота заголовка пункта (radio) ('EqualItemsWidth', 'equalItemsWidth', 'bool'), ('ItemTitleHeight', 'itemTitleHeight', 'value'), # Спец-режим ввода текста (input, моб.: Email/PhoneNumber/...) — листовой enum-скаляр ('SpecialTextInputMode', 'specialTextInputMode', 'value'), # Ширина пункта (radio/check) / выбор нескольких значений из выпадающего (input) ('ItemWidth', 'itemWidth', 'value'), ('ShowCheckBoxesInDropList', 'showCheckBoxesInDropList', 'bool'), ('MultipleValueDataPath', 'multipleValueDataPath', 'value'), ('MultipleValuePresentDataPath', 'multipleValuePresentDataPath', 'value'), # Режим авто-показа кнопок открытия/очистки (input, enum) ('AutoShowOpenButtonMode', 'autoShowOpenButtonMode', 'value'), ('AutoShowClearButtonMode', 'autoShowClearButtonMode', 'value'), # Оформление/картинка множественного выбора (input, редко; цвета — текст-контент) ('MultipleValuesTextColor', 'multipleValuesTextColor', 'value'), ('MultipleValuesBackColor', 'multipleValuesBackColor', 'value'), ('MultipleValuePictureShape', 'multipleValuePictureShape', 'value'), ('MultipleValuePictureDataPath', 'multipleValuePictureDataPath', 'value'), # Хвост листовых скаляров (по 1): автокоррекция / уникальность команды / пустое множ.значение / гориз.сжатие ('AutoCorrectionOnTextInput', 'autoCorrectionOnTextInput', 'value'), ('SpellCheckingOnTextInput', 'spellCheckingOnTextInput', 'value'), ('CommandUniqueness', 'commandUniqueness', 'bool'), ('AllowInputEmptyMultipleValues', 'allowInputEmptyMultipleValues', 'bool'), ('BehaviorOnHorizontalCompression', 'behaviorOnHorizontalCompression', 'value'), ] def emit_generic_scalars(lines, el, indent): for tag, key, kind in GENERIC_SCALARS: if key not in el or el[key] is None: continue if kind == 'bool': lines.append(f'{indent}<{tag}>{"true" if el[key] else "false"}') else: v = str(el[key]) if v == '': continue lines.append(f'{indent}<{tag}>{esc_xml(v)}') def emit_layout(lines, el, indent, skip_height=False, multi_line_default=False): # Общие layout-свойства — применимы ко всем элементам. Порядок согласован # с историческим выводом input/label, чтобы не сдвигать существующие снапшоты. # skip_height: подавить (зарезервирован; Table теперь эмитит generic-ом + свой ). # multi_line_default: input без явного autoMaxWidth при multiLine → AutoMaxWidth=false. # CommandSet (отключённые команды редактора) — общее свойство поля; в схеме рано (после TitleLocation). if el.get('excludedCommands') and len(el['excludedCommands']) > 0: lines.append(f'{indent}') for cmd in el['excludedCommands']: lines.append(f'{indent}\t{cmd}') lines.append(f'{indent}') emit_common_element_props(lines, el, indent) if 'autoMaxWidth' in el: if el.get('autoMaxWidth') is False: lines.append(f"{indent}false") elif multi_line_default: lines.append(f"{indent}false") if el.get('maxWidth') is not None: lines.append(f"{indent}{el['maxWidth']}") if el.get('autoMaxHeight') is False: lines.append(f"{indent}false") if el.get('maxHeight') is not None: lines.append(f"{indent}{el['maxHeight']}") if el.get('width'): lines.append(f"{indent}{el['width']}") if not skip_height and el.get('height'): lines.append(f"{indent}{el['height']}") if el.get('horizontalStretch') is not None: lines.append(f'{indent}{"true" if el["horizontalStretch"] else "false"}') if el.get('verticalStretch') is not None: lines.append(f'{indent}{"true" if el["verticalStretch"] else "false"}') if el.get('groupHorizontalAlign'): lines.append(f"{indent}{el['groupHorizontalAlign']}") if el.get('groupVerticalAlign'): lines.append(f"{indent}{el['groupVerticalAlign']}") if el.get('horizontalAlign'): lines.append(f"{indent}{el['horizontalAlign']}") emit_generic_scalars(lines, el, indent) def title_from_name(name): """СуммаДокумента → 'Сумма документа'. НДСВключен → 'НДС включен'.""" if not name: return '' s = re.sub(r'([А-ЯA-Z])([А-ЯA-Z][а-яa-z])', r'\1 \2', name) s = re.sub(r'([а-яa-z0-9])([А-ЯA-Z])', r'\1 \2', s) parts = s.split(' ') if not parts: return s out = [parts[0]] for p in parts[1:]: out.append(p if (len(p) > 1 and p.isupper()) else p.lower()) return ' '.join(out) def emit_title(lines, el, name, indent, auto=False): # Нет ключа title → авто-вывод из имени (помощь модели). # Явный title "" (или None) → подавить. Явный непустой → как есть. if 'title' in el: if el.get('title'): emit_mltext(lines, indent, 'Title', el['title']) elif auto and name: emit_mltext(lines, indent, 'Title', title_from_name(name)) # ToolTip элемента (всплывающая подсказка) — по схеме сразу после Title. if el.get('tooltip'): emit_mltext(lines, indent, 'ToolTip', el['tooltip']) # ToolTipRepresentation — режим показа подсказки (None/Button/ShowBottom/…), после ToolTip. if el.get('tooltipRepresentation'): lines.append(f'{indent}{el["tooltipRepresentation"]}') _TITLE_LOC_MAP = {'none': 'None', 'left': 'Left', 'right': 'Right', 'top': 'Top', 'bottom': 'Bottom', 'auto': 'Auto'} def map_title_loc(v): return _TITLE_LOC_MAP.get(str(v).lower(), str(v)) def emit_title_location(lines, el, indent, smart_default): # Нет ключа → умный дефолт (Right/None), эмитится. "" → подавить (дефолт платформы). # Значение → эмитить с маппингом регистра. if 'titleLocation' in el: if el.get('titleLocation'): lines.append(f"{indent}{map_title_loc(el['titleLocation'])}") elif smart_default: lines.append(f"{indent}{smart_default}") # --- Type emitter --- V8_TYPES = { "ValueTable": "v8:ValueTable", "ValueTree": "v8:ValueTree", "ValueList": "v8:ValueListType", "TypeDescription": "v8:TypeDescription", "Universal": "v8:Universal", "FixedArray": "v8:FixedArray", "FixedStructure": "v8:FixedStructure", } UI_TYPES = { "FormattedString": "v8ui:FormattedString", "Picture": "v8ui:Picture", "Color": "v8ui:Color", "Font": "v8ui:Font", } DCS_MAP = { "DataCompositionSettings": "dcsset:DataCompositionSettings", "DataCompositionSchema": "dcssch:DataCompositionSchema", "DataCompositionComparisonType": "dcscor:DataCompositionComparisonType", } CFG_REF_PATTERN = re.compile( r'^(CatalogRef|CatalogObject|DocumentRef|DocumentObject|EnumRef|' r'ChartOfAccountsRef|ChartOfAccountsObject|ChartOfCharacteristicTypesRef|ChartOfCharacteristicTypesObject|' r'ChartOfCalculationTypesRef|ChartOfCalculationTypesObject|' r'ExchangePlanRef|ExchangePlanObject|BusinessProcessRef|BusinessProcessObject|TaskRef|TaskObject|' r'InformationRegisterRecordSet|InformationRegisterRecordManager|' r'AccumulationRegisterRecordSet|AccountingRegisterRecordSet|' r'ConstantsSet|DataProcessorObject|ReportObject)\.' ) KNOWN_INVALID_TYPES = { 'FormDataStructure': 'Runtime type. Use object type without cfg: prefix (e.g. CatalogObject.Контрагенты, DocumentObject.Приход)', 'FormDataCollection': 'Runtime type. Use ValueTable', 'FormDataTree': 'Runtime type. Use ValueTree', 'FormDataTreeItem': 'Runtime type, not valid in XML', 'FormDataCollectionItem': 'Runtime type, not valid in XML', 'FormGroup': 'UI element type, not a data type', 'FormField': 'UI element type, not a data type', 'FormButton': 'UI element type, not a data type', 'FormDecoration': 'UI element type, not a data type', 'FormTable': 'UI element type, not a data type', } _FORM_TYPE_SYNONYMS = { "строка": "string", "число": "decimal", "булево": "boolean", "дата": "date", "датавремя": "dateTime", "number": "decimal", "bool": "boolean", "справочникссылка": "CatalogRef", "справочникобъект": "CatalogObject", "документссылка": "DocumentRef", "документобъект": "DocumentObject", "перечислениессылка": "EnumRef", "плансчетовссылка": "ChartOfAccountsRef", "планвидовхарактеристикссылка": "ChartOfCharacteristicTypesRef", "планвидоврасчётассылка": "ChartOfCalculationTypesRef", "планвидоврасчетассылка": "ChartOfCalculationTypesRef", "планобменассылка": "ExchangePlanRef", "бизнеспроцессссылка": "BusinessProcessRef", "задачассылка": "TaskRef", "определяемыйтип": "DefinedType", "характеристика": "Characteristic", "любаяссылка": "AnyRef", "любаяссылкаиб": "AnyIBRef", # Платформенные v8-типы (forgiving: англ. без префикса + рус.) → каноничный с префиксом v8: "standardperiod": "v8:StandardPeriod", "стандартныйпериод": "v8:StandardPeriod", "standardbeginningdate": "v8:StandardBeginningDate", "стандартнаядатаначала": "v8:StandardBeginningDate", "uuid": "v8:UUID", "уникальныйидентификатор": "v8:UUID", "списокзначений": "ValueList", } def resolve_type_str(type_str): if not type_str: return type_str # Lenient: strip leading cfg: prefix if user passed it (canonical form is without prefix) if type_str.startswith('cfg:'): type_str = type_str[4:] m = re.match(r'^([^(]+)\((.+)\)$', type_str) if m: base, params = m.group(1).strip(), m.group(2) r = _FORM_TYPE_SYNONYMS.get(base.lower()) return f"{r}({params})" if r else type_str if '.' in type_str: i = type_str.index('.') prefix, suffix = type_str[:i], type_str[i:] r = _FORM_TYPE_SYNONYMS.get(prefix.lower()) return f"{r}{suffix}" if r else type_str r = _FORM_TYPE_SYNONYMS.get(type_str.lower()) return r if r else type_str def emit_single_type(lines, type_str, indent): type_str = resolve_type_str(type_str) # TypeId — тип, заданный глобальным стабильным GUID (, не ). Платформа так # сериализует типы, чьё имя в этом контексте недоступно (определяемые/характеристики). GUID # глобально стабилен → эмитим verbatim (как роль-по-GUID). Маркер декомпилятора: 'typeid:GUID'. m = re.match(r'^typeid:([0-9a-fA-F-]{36})$', type_str) if m: lines.append(f'{indent}{m.group(1)}') return # boolean if type_str == 'boolean': lines.append(f'{indent}xs:boolean') return # string or string(N) or string(N,fixed) (AllowedLength: Variable дефолт / Fixed) m = re.match(r'^string(\((\d+)(\s*,\s*(fixed|variable))?\))?$', type_str, re.IGNORECASE) if m: length = m.group(2) if m.group(2) else '0' al = 'Fixed' if (m.group(4) and m.group(4).lower() == 'fixed') else 'Variable' lines.append(f'{indent}xs:string') lines.append(f'{indent}') lines.append(f'{indent}\t{length}') lines.append(f'{indent}\t{al}') 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 / time m = re.match(r'^(date|dateTime|time)$', type_str) if m: fractions_map = {'date': 'Date', 'dateTime': 'DateTime', 'time': 'Time'} fractions = fractions_map[type_str] lines.append(f'{indent}xs:dateTime') lines.append(f'{indent}') lines.append(f'{indent}\t{fractions}') lines.append(f'{indent}') return # V8 types if type_str in V8_TYPES: lines.append(f'{indent}{V8_TYPES[type_str]}') return # UI types if type_str in UI_TYPES: lines.append(f'{indent}{UI_TYPES[type_str]}') return # DCS types if type_str.startswith('DataComposition'): if type_str in DCS_MAP: lines.append(f'{indent}{DCS_MAP[type_str]}') return # Голые конфигурационные типы (cfg: без .Имя): дин-список, набор констант, общий объект отчёта. # Корпус (acc+erp 8.3.24): DynamicList 5205, ConstantsSet 103, ReportObject 10. if type_str in ('DynamicList', 'ConstantsSet', 'ReportObject'): lines.append(f'{indent}cfg:{type_str}') return # TypeSet (набор типов) → : определяемый тип / характеристика (именованные) # + «любая ссылка вида» (голый ref-вид без .Имя). Развязка с обычным типом — по наличию точки. if re.match(r'^(DefinedType|Characteristic)\.', type_str): lines.append(f'{indent}cfg:{type_str}') return if re.match(r'^(AnyRef|AnyIBRef|CatalogRef|DocumentRef|EnumRef|ExchangePlanRef|TaskRef|BusinessProcessRef|ChartOfAccountsRef|ChartOfCharacteristicTypesRef|ChartOfCalculationTypesRef)$', type_str): lines.append(f'{indent}cfg:{type_str}') return # cfg: references if CFG_REF_PATTERN.match(type_str): lines.append(f'{indent}cfg:{type_str}') return # Спец-типы платформы с собственным namespace (объявляется ЛОКАЛЬНО на ). # Префикс d5p1 неоднозначен (5 разных URI), поэтому маппинг по полному значению типа. # К таким типам привязаны спец-поля: mxl→SpreadSheetDocumentField, fd→FormattedDocumentField, # d5p1:TextDocument→TextDocumentField, pdfdoc→PDF, pl→Planner, chart/geo/graphscheme/data-analysis. special_type_ns = { "mxl:SpreadsheetDocument": "http://v8.1c.ru/8.2/data/spreadsheet", "fd:FormattedDocument": "http://v8.1c.ru/8.2/data/formatted-document", "d5p1:TextDocument": "http://v8.1c.ru/8.1/data/txtedt", "d5p1:Chart": "http://v8.1c.ru/8.2/data/chart", "d5p1:GanttChart": "http://v8.1c.ru/8.2/data/chart", "d5p1:Dendrogram": "http://v8.1c.ru/8.2/data/chart", "d5p1:FlowchartContextType": "http://v8.1c.ru/8.2/data/graphscheme", "d5p1:DataAnalysisTimeIntervalUnitType": "http://v8.1c.ru/8.2/data/data-analysis", "d5p1:GeographicalSchema": "http://v8.1c.ru/8.2/data/geo", "pdfdoc:PDFDocument": "http://v8.1c.ru/8.3/data/pdf", "pl:Planner": "http://v8.1c.ru/8.3/data/planner", } if type_str in special_type_ns: pref = type_str.split(':', 1)[0] lines.append(f'{indent}{type_str}') return # Fallback with validation if type_str in KNOWN_INVALID_TYPES: raise ValueError(f"Invalid form attribute type '{type_str}': {KNOWN_INVALID_TYPES[type_str]}") # Платформенный тип с префиксом (v8:/v8ui:/xs:/dcs*:) — verbatim (напр. v8:UUID, v8:StandardPeriod). if re.match(r'^(v8|v8ui|xs|ent|style|sys|web|win|dcs\w*):', type_str): lines.append(f'{indent}{type_str}') elif '.' in type_str: lines.append(f'{indent}cfg:{type_str}') else: print(f"WARNING: Unrecognized bare type '{type_str}' — will be emitted without namespace prefix", file=sys.stderr) lines.append(f'{indent}{type_str}') def emit_type(lines, type_str, indent, tag="Type", tag_attrs=""): # tag/tag_attrs — обёртка (по умолчанию ); для valueType ValueList вызывается с # tag="Settings", tag_attrs=' xsi:type="v8:TypeDescription"'. if not type_str: lines.append(f'{indent}<{tag}{tag_attrs}/>') return type_string = str(type_str) parts = [p.strip() for p in re.split(r'[|+]', type_string)] lines.append(f'{indent}<{tag}{tag_attrs}>') for part in parts: emit_single_type(lines, part, f'{indent}\t') lines.append(f'{indent}') # --- Element emitters --- def emit_element(lines, el, indent, in_cmd_bar=False): # Companion-панели (объект/массив-значение) → commandBar/contextMenu, до тип-синонимов. normalize_panel_synonyms(el) # Silent synonyms: model often writes XML name or Russian (ПолеПереключателя/RadioButtonField → radio). # commandBar/autoCommandBar/КоманднаяПанель → тип-элемент ТОЛЬКО при строковом значении (имя). for src, dst in ELEMENT_TYPE_SYNONYMS.items(): if src in el and dst not in el: if src in STR_ONLY_TYPE_SYNONYMS and not isinstance(el[src], str): continue el[dst] = el.pop(src) # Синонимы ключей-свойств (русские имена 1С → канон. англ.). Case/space-insensitive. # Канон побеждает: если задан и русский, и англ. ключ — англ. остаётся, русский отбрасываем. for p_name in list(el.keys()): norm = p_name.replace(' ', '').lower() canon = PROP_SYNONYMS.get(norm) if canon and p_name != canon: val = el.pop(p_name) if canon not in el: el[canon] = val type_key = None for key in TYPE_KEYS: if el.get(key) is not None: type_key = key break if not type_key: print("WARNING: Unknown element type, skipping", file=sys.stderr) return # Validate known keys (внутренние маркеры на _ пропускаем). Оформление (цвета/шрифты/граница) # проверяем против самих структур appearance — канонические ключи + forgiving-синонимы, чтобы # allowlist не дрейфовал при добавлении новых. for p_name in el.keys(): if p_name.startswith('_'): continue if p_name not in KNOWN_KEYS and p_name not in APPEARANCE_SPEC and p_name not in APPEARANCE_SYNONYMS: print(f"WARNING: Element '{el.get(type_key, '')}': unknown key '{p_name}' -- ignored. Check SKILL.md for valid keys.", file=sys.stderr) name = get_element_name(el, type_key) _ensure_unique(name, _seen_element_names, 'element') eid = new_id() emitters = { 'group': emit_group, 'columnGroup': emit_column_group, 'buttonGroup': emit_button_group, 'input': emit_input, 'check': emit_check, 'radio': emit_radio_button_field, 'label': emit_label, 'labelField': emit_label_field, 'table': emit_table, 'pages': emit_pages, 'page': emit_page, 'button': emit_button, 'picture': emit_picture_decoration, 'picField': emit_picture_field, 'calendar': emit_calendar, 'cmdBar': emit_command_bar, 'popup': emit_popup, 'searchString': lambda lines, el, name, eid, indent: emit_addition(lines, el, name, eid, 'searchString', indent), 'viewStatus': lambda lines, el, name, eid, indent: emit_addition(lines, el, name, eid, 'viewStatus', indent), 'searchControl': lambda lines, el, name, eid, indent: emit_addition(lines, el, name, eid, 'searchControl', indent), 'spreadsheet': lambda lines, el, name, eid, indent: emit_simple_field(lines, el, name, eid, indent, 'SpreadSheetDocumentField', 'spreadsheet'), 'html': lambda lines, el, name, eid, indent: emit_simple_field(lines, el, name, eid, indent, 'HTMLDocumentField', 'html'), 'textDoc': lambda lines, el, name, eid, indent: emit_simple_field(lines, el, name, eid, indent, 'TextDocumentField', 'textDoc'), 'formattedDoc': lambda lines, el, name, eid, indent: emit_simple_field(lines, el, name, eid, indent, 'FormattedDocumentField', 'formattedDoc'), 'progressBar': lambda lines, el, name, eid, indent: emit_simple_field(lines, el, name, eid, indent, 'ProgressBarField', 'progressBar'), 'trackBar': lambda lines, el, name, eid, indent: emit_simple_field(lines, el, name, eid, indent, 'TrackBarField', 'trackBar'), 'chart': lambda lines, el, name, eid, indent: emit_simple_field(lines, el, name, eid, indent, 'ChartField', 'chart'), 'graphicalSchema': lambda lines, el, name, eid, indent: emit_simple_field(lines, el, name, eid, indent, 'GraphicalSchemaField', 'graphicalSchema'), 'planner': lambda lines, el, name, eid, indent: emit_simple_field(lines, el, name, eid, indent, 'PlannerField', 'planner'), 'periodField': lambda lines, el, name, eid, indent: emit_simple_field(lines, el, name, eid, indent, 'PeriodField', 'periodField'), 'dendrogram': lambda lines, el, name, eid, indent: emit_simple_field(lines, el, name, eid, indent, 'DendrogramField', 'dendrogram'), 'ganttChart': emit_gantt_chart, } emitter = emitters.get(type_key) if emitter: if type_key == 'button': emitter(lines, el, name, eid, indent, in_cmd_bar=in_cmd_bar) else: emitter(lines, el, name, eid, indent) def _warn_unrecognized(key, raw, valid, owner): # drop-on-miss enum: значение не распознано → тег не эмитится. Громко, чтобы автор увидел потерю. print(f"[WARN] Unrecognized {key} '{raw}' on '{owner}'. Valid values: {', '.join(valid)}. Value ignored.") def emit_group(lines, el, name, eid, indent): lines.append(f'{indent}') inner = f'{indent}\t' emit_title(lines, el, name, inner) # Group orientation # Group orientation (направление). Legacy: group:'collapsible' = Vertical + behavior collapsible. group_val = str(el.get('group', '')).lower() orientation_map = { 'horizontal': 'Horizontal', 'vertical': 'Vertical', 'alwayshorizontal': 'AlwaysHorizontal', 'alwaysvertical': 'AlwaysVertical', 'horizontalifpossible': 'HorizontalIfPossible', 'collapsible': 'Vertical', } orientation = orientation_map.get(group_val) if orientation: lines.append(f'{inner}{orientation}') elif group_val: _warn_unrecognized('group orientation', el.get('group'), ('vertical', 'horizontalIfPossible', 'alwaysHorizontal'), name) # Behavior: ключ behavior (usual/collapsible/popup) → ; отсутствие = Авто (не эмитим). behavior_val = str(el['behavior']).lower() if el.get('behavior') else ('collapsible' if group_val == 'collapsible' else None) bmap = {'usual': 'Usual', 'collapsible': 'Collapsible', 'popup': 'PopUp'} if behavior_val and behavior_val in bmap: lines.append(f'{inner}{bmap[behavior_val]}') elif el.get('behavior') and behavior_val not in bmap: _warn_unrecognized('behavior', el.get('behavior'), ('collapsible', 'popup'), name) # Collapsed — у Collapsible и PopUp (не привязано к одному behavior) if el.get('collapsed') is True: lines.append(f'{inner}true') # Representation if el.get('representation'): repr_map = { 'none': 'None', 'normal': 'NormalSeparation', 'weak': 'WeakSeparation', 'strong': 'StrongSeparation', } repr_val = repr_map.get(str(el['representation']), str(el['representation'])) lines.append(f'{inner}{repr_val}') # Использование текущей строки группы (после Representation, порядок XSD) if el.get('currentRowUse'): lines.append(f'{inner}{el["currentRowUse"]}') # ShowTitle if el.get('showTitle') is not None: lines.append(f'{inner}{"true" if el["showTitle"] else "false"}') # Заголовок свёрнутого представления (collapsible/popup) — мультиязычный текст if el.get('collapsedTitle'): emit_mltext(lines, inner, 'CollapsedRepresentationTitle', el['collapsedTitle']) # United if el.get('united') is False: lines.append(f'{inner}false') # Формат значения пути к данным заголовка (; парный к titleDataPath группы) if el.get('format'): emit_mltext(lines, inner, 'Format', el['format']) if el.get('editFormat'): emit_mltext(lines, inner, 'EditFormat', el['editFormat']) emit_common_flags(lines, el, inner) emit_layout(lines, el, inner) # Оформление (цвета/шрифты/граница) — перед компаньоном emit_appearance(lines, el, inner, 'field') # Companion: ExtendedTooltip emit_companion(lines, 'ExtendedTooltip', f'{name}\u0420\u0430\u0441\u0448\u0438\u0440\u0435\u043d\u043d\u0430\u044f\u041f\u043e\u0434\u0441\u043a\u0430\u0437\u043a\u0430', inner, el.get('extendedTooltip')) # Children if el.get('children') and len(el['children']) > 0: lines.append(f'{inner}') for child in el['children']: emit_element(lines, child, f'{inner}\t') lines.append(f'{inner}') lines.append(f'{indent}') def emit_column_group(lines, el, name, eid, indent): lines.append(f'{indent}') inner = f'{indent}\t' emit_title(lines, el, name, inner) group_val = str(el.get('columnGroup', '')).lower() orientation_map = { 'horizontal': 'Horizontal', 'vertical': 'Vertical', 'incell': 'InCell', } orientation = orientation_map.get(group_val) if orientation: lines.append(f'{inner}{orientation}') elif group_val: _warn_unrecognized('columnGroup orientation', el.get('columnGroup'), ('vertical', 'horizontal', 'inCell'), name) if el.get('showTitle') is not None: lines.append(f'{inner}{"true" if el["showTitle"] else "false"}') # showInHeader эмитится общим emit_common_element_props (через emit_layout) emit_common_flags(lines, el, inner) emit_layout(lines, el, inner) # Картинка заголовка колонки-группы (после ShowInHeader/Layout, перед оформлением — порядок XSD) emit_column_pics(lines, el, inner) # Оформление (цвета/шрифты/граница) — перед компаньоном emit_appearance(lines, el, inner, 'field') emit_companion(lines, 'ExtendedTooltip', f'{name}РасширеннаяПодсказка', inner, el.get('extendedTooltip')) if el.get('children') and len(el['children']) > 0: lines.append(f'{inner}') for child in el['children']: emit_element(lines, child, f'{inner}\t') lines.append(f'{inner}') lines.append(f'{indent}') def emit_input(lines, el, name, eid, indent): lines.append(f'{indent}') inner = f'{indent}\t' if el.get('path'): lines.append(f'{inner}{el["path"]}') emit_title(lines, el, name, inner, auto=not el.get('path')) emit_common_flags(lines, el, inner) if el.get('titleLocation'): loc_map = {'none': 'None', 'left': 'Left', 'right': 'Right', 'top': 'Top', 'bottom': 'Bottom'} loc = loc_map.get(str(el['titleLocation']), str(el['titleLocation'])) lines.append(f'{inner}{loc}') if el.get('multiLine') is not None: lines.append(f'{inner}{"true" if el["multiLine"] else "false"}') if el.get('passwordMode') is not None: lines.append(f'{inner}{"true" if el["passwordMode"] else "false"}') # ChoiceButton — захват «как есть» (платформа эмитит явное значение; ref-поля выводят сама, # декомпилятор фиксирует факт. значение). Нет ключа → не эмитим (не додумываем по событию). if el.get('choiceButton') is not None: lines.append(f'{inner}{"true" if el["choiceButton"] else "false"}') # Кнопки поля ввода — захват «как есть» (платформа эмитит явное значение, в т.ч. false) if el.get('clearButton') is not None: lines.append(f'{inner}{"true" if el["clearButton"] else "false"}') if el.get('spinButton') is not None: lines.append(f'{inner}{"true" if el["spinButton"] else "false"}') if el.get('dropListButton') is not None: lines.append(f'{inner}{"true" if el["dropListButton"] else "false"}') if el.get('choiceListButton') is not None: lines.append(f'{inner}{"true" if el["choiceListButton"] else "false"}') if el.get('markIncomplete') is not None: lines.append(f'{inner}{"true" if el["markIncomplete"] else "false"}') if el.get('editMode'): lines.append(f'{inner}{el["editMode"]}') emit_column_pics(lines, el, inner) if el.get('textEdit') is False: lines.append(f'{inner}false') # InputField-специфичные скаляры (захват «как есть»: платформа эмитит явное не-дефолтное значение) for key, tag in (('wrap', 'Wrap'), ('openButton', 'OpenButton'), ('listChoiceMode', 'ListChoiceMode'), ('extendedEditMultipleValues', 'ExtendedEditMultipleValues'), ('chooseType', 'ChooseType'), ('quickChoice', 'QuickChoice'), ('autoChoiceIncomplete', 'AutoChoiceIncomplete')): if el.get(key) is not None: lines.append(f'{inner}<{tag}>{"true" if el[key] else "false"}') # Ограничение доступных типов (поле на составном типе): домен типов + явный набор. # availableTypes — формат типа реквизита (§type); emit_type сам разбирает мультитип "a | b". if el.get('typeDomainEnabled') is not None: lines.append(f'{inner}{"true" if el["typeDomainEnabled"] else "false"}') if el.get('availableTypes'): emit_type(lines, el['availableTypes'], inner, tag='AvailableTypes') # InputField-специфичные value-скаляры for key, tag in (('choiceForm', 'ChoiceForm'), ('choiceHistoryOnInput', 'ChoiceHistoryOnInput'), ('choiceFoldersAndItems', 'ChoiceFoldersAndItems'), ('footerDataPath', 'FooterDataPath')): if el.get(key): lines.append(f'{inner}<{tag}>{esc_xml(str(el[key]))}') # MinValue/MaxValue — типизированное. JSON-число → xs:decimal, строка → xs:string (тип сохранён декомпилятором). for key, tag in (('minValue', 'MinValue'), ('maxValue', 'MaxValue')): if el.get(key) is not None: mvt = 'xs:string' if isinstance(el[key], str) else 'xs:decimal' lines.append(f'{inner}<{tag} xsi:type="{mvt}">{esc_xml(str(el[key]))}') if el.get('choiceButtonRepresentation'): lines.append(f'{inner}{el["choiceButtonRepresentation"]}') emit_picture_ref(lines, el.get('choiceButtonPicture'), 'ChoiceButtonPicture', inner) emit_layout(lines, el, inner, multi_line_default=(el.get('multiLine') is True)) if el.get('inputHint'): emit_mltext(lines, inner, 'InputHint', el['inputHint']) if el.get('warningOnEdit') is not None: emit_mltext(lines, inner, 'WarningOnEdit', el['warningOnEdit']) if el.get('footerText') is not None: emit_mltext(lines, inner, 'FooterText', el['footerText']) # Формат / формат редактирования (LocalStringType — строка или {ru,en}) if el.get('format'): emit_mltext(lines, inner, 'Format', el['format']) if el.get('editFormat'): emit_mltext(lines, inner, 'EditFormat', el['editFormat']) emit_choice_list(lines, el, inner) # Связи по типу / связи параметров выбора / параметры выбора emit_type_link(lines, el, inner) emit_choice_parameter_links(lines, el, inner) emit_choice_parameters(lines, el, inner) # Оформление (цвета/шрифты/граница) — перед компаньонами emit_appearance(lines, el, inner, 'field') # Companions emit_companion_panel(lines, 'ContextMenu', f'{name}\u041a\u043e\u043d\u0442\u0435\u043a\u0441\u0442\u043d\u043e\u0435\u041c\u0435\u043d\u044e', inner, el.get('contextMenu')) emit_companion(lines, 'ExtendedTooltip', f'{name}\u0420\u0430\u0441\u0448\u0438\u0440\u0435\u043d\u043d\u0430\u044f\u041f\u043e\u0434\u0441\u043a\u0430\u0437\u043a\u0430', inner, el.get('extendedTooltip')) emit_events(lines, el, name, inner, 'input') lines.append(f'{indent}') def emit_check(lines, el, name, eid, indent): lines.append(f'{indent}') inner = f'{indent}\t' if el.get('path'): lines.append(f'{inner}{el["path"]}') emit_title(lines, el, name, inner, auto=not el.get('path')) emit_common_flags(lines, el, inner) if el.get('editMode'): lines.append(f'{inner}{el["editMode"]}') emit_column_pics(lines, el, inner) # CheckBoxType: нет ключа → умный дефолт Auto; "" → подавить; значение → маппинг _cbt_map = {'auto': 'Auto', 'checkbox': 'CheckBox', 'switcher': 'Switcher', 'tumbler': 'Tumbler'} if 'checkBoxType' in el: if el.get('checkBoxType'): lines.append(f'{inner}{_cbt_map.get(str(el["checkBoxType"]).lower(), el["checkBoxType"])}') else: lines.append(f'{inner}Auto') emit_title_location(lines, el, inner, 'Right') emit_layout(lines, el, inner) if el.get('warningOnEdit') is not None: emit_mltext(lines, inner, 'WarningOnEdit', el['warningOnEdit']) # FooterDataPath / FooterText — общие cell-свойства колонки (как у input/labelField) if el.get('footerDataPath'): lines.append(f'{inner}{esc_xml(str(el["footerDataPath"]))}') if el.get('footerText') is not None: emit_mltext(lines, inner, 'FooterText', el['footerText']) # Формат / формат редактирования (LocalStringType — строка или {ru,en}) if el.get('format'): emit_mltext(lines, inner, 'Format', el['format']) if el.get('editFormat'): emit_mltext(lines, inner, 'EditFormat', el['editFormat']) # Оформление (цвета/шрифты/граница) — перед компаньонами emit_appearance(lines, el, inner, 'field') # Companions emit_companion_panel(lines, 'ContextMenu', f'{name}\u041a\u043e\u043d\u0442\u0435\u043a\u0441\u0442\u043d\u043e\u0435\u041c\u0435\u043d\u044e', inner, el.get('contextMenu')) emit_companion(lines, 'ExtendedTooltip', f'{name}\u0420\u0430\u0441\u0448\u0438\u0440\u0435\u043d\u043d\u0430\u044f\u041f\u043e\u0434\u0441\u043a\u0430\u0437\u043a\u0430', inner, el.get('extendedTooltip')) emit_events(lines, el, name, inner, 'check') lines.append(f'{indent}') def emit_radio_button_field(lines, el, name, eid, indent): lines.append(f'{indent}') inner = f'{indent}\t' if el.get('path'): lines.append(f'{inner}{el["path"]}') emit_title(lines, el, name, inner, auto=not el.get('path')) emit_common_flags(lines, el, inner) if el.get('editMode'): lines.append(f'{inner}{el["editMode"]}') emit_title_location(lines, el, inner, 'None') rbt = normalize_radio_button_type(el.get('radioButtonType')) lines.append(f'{inner}{rbt}') if el.get('columnsCount') is not None: lines.append(f'{inner}{el["columnsCount"]}') emit_choice_list(lines, el, inner) emit_layout(lines, el, inner) if el.get('warningOnEdit') is not None: emit_mltext(lines, inner, 'WarningOnEdit', el['warningOnEdit']) # Оформление (цвета/шрифты/граница) — перед компаньонами emit_appearance(lines, el, inner, 'field') emit_companion_panel(lines, 'ContextMenu', f'{name}КонтекстноеМеню', inner, el.get('contextMenu')) emit_companion(lines, 'ExtendedTooltip', f'{name}РасширеннаяПодсказка', inner, el.get('extendedTooltip')) emit_events(lines, el, name, inner, 'radio') lines.append(f'{indent}') # Заголовок декорации (Label/Picture): formatted-aware через единую ML-text форму # (reuse resolve_ml_formatted, как у extendedTooltip). Sibling-ключ formatted — back-compat override. def emit_decoration_title(lines, el, name, indent, auto=False): has_key = 'title' in el title_val = el['title'] if has_key else (title_from_name(name) if (auto and name) else None) if title_val: text, fmt = resolve_ml_formatted(title_val) if 'formatted' in el: fmt = bool(el['formatted']) lines.append(f'{indent}<Title formatted="{"true" if fmt else "false"}">') emit_ml_items(lines, f'{indent}\t', text) lines.append(f'{indent}') if el.get('tooltip'): emit_mltext(lines, indent, 'ToolTip', el['tooltip']) if el.get('tooltipRepresentation'): lines.append(f'{indent}{el["tooltipRepresentation"]}') def emit_label(lines, el, name, eid, indent): lines.append(f'{indent}') inner = f'{indent}\t' # Порядок как у платформы: own-content (флаги/hyperlink/layout/оформление) ПЕРЕД Title # (корпус layout-first 16970 vs 44 — заодно убирает шум атрибуции харнесса на многострочном Title). emit_common_flags(lines, el, inner) if el.get('hyperlink') is True: lines.append(f'{inner}true') emit_layout(lines, el, inner) emit_appearance(lines, el, inner, 'decoration') emit_decoration_title(lines, el, name, inner, auto=True) # Companions emit_companion_panel(lines, 'ContextMenu', f'{name}\u041a\u043e\u043d\u0442\u0435\u043a\u0441\u0442\u043d\u043e\u0435\u041c\u0435\u043d\u044e', inner, el.get('contextMenu')) emit_companion(lines, 'ExtendedTooltip', f'{name}\u0420\u0430\u0441\u0448\u0438\u0440\u0435\u043d\u043d\u0430\u044f\u041f\u043e\u0434\u0441\u043a\u0430\u0437\u043a\u0430', inner, el.get('extendedTooltip')) emit_events(lines, el, name, inner, 'label') lines.append(f'{indent}') def emit_label_field(lines, el, name, eid, indent): lines.append(f'{indent}') inner = f'{indent}\t' if el.get('path'): lines.append(f'{inner}{el["path"]}') emit_title(lines, el, name, inner, auto=not el.get('path')) emit_common_flags(lines, el, inner) if el.get('titleLocation'): lines.append(f'{inner}{map_title_loc(el["titleLocation"])}') if el.get('editMode'): lines.append(f'{inner}{el["editMode"]}') # FooterDataPath — путь данных подвала колонки (общий cell-prop, как у input); после EditMode if el.get('footerDataPath'): lines.append(f'{inner}{esc_xml(str(el["footerDataPath"]))}') # PasswordMode на LabelField — платформа эмитит явный false (редко); факт. значение if el.get('passwordMode') is not None: lines.append(f'{inner}{"true" if el["passwordMode"] else "false"}') emit_column_pics(lines, el, inner) # ВНИМАНИЕ: у LabelField платформенный тег (опечатка 1С), не . if el.get('hyperlink') is True: lines.append(f'{inner}true') emit_layout(lines, el, inner) if el.get('warningOnEdit') is not None: emit_mltext(lines, inner, 'WarningOnEdit', el['warningOnEdit']) if el.get('footerText') is not None: emit_mltext(lines, inner, 'FooterText', el['footerText']) # Формат / формат редактирования (LocalStringType — строка или {ru,en}) if el.get('format'): emit_mltext(lines, inner, 'Format', el['format']) if el.get('editFormat'): emit_mltext(lines, inner, 'EditFormat', el['editFormat']) # Оформление (цвета/шрифты/граница + header/footer) — перед компаньонами emit_appearance(lines, el, inner, 'field') # Companions emit_companion_panel(lines, 'ContextMenu', f'{name}\u041a\u043e\u043d\u0442\u0435\u043a\u0441\u0442\u043d\u043e\u0435\u041c\u0435\u043d\u044e', inner, el.get('contextMenu')) emit_companion(lines, 'ExtendedTooltip', f'{name}\u0420\u0430\u0441\u0448\u0438\u0440\u0435\u043d\u043d\u0430\u044f\u041f\u043e\u0434\u0441\u043a\u0430\u0437\u043a\u0430', inner, el.get('extendedTooltip')) emit_events(lines, el, name, inner, 'labelField') lines.append(f'{indent}') # Блок свойств таблицы, привязанной к динамическому списку (Group A defaults + B/C). def emit_dynlist_table_block(lines, el, indent): # (useAlternationRowColor — общее свойство таблицы, эмитится в emit_table) # Group A (гарант. блок): дефолт + override ar = 'true' if el.get('autoRefresh') is True else 'false' lines.append(f'{indent}{ar}') arp = el['autoRefreshPeriod'] if el.get('autoRefreshPeriod') is not None else 60 lines.append(f'{indent}{arp}') lines.append(f'{indent}') lines.append(f'{indent}\tCustom') lines.append(f'{indent}\t0001-01-01T00:00:00') lines.append(f'{indent}\t0001-01-01T00:00:00') lines.append(f'{indent}') cfi = el.get('choiceFoldersAndItems') or 'Items' lines.append(f'{indent}{cfi}') rcr = 'true' if el.get('restoreCurrentRow') is True else 'false' lines.append(f'{indent}{rcr}') lines.append(f'{indent}') sr = 'false' if el.get('showRoot') is False else 'true' lines.append(f'{indent}{sr}') arc = 'true' if el.get('allowRootChoice') is True else 'false' lines.append(f'{indent}{arc}') uodc = el.get('updateOnDataChange') or 'Auto' lines.append(f'{indent}{uodc}') if el.get('userSettingsGroup'): lines.append(f'{indent}{el["userSettingsGroup"]}') agcru = 'false' if el.get('allowGettingCurrentRowURL') is False else 'true' lines.append(f'{indent}{agcru}') def emit_table(lines, el, name, eid, indent): _current_table_name['name'] = name # дефолт source для кастомных дополнений в commandBar lines.append(f'{indent}') inner = f'{indent}\t' if el.get('path'): lines.append(f'{inner}{el["path"]}') emit_title(lines, el, name, inner, auto=not el.get('path')) emit_common_flags(lines, el, inner) if el.get('representation'): lines.append(f'{inner}{el["representation"]}') if el.get('titleLocation'): lines.append(f'{inner}{map_title_loc(el["titleLocation"])}') # ChangeRowSet/Order — явное значение (в т.ч. false: платформа пишет его на ValueTable) if 'changeRowSet' in el and el['changeRowSet'] is not None: lines.append(f'{inner}{"true" if el["changeRowSet"] is True else "false"}') if 'changeRowOrder' in el and el['changeRowOrder'] is not None: lines.append(f'{inner}{"true" if el["changeRowOrder"] is True else "false"}') if el.get('autoInsertNewRow') is True: lines.append(f'{inner}true') # RowFilter — nil-плейсхолдер (ключ присутствует → эмитим) if 'rowFilter' in el: lines.append(f'{inner}') # Высота в строках () — отдельное свойство от (высота элемента, # эмитится generic-ом emit_layout ниже). Таблица может нести оба (237 в корпусе). if el.get('heightInTableRows'): lines.append(f'{inner}{el["heightInTableRows"]}') if el.get('header') is False: lines.append(f'{inner}
false
') if el.get('footer') is True: lines.append(f'{inner}
true
') if el.get('commandBarLocation'): lines.append(f'{inner}{el["commandBarLocation"]}') if el.get('searchStringLocation'): lines.append(f'{inner}{el["searchStringLocation"]}') if el.get('choiceMode') is True: lines.append(f'{inner}true') # Скаляры таблицы (захват «как есть»). Autofill — СВОЁ свойство таблицы (≠ AutoCommandBar autofill = tableAutofill). if el.get('autofill') is not None: lines.append(f'{inner}{"true" if el["autofill"] else "false"}') if el.get('multipleChoice') is True: lines.append(f'{inner}true') if el.get('searchOnInput'): lines.append(f'{inner}{el["searchOnInput"]}') if el.get('markIncomplete') is not None: lines.append(f'{inner}{"true" if el["markIncomplete"] else "false"}') # Высота шапки/подвала в строках (pass-through; 1С толерантна к порядку детей Table) if el.get('headerHeight') is not None: lines.append(f'{inner}{el["headerHeight"]}') if el.get('footerHeight') is not None: lines.append(f'{inner}{el["footerHeight"]}') if el.get('useAlternationRowColor') is True: lines.append(f'{inner}true') if el.get('selectionMode'): lines.append(f'{inner}{el["selectionMode"]}') if el.get('rowSelectionMode'): lines.append(f'{inner}{el["rowSelectionMode"]}') if el.get('verticalLines') is False: lines.append(f'{inner}false') if el.get('horizontalLines') is False: lines.append(f'{inner}false') if el.get('initialTreeView'): lines.append(f'{inner}{el["initialTreeView"]}') if el.get('enableDrag') is not None: lines.append(f'{inner}{"true" if el["enableDrag"] else "false"}') if el.get('rowPictureDataPath'): lines.append(f'{inner}{el["rowPictureDataPath"]}') # RowsPicture — та же конвенция, что ValuesPicture (дефолт LoadTransparent=false; abs/TransparentPixel) emit_picture_ref(lines, el.get('rowsPicture'), 'RowsPicture', inner) # Использование текущей строки таблицы (pass-through; в корпусе соседствует с блоком дин-списка) if el.get('currentRowUse'): lines.append(f'{inner}{el["currentRowUse"]}') # Запрос обновления дин-списка (pass-through; в корпусе всегда PullFromTop) if el.get('refreshRequest'): lines.append(f'{inner}{el["refreshRequest"]}') # Блок свойств дин-список-таблицы (помечена эвристикой) if el.get('_dynList'): emit_dynlist_table_block(lines, el, inner) if el.get('viewStatusLocation'): lines.append(f'{inner}{el["viewStatusLocation"]}') if el.get('searchControlLocation'): lines.append(f'{inner}{el["searchControlLocation"]}') emit_layout(lines, el, inner) # CommandSet таблицы эмитится через emit_layout (общий механизм поля) # Оформление (цвета/граница таблицы) — перед компаньонами emit_appearance(lines, el, inner, 'field') # Companions emit_companion_panel(lines, 'ContextMenu', f'{name}\u041a\u043e\u043d\u0442\u0435\u043a\u0441\u0442\u043d\u043e\u0435\u041c\u0435\u043d\u044e', inner, el.get('contextMenu')) # AutoCommandBar — with optional Autofill control if el.get('commandBar') is not None: emit_companion_panel(lines, 'AutoCommandBar', f'{name}\u041a\u043e\u043c\u0430\u043d\u0434\u043d\u0430\u044f\u041f\u0430\u043d\u0435\u043b\u044c', inner, el.get('commandBar')) elif el.get('tableAutofill') is not None: acb_id = new_id() acb_name = f'{name}\u041a\u043e\u043c\u0430\u043d\u0434\u043d\u0430\u044f\u041f\u0430\u043d\u0435\u043b\u044c' af_val = 'true' if el['tableAutofill'] else 'false' lines.append(f'{inner}') lines.append(f'{inner}\t{af_val}') lines.append(f'{inner}') else: emit_companion(lines, 'AutoCommandBar', f'{name}\u041a\u043e\u043c\u0430\u043d\u0434\u043d\u0430\u044f\u041f\u0430\u043d\u0435\u043b\u044c', inner) emit_companion(lines, 'ExtendedTooltip', f'{name}\u0420\u0430\u0441\u0448\u0438\u0440\u0435\u043d\u043d\u0430\u044f\u041f\u043e\u0434\u0441\u043a\u0430\u0437\u043a\u0430', inner, el.get('extendedTooltip')) adds = el.get('additions') emit_table_addition(lines, 'searchString', name, inner, get_addition_override(adds, 'searchString')) emit_table_addition(lines, 'viewStatus', name, inner, get_addition_override(adds, 'viewStatus')) emit_table_addition(lines, 'searchControl', name, inner, get_addition_override(adds, 'searchControl')) # Columns if el.get('columns') and len(el['columns']) > 0: lines.append(f'{inner}') for col in el['columns']: emit_element(lines, col, f'{inner}\t') lines.append(f'{inner}') emit_events(lines, el, name, inner, 'table') lines.append(f'{indent}
') def emit_pages(lines, el, name, eid, indent): lines.append(f'{indent}') inner = f'{indent}\t' emit_title(lines, el, name, inner) if el.get('pagesRepresentation'): lines.append(f'{inner}{el["pagesRepresentation"]}') # \u0418\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u0438\u0435 \u0442\u0435\u043a\u0443\u0449\u0435\u0439 \u0441\u0442\u0440\u043e\u043a\u0438 (\u043f\u043e\u0441\u043b\u0435 PagesRepresentation, \u043f\u043e\u0440\u044f\u0434\u043e\u043a XSD) if el.get('currentRowUse'): lines.append(f'{inner}{el["currentRowUse"]}') emit_common_flags(lines, el, inner) emit_layout(lines, el, inner) # \u041e\u0444\u043e\u0440\u043c\u043b\u0435\u043d\u0438\u0435 (\u0446\u0432\u0435\u0442\u0430/\u0448\u0440\u0438\u0444\u0442\u044b/\u0433\u0440\u0430\u043d\u0438\u0446\u0430) \u0437\u0430\u0433\u043e\u043b\u043e\u0432\u043a\u0430 \u0433\u0440\u0443\u043f\u043f\u044b \u0441\u0442\u0440\u0430\u043d\u0438\u0446 \u2014 TitleFont/TitleTextColor/\u2026 (\u043a\u0430\u043a \u0443 Page) emit_appearance(lines, el, inner, 'field') # Companion emit_companion(lines, 'ExtendedTooltip', f'{name}\u0420\u0430\u0441\u0448\u0438\u0440\u0435\u043d\u043d\u0430\u044f\u041f\u043e\u0434\u0441\u043a\u0430\u0437\u043a\u0430', inner, el.get('extendedTooltip')) emit_events(lines, el, name, inner, 'pages') # Children (pages) if el.get('children') and len(el['children']) > 0: lines.append(f'{inner}') for child in el['children']: emit_element(lines, child, f'{inner}\t') lines.append(f'{inner}') lines.append(f'{indent}') def emit_page(lines, el, name, eid, indent): lines.append(f'{indent}') inner = f'{indent}\t' emit_title(lines, el, name, inner, auto=True) emit_common_flags(lines, el, inner) # Картинка страницы (иконка вкладки): после Title/флагов, перед Group (порядок XSD). # Конвенция как у ValuesPicture (дефолт LoadTransparent=false): скаляр-Ref/'abs:X' или объект. emit_picture_ref(lines, el.get('picture'), 'Picture', inner) if el.get('group'): orientation_map = { 'horizontal': 'Horizontal', 'vertical': 'Vertical', 'alwayshorizontal': 'AlwaysHorizontal', 'alwaysvertical': 'AlwaysVertical', 'horizontalifpossible': 'HorizontalIfPossible', } orientation = orientation_map.get(str(el['group']).lower()) if orientation: lines.append(f'{inner}{orientation}') else: _warn_unrecognized('page group orientation', el['group'], ('vertical', 'horizontalIfPossible', 'alwaysHorizontal'), name) if el.get('showTitle') is not None: lines.append(f'{inner}{"true" if el["showTitle"] else "false"}') # Формат значения пути к данным заголовка (; парный к titleDataPath страницы) if el.get('format'): emit_mltext(lines, inner, 'Format', el['format']) if el.get('editFormat'): emit_mltext(lines, inner, 'EditFormat', el['editFormat']) emit_layout(lines, el, inner) # \u041e\u0444\u043e\u0440\u043c\u043b\u0435\u043d\u0438\u0435 \u0441\u0442\u0440\u0430\u043d\u0438\u0446\u044b (BackColor / TitleTextColor / TitleFont) \u2014 \u043f\u043e\u0441\u043b\u0435 ShowTitle, \u043f\u0435\u0440\u0435\u0434 \u043a\u043e\u043c\u043f\u0430\u043d\u044c\u043e\u043d\u043e\u043c emit_appearance(lines, el, inner, 'field') # Companion emit_companion(lines, 'ExtendedTooltip', f'{name}\u0420\u0430\u0441\u0448\u0438\u0440\u0435\u043d\u043d\u0430\u044f\u041f\u043e\u0434\u0441\u043a\u0430\u0437\u043a\u0430', inner, el.get('extendedTooltip')) # Children if el.get('children') and len(el['children']) > 0: lines.append(f'{inner}') for child in el['children']: emit_element(lines, child, f'{inner}\t') lines.append(f'{inner}') lines.append(f'{indent}') def emit_button(lines, el, name, eid, indent, in_cmd_bar=False): lines.append(f'{indent}') def emit_picture_decoration(lines, el, name, eid, indent): lines.append(f'{indent}') inner = f'{indent}\t' emit_decoration_title(lines, el, name, inner) # Текст при невыбранной картинке (NonselectedPictureText) — после Title (порядок корпуса) if el.get('nonselectedPictureText') is not None: emit_mltext(lines, inner, 'NonselectedPictureText', el['nonselectedPictureText']) emit_common_flags(lines, el, inner) # Источник картинки — ТОЛЬКО src (ключ 'picture' = тип/имя элемента, не источник). # Префикс "abs:" → встроенная картинка ; иначе именованная/стилевая . if el.get('src'): src_str = str(el['src']) lt = 'true' if el.get('loadTransparent') is True else 'false' lines.append(f'{inner}') if src_str.startswith('abs:'): lines.append(f'{inner}\t{esc_xml(src_str[4:])}') else: lines.append(f'{inner}\t{esc_xml(src_str)}') lines.append(f'{inner}\t{lt}') tpx = el.get('transparentPixel') if tpx: lines.append(f'{inner}\t') lines.append(f'{inner}') if el.get('hyperlink') is True: lines.append(f'{inner}true') emit_layout(lines, el, inner) # EnableDrag — фактическое значение (декорация-картинка перетаскиваема; декомпилятор ловит generic-ом) if el.get('enableDrag') is not None: lines.append(f'{inner}{"true" if el["enableDrag"] else "false"}') # Оформление (цвета/шрифт/граница) — профиль декорации (1С толерантна к порядку appearance) emit_appearance(lines, el, inner, 'decoration') # Companions emit_companion_panel(lines, 'ContextMenu', f'{name}\u041a\u043e\u043d\u0442\u0435\u043a\u0441\u0442\u043d\u043e\u0435\u041c\u0435\u043d\u044e', inner, el.get('contextMenu')) emit_companion(lines, 'ExtendedTooltip', f'{name}\u0420\u0430\u0441\u0448\u0438\u0440\u0435\u043d\u043d\u0430\u044f\u041f\u043e\u0434\u0441\u043a\u0430\u0437\u043a\u0430', inner, el.get('extendedTooltip')) emit_events(lines, el, name, inner, 'picture') lines.append(f'{indent}') def emit_picture_field(lines, el, name, eid, indent): lines.append(f'{indent}') inner = f'{indent}\t' if el.get('path'): lines.append(f'{inner}{el["path"]}') emit_title(lines, el, name, inner) emit_common_flags(lines, el, inner) if el.get('editMode'): lines.append(f'{inner}{el["editMode"]}') emit_column_pics(lines, el, inner) if el.get('titleLocation'): lines.append(f'{inner}{map_title_loc(el["titleLocation"])}') if el.get('hyperlink') is True: lines.append(f'{inner}true') emit_layout(lines, el, inner) # EnableDrag — фактическое значение (поле картинки перетаскиваемо; декомпилятор ловит generic-ом) if el.get('enableDrag') is not None: lines.append(f'{inner}{"true" if el["enableDrag"] else "false"}') # FooterDataPath / FooterText — общие cell-свойства колонки (как у input/labelField) if el.get('footerDataPath'): lines.append(f'{inner}{esc_xml(str(el["footerDataPath"]))}') if el.get('footerText') is not None: emit_mltext(lines, inner, 'FooterText', el['footerText']) # ValuesPicture — picture (collection) used to render the field's value. # Required for a Boolean-bound PictureField to actually show an icon. # Скаляр (Ref) или объект {src, loadTransparent}; LoadTransparent эмитится всегда. emit_picture_ref(lines, el.get('valuesPicture'), 'ValuesPicture', inner) if el.get('nonselectedPictureText') is not None: emit_mltext(lines, inner, 'NonselectedPictureText', el['nonselectedPictureText']) # Оформление (цвета/шрифты/граница) — перед компаньонами emit_appearance(lines, el, inner, 'field') # Companions emit_companion_panel(lines, 'ContextMenu', f'{name}\u041a\u043e\u043d\u0442\u0435\u043a\u0441\u0442\u043d\u043e\u0435\u041c\u0435\u043d\u044e', inner, el.get('contextMenu')) emit_companion(lines, 'ExtendedTooltip', f'{name}\u0420\u0430\u0441\u0448\u0438\u0440\u0435\u043d\u043d\u0430\u044f\u041f\u043e\u0434\u0441\u043a\u0430\u0437\u043a\u0430', inner, el.get('extendedTooltip')) emit_events(lines, el, name, inner, 'picField') lines.append(f'{indent}') def emit_simple_field(lines, el, name, eid, indent, xml_tag, type_key): # Спец-поля "документ/датчик" (SpreadSheet/HTML/Text/Formatted/ProgressBar/TrackBar): # единый скелет поля. Типоспец. enum/bool скаляры — через generic (emit_layout); # числовые скаляры датчиков (min/max/шаги) — без xsi:type; enableDrag — фактическое значение. lines.append(f'{indent}<{xml_tag} name="{name}" id="{eid}"{di_attr(el)}>') inner = f'{indent}\t' if el.get('path'): lines.append(f'{inner}{el["path"]}') emit_title(lines, el, name, inner, auto=not el.get('path')) emit_common_flags(lines, el, inner) if el.get('titleLocation'): lines.append(f'{inner}{map_title_loc(el["titleLocation"])}') if el.get('editMode'): lines.append(f'{inner}{el["editMode"]}') emit_layout(lines, el, inner) # EnableDrag — фактическое значение (SpreadSheet; платформа эмитит явный false). enableStartDrag — через emit_layout. if el.get('enableDrag') is not None: lines.append(f'{inner}{"true" if el["enableDrag"] else "false"}') # Датчики (ProgressBar/TrackBar) — числовые скаляры (без xsi:type) for key, tag in (('minValue', 'MinValue'), ('maxValue', 'MaxValue'), ('largeStep', 'LargeStep'), ('markingStep', 'MarkingStep'), ('step', 'Step')): if el.get(key) is not None: lines.append(f'{inner}<{tag}>{el[key]}') # Оформление (цвета/шрифты/граница) — перед компаньонами emit_appearance(lines, el, inner, 'field') # Companions emit_companion_panel(lines, 'ContextMenu', f'{name}КонтекстноеМеню', inner, el.get('contextMenu')) emit_companion(lines, 'ExtendedTooltip', f'{name}РасширеннаяПодсказка', inner, el.get('extendedTooltip')) emit_events(lines, el, name, inner, type_key) lines.append(f'{indent}') def emit_gantt_chart(lines, el, name, eid, indent): # GanttChartField — скелет поля + вложенная (полноценная таблица, через emit_element). lines.append(f'{indent}') inner = f'{indent}\t' if el.get('path'): lines.append(f'{inner}{el["path"]}') emit_title(lines, el, name, inner, auto=not el.get('path')) emit_common_flags(lines, el, inner) if el.get('titleLocation'): lines.append(f'{inner}{map_title_loc(el["titleLocation"])}') emit_layout(lines, el, inner) emit_appearance(lines, el, inner, 'field') emit_companion_panel(lines, 'ContextMenu', f'{name}КонтекстноеМеню', inner, el.get('contextMenu')) emit_companion(lines, 'ExtendedTooltip', f'{name}РасширеннаяПодсказка', inner, el.get('extendedTooltip')) # Вложенная таблица диаграммы Ганта (стандартный Table — переиспользуем emit_element) if el.get('ganttTable'): emit_element(lines, el['ganttTable'], inner) emit_events(lines, el, name, inner, 'ganttChart') lines.append(f'{indent}') def emit_calendar(lines, el, name, eid, indent): lines.append(f'{indent}') inner = f'{indent}\t' if el.get('path'): lines.append(f'{inner}{el["path"]}') emit_title(lines, el, name, inner, auto=not el.get('path')) emit_common_flags(lines, el, inner) if el.get('titleLocation'): loc_map = {'none': 'None', 'left': 'Left', 'right': 'Right', 'top': 'Top', 'bottom': 'Bottom', 'auto': 'Auto'} loc = loc_map.get(str(el['titleLocation']), str(el['titleLocation'])) lines.append(f'{inner}{loc}') emit_layout(lines, el, inner) # Календарно-специфичные свойства (порядок схемы: после layout, до companions) if el.get('selectionMode'): lines.append(f'{inner}{el["selectionMode"]}') if el.get('showCurrentDate') is not None: lines.append(f'{inner}{"true" if el["showCurrentDate"] else "false"}') if el.get('widthInMonths') is not None: lines.append(f'{inner}{el["widthInMonths"]}') if el.get('heightInMonths') is not None: lines.append(f'{inner}{el["heightInMonths"]}') if el.get('showMonthsPanel') is not None: lines.append(f'{inner}{"true" if el["showMonthsPanel"] else "false"}') # Оформление (цвета/шрифты/граница) — перед компаньонами emit_appearance(lines, el, inner, 'field') # Companions emit_companion_panel(lines, 'ContextMenu', f'{name}\u041a\u043e\u043d\u0442\u0435\u043a\u0441\u0442\u043d\u043e\u0435\u041c\u0435\u043d\u044e', inner, el.get('contextMenu')) emit_companion(lines, 'ExtendedTooltip', f'{name}\u0420\u0430\u0441\u0448\u0438\u0440\u0435\u043d\u043d\u0430\u044f\u041f\u043e\u0434\u0441\u043a\u0430\u0437\u043a\u0430', inner, el.get('extendedTooltip')) emit_events(lines, el, name, inner, 'calendar') lines.append(f'{indent}') def emit_command_bar(lines, el, name, eid, indent): lines.append(f'{indent}') inner = f'{indent}\t' emit_title(lines, el, name, inner) if el.get('commandSource'): lines.append(f'{inner}{el["commandSource"]}') if el.get('autofill') is True: lines.append(f'{inner}true') # CommandBar хранит HorizontalLocation фактически (включая Auto); ≠ дополнениям (Auto=скип) if el.get('horizontalLocation'): _hlv = {'auto': 'Auto', 'left': 'Left', 'right': 'Right', 'center': 'Center'}.get(str(el['horizontalLocation']).lower(), str(el['horizontalLocation'])) lines.append(f'{inner}{_hlv}') emit_common_flags(lines, el, inner) emit_layout(lines, el, inner) emit_companion(lines, 'ExtendedTooltip', f'{name}РасширеннаяПодсказка', inner, el.get('extendedTooltip')) # Children if el.get('children') and len(el['children']) > 0: lines.append(f'{inner}') for child in el['children']: emit_element(lines, child, f'{inner}\t', in_cmd_bar=True) lines.append(f'{inner}') lines.append(f'{indent}') def emit_popup(lines, el, name, eid, indent): lines.append(f'{indent}') inner = f'{indent}\t' emit_title(lines, el, name, inner, auto=True) emit_common_flags(lines, el, inner) # Источник команд попапа (после Title/ToolTip, перед компаньоном) — как у ButtonGroup/CommandBar if el.get('commandSource'): lines.append(f'{inner}{el["commandSource"]}') emit_command_picture(lines, el.get('picture'), el.get('loadTransparent'), inner) if el.get('representation'): lines.append(f'{inner}{el["representation"]}') emit_layout(lines, el, inner) # Оформление попапа (TitleTextColor / TitleFont) — перед компаньоном emit_appearance(lines, el, inner, 'field') emit_companion(lines, 'ExtendedTooltip', f'{name}РасширеннаяПодсказка', inner, el.get('extendedTooltip')) # Children if el.get('children') and len(el['children']) > 0: lines.append(f'{inner}') for child in el['children']: emit_element(lines, child, f'{inner}\t', in_cmd_bar=True) lines.append(f'{inner}') lines.append(f'{indent}') def emit_button_group(lines, el, name, eid, indent): lines.append(f'{indent}') inner = f'{indent}\t' emit_title(lines, el, name, inner) if el.get('commandSource'): lines.append(f'{inner}{el["commandSource"]}') if el.get('representation'): lines.append(f'{inner}{el["representation"]}') emit_common_flags(lines, el, inner) emit_layout(lines, el, inner) # Companion: ExtendedTooltip emit_companion(lines, 'ExtendedTooltip', f'{name}РасширеннаяПодсказка', inner, el.get('extendedTooltip')) # Children (кнопки в контексте командной панели) if el.get('children') and len(el['children']) > 0: lines.append(f'{inner}') for child in el['children']: emit_element(lines, child, f'{inner}\t', in_cmd_bar=True) lines.append(f'{inner}') lines.append(f'{indent}') # --- Attribute emitter --- def emit_functional_options(lines, fo, indent): # FunctionalOption.X…> — у Attribute/Command/Column. # Forgiving: "X"/"FunctionalOption.X" → FunctionalOption.X; GUID (расширение) — как есть. if not fo: return lines.append(f'{indent}') for opt in fo: v = str(opt) if re.match(r'^[0-9a-fA-F]{8}-[0-9a-fA-F-]{27,}$', v): pass elif v.startswith('FunctionalOption.'): pass else: v = f'FunctionalOption.{v}' lines.append(f'{indent}\t{v}') lines.append(f'{indent}') def emit_attr_column(lines, col, indent): # Колонка реквизита (ValueTable/Tree или AdditionalColumns): name/Title/Type/FunctionalOptions. col_id = new_id() lines.append(f'{indent}') if col.get('title'): emit_mltext(lines, f'{indent}\t', 'Title', col['title']) emit_type(lines, str(col.get('type', '')), f'{indent}\t') # Проверка заполнения колонки → (как у реквизита; bool true→ShowError / строка verbatim) cfc = col.get('fillCheck') if col.get('fillCheck') is not None else col.get('fillChecking') if cfc is not None: cfcv = ('ShowError' if cfc else None) if isinstance(cfc, bool) else str(cfc) if cfcv: lines.append(f'{indent}\t{cfcv}') emit_functional_options(lines, col.get('functionalOptions'), f'{indent}\t') # Ролевой доступ колонки (View/Edit) — xr-флаг, как у самого реквизита if col.get('view') is not None: emit_xr_flag(lines, 'View', col['view'], f'{indent}\t') if col.get('edit') is not None: emit_xr_flag(lines, 'Edit', col['edit'], f'{indent}\t') lines.append(f'{indent}') # --- Schema-параметры динамического списка (DataCompositionSchemaParameter) --- # Зеркало form-compile.ps1 (Emit-DLParameters). Та же сущность, что параметры СКД, но в # форме: обёртка + дети dcssch:. DSL переиспользует грамматику параметров СКД. # Контекстные дефолты: useRestriction эмитим ВСЕГДА, дефолт true (в СКД false); title — авто # из имени; пустое value — всегда xsi:nil (даже при известном типе). Канон. порядок детей # (по корпусу): name, title, valueType, value, useRestriction, expression, availableValue*, # valueListAllowed, availableAsField, inputParameters, denyIncompleteValues, use. def emit_dl_mltext(lines, indent, tag, text): # ML-текст с xsi:type="v8:LocalStringType" (в dcssch:* обязателен; emit_mltext его не ставит). lines.append(f'{indent}<{tag} xsi:type="v8:LocalStringType">') emit_ml_items(lines, f'{indent}\t', text) lines.append(f'{indent}') def split_dl_valuelist_csv(s): result = [] if s is None: return result items = [] buf = [] in_quote = None for ch in s: if in_quote: buf.append(ch) if ch == in_quote: in_quote = None elif ch in ("'", '"'): in_quote = ch buf.append(ch) elif ch == ',': items.append(''.join(buf)); buf = [] else: buf.append(ch) if buf: items.append(''.join(buf)) for raw in items: t = raw.strip() if len(t) >= 2 and ((t[0] == "'" and t[-1] == "'") or (t[0] == '"' and t[-1] == '"')): t = t[1:-1] if t != '': result.append(t) return result def parse_dl_param_shorthand(s): result = {'name': '', 'type': '', 'value': None, 'title': None} if '@valueList' in s: result['valueListAllowed'] = True s = re.sub(r'\s*@valueList', '', s) if '@hidden' in s: result['hidden'] = True s = re.sub(r'\s*@hidden', '', s) m = re.search(r'\[([^\]]*)\]', s) if m: result['title'] = m.group(1).strip() s = re.sub(r'\s*\[[^\]]*\]\s*', ' ', s).strip() # Тип может быть СОСТАВНЫМ (A | B | C — с пробелами); значение — после '=' (тип '=' не содержит). m = re.match(r'^([^:]+):\s*([^=]+?)(\s*=\s*(.*))?$', s) if m: result['name'] = m.group(1).strip() type_raw = m.group(2).strip() if re.search(r'[|+]', type_raw): result['type'] = ' | '.join(resolve_type_str(p.strip()) for p in re.split(r'\s*[|+]\s*', type_raw)) else: result['type'] = resolve_type_str(type_raw) if m.group(4): rhs = m.group(4).strip() items = split_dl_valuelist_csv(rhs) if len(items) >= 2: result['value'] = items result['valueListAllowed'] = True elif len(items) == 1: result['value'] = items[0] else: result['value'] = rhs else: result['name'] = s.strip() return result def is_dl_empty_value(v): if v is None: return True sv = str(v).strip() return sv == '' or sv == '_' or sv.lower() == 'null' def emit_dl_value(lines, type_str, val, indent, value_list_allowed=False): if is_dl_empty_value(val): # Дин-список: пустое значение платформа ВСЕГДА пишет как xsi:nil (даже при известном типе). if value_list_allowed: return lines.append(f'{indent}') return if isinstance(val, bool): val_str = 'true' if val else 'false' else: val_str = str(val) t = type_str or '' if re.match(r'^(date|dateTime|time)', t): lines.append(f'{indent}{esc_xml(val_str)}') elif t == 'boolean': lines.append(f'{indent}{esc_xml(val_str)}') elif t == 'v8:Type': ns_attr = _value_type_ns_attr('v8:Type', val_str) lines.append(f'{indent}{esc_xml(val_str)}') elif re.match(r'^ent:', t): # системное перечисление (ent:X) — value несёт тот же xsi:type lines.append(f'{indent}{esc_xml(val_str)}') elif re.match(r'^decimal', t): lines.append(f'{indent}{esc_xml(val_str)}') elif re.match(r'^string', t): lines.append(f'{indent}{esc_xml(val_str)}') elif re.match(r'^(CatalogRef|DocumentRef|EnumRef|ChartOfAccountsRef|ChartOfCharacteristicTypesRef|ChartOfCalculationTypesRef|BusinessProcessRef|TaskRef|ExchangePlanRef)\.', t): lines.append(f'{indent}{esc_xml(val_str)}') else: if re.match(r'^\d{4}-\d{2}-\d{2}T', val_str): lines.append(f'{indent}{esc_xml(val_str)}') elif val_str in ('true', 'false'): lines.append(f'{indent}{esc_xml(val_str)}') elif re.match(r'^(ПланСчетов|Справочник|Перечисление|Документ|ПланВидовХарактеристик|ПланВидовРасчета|БизнесПроцесс|Задача|РегистрСведений|ПланОбмена)\.', val_str) or 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_dl_value_type(lines, type_str, indent): if not type_str: return lines.append(f'{indent}') for part in re.split(r'\s*[|+]\s*', str(type_str)): emit_single_type(lines, part.strip(), f'{indent}\t') lines.append(f'{indent}') def emit_dl_available_value(lines, av, type_str, indent): lines.append(f'{indent}') av_val = av.get('value') if isinstance(av, dict) else None emit_dl_value(lines, type_str, av_val, f'{indent}\t', False) pres = (av.get('presentation') or av.get('title')) if isinstance(av, dict) else None if pres: emit_dl_mltext(lines, f'{indent}\t', 'dcssch:presentation', pres) lines.append(f'{indent}') def emit_dl_input_parameters(lines, ip, indent): if ip is None: return items = ip if isinstance(ip, list) else [ip] if len(items) == 0: return lines.append(f'{indent}') for item in items: lines.append(f'{indent}\t') if 'use' in item and item.get('use') is not None and not item.get('use'): lines.append(f'{indent}\t\tfalse') lines.append(f'{indent}\t\t{esc_xml(str(item.get("parameter", "")))}') if 'choiceParameters' in item: cp_items = item.get('choiceParameters') or [] if len(cp_items) == 0: lines.append(f'{indent}\t\t') else: lines.append(f'{indent}\t\t') for cp in cp_items: lines.append(f'{indent}\t\t\t') lines.append(f'{indent}\t\t\t\t{esc_xml(str(cp.get("name", "")))}') for v in (cp.get('values') or []): if isinstance(v, bool): lines.append(f'{indent}\t\t\t\t{"true" if v else "false"}') elif isinstance(v, (int, float)): lines.append(f'{indent}\t\t\t\t{v}') else: lines.append(f'{indent}\t\t\t\t{esc_xml(str(v))}') lines.append(f'{indent}\t\t\t') lines.append(f'{indent}\t\t') elif 'choiceParameterLinks' in item: cpl_items = item.get('choiceParameterLinks') or [] if len(cpl_items) == 0: lines.append(f'{indent}\t\t') else: lines.append(f'{indent}\t\t') for cpl in cpl_items: lines.append(f'{indent}\t\t\t') lines.append(f'{indent}\t\t\t\t{esc_xml(str(cpl.get("name", "")))}') lines.append(f'{indent}\t\t\t\t{esc_xml(str(cpl.get("value", "")))}') mode = str(cpl.get('mode') or 'Auto') lines.append(f'{indent}\t\t\t\t{mode}') lines.append(f'{indent}\t\t\t') lines.append(f'{indent}\t\t') elif 'typeLink' in item: # Связь по типу (dcscor:TypeLink) — field + linkItem (структурное значение параметра). tl = item.get('typeLink') or {} lines.append(f'{indent}\t\t') if tl.get('field') is not None: lines.append(f'{indent}\t\t\t{esc_xml(str(tl.get("field")))}') if tl.get('linkItem') is not None: lines.append(f'{indent}\t\t\t{esc_xml(str(tl.get("linkItem")))}') lines.append(f'{indent}\t\t') elif 'value' in item: val = item.get('value') if isinstance(val, bool): lines.append(f'{indent}\t\t{"true" if val else "false"}') elif isinstance(val, (int, float)): lines.append(f'{indent}\t\t{val}') elif isinstance(val, dict): emit_dl_mltext(lines, f'{indent}\t\t', 'dcscor:value', val) else: lines.append(f'{indent}\t\t{esc_xml(str(val))}') lines.append(f'{indent}\t') lines.append(f'{indent}') # ── dataParameters (значения параметров запроса в настройках компоновки) — порт из skd ── def _test_empty_value(v): if v is None: return True s = str(v).strip() return s == '' or s == '_' or s.lower() == 'null' def emit_empty_value(lines, type_str, indent, tag_prefix='', value_list_allowed=False): if value_list_allowed: return t = type_str or '' t_bare = t[3:] if t.startswith('xs:') else t pf = tag_prefix if t == '': lines.append(f'{indent}<{pf}value xsi:nil="true"/>') elif t == 'StandardPeriod': lines.append(f'{indent}<{pf}value xsi:type="v8:StandardPeriod">') lines.append(f'{indent}\tCustom') lines.append(f'{indent}\t0001-01-01T00:00:00') lines.append(f'{indent}\t0001-01-01T00:00:00') lines.append(f'{indent}') elif re.match(r'^string', t_bare): lines.append(f'{indent}<{pf}value xsi:type="xs:string"/>') elif re.match(r'^(date|time)', t_bare): lines.append(f'{indent}<{pf}value xsi:type="xs:dateTime">0001-01-01T00:00:00') elif re.match(r'^decimal', t_bare): lines.append(f'{indent}<{pf}value xsi:type="xs:decimal">0') elif t_bare == 'boolean': lines.append(f'{indent}<{pf}value xsi:type="xs:boolean">false') else: lines.append(f'{indent}<{pf}value xsi:nil="true"/>') _DP_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} 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() m = re.match(r'^([^=]+)=\s*(.+)$', s) if m: result['parameter'] = m.group(1).strip() val_str = m.group(2).strip() if val_str in _DP_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 in ('true', 'false'): result['value'] = (val_str == 'true') else: result['value'] = val_str else: result['parameter'] = s return result def emit_data_parameters(lines, items, indent, block_view_mode=None): if not items or len(items) == 0: return lines.append(f'{indent}') for dp in items: if isinstance(dp, str): parsed = parse_data_param_shorthand(dp) dp = {'parameter': parsed['parameter']} if parsed['value'] is not None: dp['value'] = parsed['value'] if parsed['use'] is False: dp['use'] = False if parsed['userSettingID']: dp['userSettingID'] = parsed['userSettingID'] if parsed['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.get("parameter", "")))}') vtype = str(dp.get('valueType') or '') val = dp.get('value') if isinstance(val, list): # Список значений параметра (valueListAllowed) — отдельный на каждое. avtype = str(dp.get('valueType', '')) for v in val: v_str = ('true' if v else 'false') if isinstance(v, bool) else str(v) if re.match(r'^[a-zA-Z]+:', avtype): lines.append(f'{indent}\t\t{esc_xml(v_str)}') elif re.match(r'^(ПланСчетов|Справочник|Перечисление|Документ|ПланВидовХарактеристик|ПланВидовРасчета|БизнесПроцесс|Задача|РегистрСведений|ПланОбмена)\.', v_str) or re.match(r'^(ChartOfAccounts|Catalog|Enum|Document|ChartOfCharacteristicTypes|ChartOfCalculationTypes|BusinessProcess|Task|InformationRegister|ExchangePlan)\.', v_str): lines.append(f'{indent}\t\t{esc_xml(v_str)}') else: lines.append(f'{indent}\t\t{esc_xml(v_str)}') elif dp.get('nilValue') is True: lines.append(f'{indent}\t\t') elif _test_empty_value(val) and vtype: emit_empty_value(lines, vtype, f'{indent}\t\t', tag_prefix='dcscor:', value_list_allowed=False) elif _test_empty_value(val): pass # нет значения → не эмитим value-узел (form дин-список: use=false плейсхолдер) elif val is not None: if isinstance(val, dict) and val.get('variant'): variant = str(val.get('variant')) has_date = 'date' in val has_sd = 'startDate' in val is_sbd = has_date or (not has_sd and variant.startswith('BeginningOf')) if is_sbd: lines.append(f'{indent}\t\t') lines.append(f'{indent}\t\t\t{esc_xml(variant)}') if variant == 'Custom': d = str(val.get('date') or '0001-01-01T00:00:00') lines.append(f'{indent}\t\t\t{esc_xml(d)}') lines.append(f'{indent}\t\t') else: lines.append(f'{indent}\t\t') lines.append(f'{indent}\t\t\t{esc_xml(variant)}') if variant == 'Custom': sd = str(val.get('startDate') or '0001-01-01T00:00:00') ed = str(val.get('endDate') or '0001-01-01T00:00:00') lines.append(f'{indent}\t\t\t{esc_xml(sd)}') lines.append(f'{indent}\t\t\t{esc_xml(ed)}') lines.append(f'{indent}\t\t') elif re.match(r'^[a-zA-Z]+:', vtype): v_str = str(val).lower() if isinstance(val, bool) else str(val) lines.append(f'{indent}\t\t{esc_xml(v_str)}') elif vtype == 'boolean' or isinstance(val, bool): lines.append(f'{indent}\t\t{esc_xml(str(val).lower())}') elif re.match(r'^date', vtype) or re.match(r'^\d{4}-\d{2}-\d{2}T', str(val)): lines.append(f'{indent}\t\t{esc_xml(str(val))}') elif re.match(r'^decimal', vtype): lines.append(f'{indent}\t\t{esc_xml(str(val))}') elif re.match(r'^string', vtype): lines.append(f'{indent}\t\t{esc_xml(str(val))}') elif re.match(r'^(ПланСчетов|Справочник|Перечисление|Документ|ПланВидовХарактеристик|ПланВидовРасчета|БизнесПроцесс|Задача|РегистрСведений|ПланОбмена)\.', str(val)) or re.match(r'^(ChartOfAccounts|Catalog|Enum|Document|ChartOfCharacteristicTypes|ChartOfCalculationTypes|BusinessProcess|Task|InformationRegister|ExchangePlan)\.', 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'): emit_us_presentation(lines, f'{indent}\t\t', 'dcsset:userSettingPresentation', dp['userSettingPresentation']) lines.append(f'{indent}\t') if block_view_mode is not None: lines.append(f'{indent}\t{esc_xml(str(block_view_mode))}') lines.append(f'{indent}') def emit_dl_parameter(lines, p, parsed, indent): is_obj = not isinstance(p, str) lines.append(f'{indent}') ci = f'{indent}\t' lines.append(f'{ci}{esc_xml(parsed["name"])}') # Title: явный override (shorthand [..] / объект title/presentation) или авто из имени. title = None if parsed.get('title'): title = parsed['title'] elif is_obj and p.get('title'): title = p['title'] elif is_obj and p.get('presentation'): title = p['presentation'] if title is None or (isinstance(title, str) and title == ''): title = title_from_name(parsed['name']) emit_dl_mltext(lines, ci, 'dcssch:title', title) # valueType if parsed.get('type'): emit_dl_value_type(lines, parsed['type'], ci) # value (дефолт nil; при valueListAllowed пустое — опускаем) vla = bool(parsed.get('valueListAllowed')) pv = parsed.get('value') if isinstance(pv, list): for v in pv: emit_dl_value(lines, parsed.get('type', ''), v, ci, False) elif parsed.get('value_explicit') and pv is not None and str(pv) == '' and (str(parsed.get('type', '')) == '' or re.match(r'^string', str(parsed.get('type', '')))): # Явный пустой СТРОКОВЫЙ параметр (value:"" от декомпилятора) → типизированный пустой # , НЕ nil. Решается ФОРМОЙ value (""→typed-empty, # null/отсутствие→nil), независимо от valueListAllowed; декомпилятор различает ""/null. # Корпус: 26 xs:string typed-empty. lines.append(f'{ci}') elif vla and is_dl_empty_value(pv) and parsed.get('value_explicit'): # valueListAllowed + явный пустой (value:null от декомпилятора) → платформа пишет nil lines.append(f'{ci}') else: emit_dl_value(lines, parsed.get('type', ''), pv, ci, vla) # useRestriction — ВСЕГДА; дефолт true; false только при явном useRestriction:false. ur = True if is_obj and 'useRestriction' in p: ur = bool(p['useRestriction']) lines.append(f'{ci}{"true" if ur else "false"}') # expression expr = str(p['expression']) if (is_obj and p.get('expression')) else None if expr: lines.append(f'{ci}{esc_xml(expr)}') # availableValues if is_obj and p.get('availableValues'): for av in p['availableValues']: emit_dl_available_value(lines, av, parsed.get('type', ''), ci) # valueListAllowed if vla: lines.append(f'{ci}true') # availableAsField=false (hidden или явный) aaf = None if parsed.get('hidden') is True: aaf = False if is_obj and 'availableAsField' in p: aaf = bool(p['availableAsField']) if aaf is False: lines.append(f'{ci}false') # inputParameters if is_obj and p.get('inputParameters'): emit_dl_input_parameters(lines, p['inputParameters'], ci) # denyIncompleteValues if is_obj and p.get('denyIncompleteValues') is True: lines.append(f'{ci}true') # use if is_obj and p.get('use'): lines.append(f'{ci}{esc_xml(str(p["use"]))}') lines.append(f'{indent}') def emit_dl_parameters(lines, params, indent): if not params: return for p in params: if isinstance(p, str): parsed = parse_dl_param_shorthand(p) else: resolved_type = '' if p.get('type'): if isinstance(p['type'], list): resolved_type = ' | '.join(resolve_type_str(str(x)) for x in p['type']) else: resolved_type = resolve_type_str(str(p['type'])) elif p.get('valueType'): resolved_type = resolve_type_str(str(p['valueType'])) parsed = {'name': str(p.get('name', '')), 'type': resolved_type, 'value': p.get('value') if 'value' in p else None, 'value_explicit': ('value' in p), 'title': None} if p.get('valueListAllowed') is True: parsed['valueListAllowed'] = True if p.get('hidden') is True: parsed['hidden'] = True emit_dl_parameter(lines, p, parsed, indent) def emit_attributes(lines, attrs, indent, conditional_appearance=None): has_ca = bool(conditional_appearance) and len(conditional_appearance) > 0 # Платформа ВСЕГДА эмитит (100% корпуса; 162 формы — пустой ). if (not attrs or len(attrs) == 0) and not has_ca: lines.append(f'{indent}') return if not attrs or len(attrs) == 0: # Нет реквизитов, но есть условное оформление (последний child ) lines.append(f'{indent}') emit_conditional_appearance(lines, conditional_appearance, f'{indent}\t', wrap_tag='ConditionalAppearance') lines.append(f'{indent}') return lines.append(f'{indent}') seen_attrs = set() for attr in attrs: attr_id = new_id() attr_name = str(attr['name']) _ensure_unique(attr_name, seen_attrs, 'attribute') lines.append(f'{indent}\t') inner = f'{indent}\t\t' # Title атрибута (зеркало emit_title): нет ключа → авто-вывод из имени (кроме main); # title "" → подавить; непустой → эмитить как есть. if 'title' in attr: if attr.get('title'): emit_mltext(lines, inner, 'Title', attr['title']) elif attr.get('main') is not True: emit_mltext(lines, inner, 'Title', title_from_name(attr_name)) # Type if attr.get('type'): emit_type(lines, str(attr['type']), inner) else: lines.append(f'{inner}') # valueType: ОписаниеТипов значений ValueList → # (та же грамматика типа, включая составной "A | B"). Forgiving-синонимы. # Три состояния: нет ключа → нет Settings; "" → пустой ; тип → с типом. vt_spec = None has_vt = False for k in ('valueType', 'typeDescription', 'описаниеТипов', 'типЗначений'): if k in attr: vt_spec = attr[k] has_vt = True break if has_vt: emit_type(lines, '' if vt_spec is None else str(vt_spec), inner, tag="Settings", tag_attrs=' xsi:type="v8:TypeDescription"') # Planner design-time (встроенный конфиг планировщика). if attr.get('planner') is not None: emit_planner_settings(lines, attr['planner'], inner) # Chart/GanttChart design-time (тип выводится из типа реквизита). if attr.get('chart') is not None: ctype = 'd4p1:GanttChart' if 'GanttChart' in str(attr.get('type', '')) else 'd4p1:Chart' emit_chart_settings(lines, attr['chart'], inner, ctype) if attr.get('main') is True: lines.append(f'{inner}true') # Доступ по ролям: просмотр/редактирование (порядок схемы: View → Edit, после MainAttribute) if attr.get('view') is not None: emit_xr_flag(lines, 'View', attr.get('view'), inner) if attr.get('edit') is not None: emit_xr_flag(lines, 'Edit', attr.get('edit'), inner) main_saved = False if attr.get('main') is True and attr.get('type'): t = str(attr['type']) main_saved = bool(re.match(r'^(CatalogObject|DocumentObject|ChartOfAccountsObject|ChartOfCalculationTypesObject|ChartOfCharacteristicTypesObject|ExchangePlanObject|BusinessProcessObject|TaskObject)\.', t)) or ('RecordManager.' in t) # Явный ключ savedData побеждает (в т.ч. False → суппресс авто-вывода main_saved); нет ключа → авто. emit_saved = (attr['savedData'] is True) if 'savedData' in attr else main_saved if emit_saved: lines.append(f'{inner}true') # Save: сохранение значения реквизита в пользовательских настройках. true → имя; # строка/массив → под-поля с авто-префиксом "имя." (путь с точкой / UUID / =имя — как есть). # Нет ключа или false → не эмитим. if 'save' in attr and attr['save'] is not None: save_fields = [] sv = attr['save'] if isinstance(sv, bool): if sv: save_fields.append(attr_name) else: for e in (sv if isinstance(sv, (list, tuple)) else [sv]): fld = str(e) if not fld: continue if fld != attr_name and '.' not in fld and not re.match(r'^\d+/\d+', fld): fld = f'{attr_name}.{fld}' if fld not in save_fields: save_fields.append(fld) if save_fields: lines.append(f'{inner}') for f in save_fields: lines.append(f'{inner}\t{esc_xml(f)}') lines.append(f'{inner}') # Проверка заполнения → (реальный тег; в схеме нет). # bool true → ShowError; строка → verbatim. Синоним fillChecking. fc_raw = attr['fillCheck'] if 'fillCheck' in attr else attr.get('fillChecking') if fc_raw: fcv = 'ShowError' if isinstance(fc_raw, bool) else str(fc_raw) lines.append(f'{inner}{fcv}') # UseAlways: поля, всегда читаемые. Две формы DSL сливаются: # attr.useAlways[] (короткие имена) + columns с useAlways:true → ИмяРеквизита.Поле. ua_fields = [] for e in (attr.get('useAlways') or []): fld = str(e) # Префикс "ИмяРеквизита." добавляем к коротким именам. Поля дин-списка с маркером "~" # (query-поля, ~13% корпуса) — префикс ставится ПОСЛЕ "~": ~Остановлен → ~Список.Остановлен. # Полная форма (~Список.Остановлен / Список.Остановлен) — verbatim (forgiving ввод). if fld.startswith('~'): bare = fld[1:] if not re.match(r'^' + re.escape(attr_name) + r'\.', bare): bare = f'{attr_name}.{bare}' fld = f'~{bare}' elif not re.match(r'^' + re.escape(attr_name) + r'\.', fld) and not re.match(r'^\d+/\d+', fld): # UUID-ссылка (1/0:GUID) — НЕ префиксуем (платформа хранит её без "имя.") fld = f'{attr_name}.{fld}' if fld not in ua_fields: ua_fields.append(fld) for col in (attr.get('columns') or []): if col.get('useAlways') is True: fld = f'{attr_name}.{col["name"]}' if fld not in ua_fields: ua_fields.append(fld) if ua_fields: lines.append(f'{inner}') for f in ua_fields: lines.append(f'{inner}\t{f}') lines.append(f'{inner}') emit_functional_options(lines, attr.get('functionalOptions'), inner) # Columns: прямые + (доп. колонки табличных частей объекта). # Прямые сначала, затем AdditionalColumns-группы. Для дин-списка (settings) прямые НЕ эмитим. has_direct_cols = bool(attr.get('columns')) and len(attr['columns']) > 0 and not attr.get('settings') has_add_cols = bool(attr.get('additionalColumns')) and len(attr['additionalColumns']) > 0 if has_direct_cols or has_add_cols: lines.append(f'{inner}') if has_direct_cols: seen_cols = set() # колонки уникальны в пределах своего реквизита for col in attr['columns']: _ensure_unique(str(col['name']), seen_cols, f"column of '{attr_name}'") emit_attr_column(lines, col, f'{inner}\t') if has_add_cols: for ac in attr['additionalColumns']: ac_cols = ac.get('columns') or [] if not ac_cols: # Пустая группа доп.колонок (table-ref без колонок) → self-closing (как платформа) lines.append(f'{inner}\t') continue lines.append(f'{inner}\t') seen_ac_cols = set() # уникальность в пределах группы AdditionalColumns for col in ac_cols: _ensure_unique(str(col['name']), seen_ac_cols, f"column of '{attr_name}'") emit_attr_column(lines, col, f'{inner}\t\t') lines.append(f'{inner}\t') lines.append(f'{inner}') # Settings (динамический список) if attr.get('settings'): s = attr['settings'] lines.append(f'{inner}') si = f'{inner}\t' # Порядок платформы: AutoFillAvailableFields, ManualQuery, DynamicDataRead, QueryText, Field*, MainTable, ListSettings # AutoFillAvailableFields — дефолт true; эмитим только при заданном ключе (отклонение). if s.get('autoFillAvailableFields') is not None: lines.append(f'{si}{"true" if s["autoFillAvailableFields"] else "false"}') # Порядок платформы: ManualQuery, DynamicDataRead, QueryText, Field*, MainTable, ListSettings has_query = bool(s.get('query') and str(s['query']).strip()) # Явный ключ manualQuery (в т.ч. False) ПОБЕЖДАЕТ эвристику has_query (платформа изредка # хранит QueryText при ManualQuery=false — декомпилятор фиксирует отклонение). if s.get('manualQuery') is not None: mq = 'true' if s['manualQuery'] else 'false' else: mq = 'true' if has_query else 'false' lines.append(f'{si}{mq}') # DynamicDataRead: дефолт true; false только при явном отключении ddr = 'false' if s.get('dynamicDataRead') is False else 'true' lines.append(f'{si}{ddr}') if has_query: qtext = resolve_query_value(str(s['query']), QUERY_BASE_DIR) lines.append(f'{si}{esc_xml(qtext)}') # Явные поля набора (редко): override title/dataPath if s.get('fields'): for fld in s['fields']: # Тип поля набора: DataSetFieldField (дефолт) vs DataSetFieldNestedDataSet # (поле-вложенный набор = реквизит табличной части; маркер nested). # folder = папка-группировка полей (DataSetFieldFolder, без ); nested = вложенный набор. is_folder = bool(fld.get('folder')) ftype = 'DataSetFieldNestedDataSet' if fld.get('nested') else ('DataSetFieldFolder' if is_folder else 'DataSetFieldField') lines.append(f'{si}') # dataPath: явный (включая "" → self-closing) побеждает; иначе fallback на field. if fld.get('dataPath') is not None: dp = str(fld.get('dataPath')) elif is_folder: dp = '' else: dp = str(fld.get('field', '')) if dp == '': lines.append(f'{si}\t') else: lines.append(f'{si}\t{esc_xml(dp)}') if not is_folder: lines.append(f'{si}\t{esc_xml(str(fld.get("field", "")))}') if fld.get('title'): lines.append(f'{si}\t') emit_ml_items(lines, f'{si}\t\t', fld['title']) lines.append(f'{si}\t') # Ограничения использования поля — после title, перед presentationExpression emit_restrict_block(lines, 'useRestriction', fld.get('useRestriction'), f'{si}\t') emit_restrict_block(lines, 'attributeUseRestriction', fld.get('attributeUseRestriction'), f'{si}\t') # presentationExpression поля — перед valueType (порядок исходника) if fld.get('presentationExpression'): lines.append(f'{si}\t{esc_xml(str(fld["presentationExpression"]))}') # valueType поля набора (тип значения; вычисляемые/кастомные поля) if fld.get('valueType'): emit_dl_value_type(lines, fld['valueType'], f'{si}\t') # appearance поля (формат/оформление) — после valueType (порядок исходника) if fld.get('appearance'): lines.append(f'{si}\t') for ak, av in fld['appearance'].items(): emit_appearance_value(lines, ak, av, f'{si}\t\t') lines.append(f'{si}\t') # inputParameters поля (связь по параметрам выбора) — в конце if fld.get('inputParameters'): emit_dl_input_parameters(lines, fld['inputParameters'], f'{si}\t') lines.append(f'{si}') # Вычисляемые поля DataSet () — после Field*, до Parameter*. emit_calc_fields(lines, s.get('calculatedFields'), si) # Schema-параметры дин-списка (DataCompositionSchemaParameter) — после Field*, до MainTable. emit_dl_parameters(lines, s.get('parameters'), si) # Ключ набора (query-based список без MainTable): KeyType (RowNumber/FieldValue/RowKey) # + KeyField* — после Parameter*, до MainTable. Захват/эмит факт. значений. if s.get('keyType'): lines.append(f'{si}{esc_xml(str(s["keyType"]))}') if s.get('keyFields'): for kf in s['keyFields']: lines.append(f'{si}{esc_xml(str(kf))}') if s.get('mainTable'): lines.append(f'{si}{normalize_meta_type_ref(str(s["mainTable"]))}') # GetInvisibleFieldPresentations — после MainTable (дефолт true; эмитим только при заданном ключе = отклонении false). if s.get('getInvisibleFieldPresentations') is not None: lines.append(f'{si}{"true" if s["getInvisibleFieldPresentations"] else "false"}') # AutoSaveUserSettings — после MainTable (дефолт true; эмитим только при заданном ключе = отклонении). if s.get('autoSaveUserSettings') is not None: lines.append(f'{si}{"true" if s["autoSaveUserSettings"] else "false"}') # ListSettings: filter/order/conditionalAppearance (skd-грамматика) + каноничные блок-GUID. # Нет items → контейнеры всё равно эмитятся (blockMeta) = каноничный пустой скелет платформы. lsi = f'{si}\t' lines.append(f'{si}') ls_open_idx = len(lines) - 1 # для self-closing, если внутри ничего не эмитнётся ls_shape = s.get('listSettings') if ls_shape is not None: # Частичная/минимальная форма скелета — эмитим ТОЛЬКО указанные части с их блок-метой. for tag, pv in ls_shape.items(): # Значение дескриптора: строка-код "vu" ИЛИ объект {meta, presentation} # (контейнер несёт собственный userSettingPresentation — подпись настройки). if isinstance(pv, dict): meta = str(pv.get('meta', '')); bpres = pv.get('presentation') else: meta = str(pv); bpres = None bvm = 'Normal' if 'v' in meta else None if tag == 'filter': bus = CANON_FILTER_ID if 'u' in meta else None emit_filter(lines, s.get('filter'), lsi, block_view_mode=bvm, block_user_setting_id=bus, block_user_setting_presentation=bpres) elif tag == 'order': bus = CANON_ORDER_ID if 'u' in meta else None emit_order(lines, s.get('order'), lsi, block_view_mode=bvm, block_user_setting_id=bus, block_user_setting_presentation=bpres) elif tag == 'conditionalAppearance': bus = CANON_CA_ID if 'u' in meta else None emit_conditional_appearance(lines, s.get('conditionalAppearance'), lsi, block_view_mode=bvm, block_user_setting_id=bus, block_user_setting_presentation=bpres) elif tag == 'itemsViewMode': lines.append(f'{lsi}Normal') elif tag == 'itemsUserSettingID': lines.append(f'{lsi}{CANON_ITEMS_ID}') elif tag == 'itemsUserSettingPresentation': emit_us_presentation(lines, lsi, 'dcsset:itemsUserSettingPresentation', pv) elif tag == 'dataParameters': emit_data_parameters(lines, s.get('dataParameters'), lsi) elif tag == 'structure': emit_list_grouping(lines, get_list_grouping_value(s), lsi) else: # Полный каноничный скелет (умолчание, ~93% форм) — без изменений. emit_filter(lines, s.get('filter'), lsi, block_view_mode='Normal', block_user_setting_id=CANON_FILTER_ID) # dataParameters — после filter, до order (XSD-порядок ListSettings) if 'dataParameters' in s: emit_data_parameters(lines, s.get('dataParameters'), lsi) emit_order(lines, s.get('order'), lsi, block_view_mode='Normal', block_user_setting_id=CANON_ORDER_ID) emit_conditional_appearance(lines, s.get('conditionalAppearance'), lsi, block_view_mode='Normal', block_user_setting_id=CANON_CA_ID) # Группировка строк списка (авторинг без round-trip дескриптора) — после CA, до itemsViewMode emit_list_grouping(lines, get_list_grouping_value(s), lsi) lines.append(f'{lsi}Normal') lines.append(f'{lsi}{CANON_ITEMS_ID}') if len(lines) - 1 == ls_open_idx: # Пустой дескриптор listSettings:{} (оригинал = ) → зеркалим self-closing. lines[ls_open_idx] = f'{si}' else: lines.append(f'{si}') lines.append(f'{inner}') lines.append(f'{indent}\t') # Условное оформление формы — последний child (та же DCS-грамматика, что settings CA) emit_conditional_appearance(lines, conditional_appearance, f'{indent}\t', wrap_tag='ConditionalAppearance') lines.append(f'{indent}') # --- Parameter emitter --- def emit_parameters(lines, params, indent): if not params or len(params) == 0: return lines.append(f'{indent}') seen_params = set() for param in params: _ensure_unique(str(param['name']), seen_params, 'parameter') lines.append(f'{indent}\t') inner = f'{indent}\t\t' emit_type(lines, str(param.get('type', '')), inner) if param.get('key') is True: lines.append(f'{inner}true') lines.append(f'{indent}\t') lines.append(f'{indent}') # --- Command emitter --- def emit_commands(lines, cmds, indent): if not cmds or len(cmds) == 0: return lines.append(f'{indent}') seen_cmds = set() for cmd in cmds: cmd_id = new_id() _ensure_unique(str(cmd['name']), seen_cmds, 'command') lines.append(f'{indent}\t') inner = f'{indent}\t\t' # Заголовок команды (зеркало emit_title): ключ есть+непустой → эмитим; ключ есть+"" → суппресс # (в оригинале нет — не додумывать); ключ отсутствует → авто-вывод из имени. if 'title' in cmd: if cmd['title']: emit_mltext(lines, inner, 'Title', cmd['title']) else: cmd_title = title_from_name(str(cmd['name'])) if cmd_title: emit_mltext(lines, inner, 'Title', cmd_title) if cmd.get('tooltip'): emit_mltext(lines, inner, 'ToolTip', cmd['tooltip']) # Доступность команды по ролям (после ToolTip, до Action) if cmd.get('use') is not None: emit_xr_flag(lines, 'Use', cmd.get('use'), inner) if cmd.get('action'): lines.append(f'{inner}<Action>{cmd["action"]}</Action>') if cmd.get('modifiesSavedData') is True: lines.append(f'{inner}<ModifiesSavedData>true</ModifiesSavedData>') emit_functional_options(lines, cmd.get('functionalOptions'), inner) if cmd.get('currentRowUse'): lines.append(f'{inner}<CurrentRowUse>{cmd["currentRowUse"]}</CurrentRowUse>') # Используемая таблица — имя элемента-таблицы (xsi:type обязателен). # Forgiving-ключи: table / associatedTableElementId (XML-тег) / ИспользуемаяТаблица (рус., регистр-незав.) _cmd_norm = {k.replace(' ', '').lower(): v for k, v in cmd.items()} cmd_table = (_cmd_norm.get('table') or _cmd_norm.get('associatedtableelementid') or _cmd_norm.get('используемаятаблица')) if cmd_table: lines.append(f'{inner}<AssociatedTableElementId xsi:type="xs:string">{esc_xml(str(cmd_table))}</AssociatedTableElementId>') if cmd.get('shortcut'): lines.append(f'{inner}<Shortcut>{cmd["shortcut"]}</Shortcut>') emit_command_picture(lines, cmd.get('picture'), cmd.get('loadTransparent'), inner) if cmd.get('representation'): lines.append(f'{inner}<Representation>{cmd["representation"]}</Representation>') lines.append(f'{indent}\t</Command>') lines.append(f'{indent}</Commands>') # Командный интерфейс формы (<CommandInterface>): панели CommandBar + NavigationPanel. # Элемент: строка (голый command, Type=Auto) или dict. Порядок тегов: # Command, Type(деф. Auto), Attribute, CommandGroup, Index, DefaultVisible, Visible(xr-flag). def _resolve_command_group_key(key, panel_tag): """Ключ-группа древовидной формы → CommandGroup (зависит от панели); иначе verbatim.""" k = re.sub(r'\s', '', str(key)).lower() if panel_tag == 'NavigationPanel': m = {'important': 'FormNavigationPanelImportant', 'важное': 'FormNavigationPanelImportant', 'goto': 'FormNavigationPanelGoTo', 'перейти': 'FormNavigationPanelGoTo', 'seealso': 'FormNavigationPanelSeeAlso', 'смтакже': 'FormNavigationPanelSeeAlso'} else: m = {'important': 'FormCommandBarImportant', 'важное': 'FormCommandBarImportant', 'createbasedon': 'FormCommandBarCreateBasedOn', 'создатьнаосновании': 'FormCommandBarCreateBasedOn'} return m.get(k, key) def emit_command_interface(lines, ci, indent): if not ci: return inner = f'{indent}\t' panels = [ ('CommandBar', ('commandBar', 'команднаяПанель', 'КоманднаяПанель')), ('NavigationPanel', ('navigationPanel', 'панельНавигации', 'ПанельНавигации')), ] present = [] for tag, syns in panels: items = None for syn in syns: if isinstance(ci, dict) and syn in ci: items = ci[syn] break if items is not None: present.append((tag, items)) if not present: return lines.append(f'{indent}<CommandInterface>') for tag, items in present: lines.append(f'{inner}<{tag}>') # Нормализация: плоский список пар (элемент, group-из-дерева). dict → древовидная форма. flat = [] if isinstance(items, dict): for gkey, gitems in items.items(): grp_tree = _resolve_command_group_key(gkey, tag) for it in gitems: flat.append((it, grp_tree)) else: for it in items: flat.append((it, None)) for item, tree_group in flat: if isinstance(item, str): cmd, typ, attr, grp, idx, dv, vis = item, 'Auto', None, None, None, None, None else: cmd = get_el_prop(item, ('command', 'команда')) typ = get_el_prop(item, ('type', 'тип')) or 'Auto' attr = get_el_prop(item, ('attribute', 'реквизит')) grp = get_el_prop(item, ('group', 'группа', 'группаКоманд')) idx = get_el_prop(item, ('index', 'индекс')) dv = get_el_prop(item, ('defaultVisible', 'видимость', 'видимостьПоУмолчанию')) vis = get_el_prop(item, ('visible', 'видимостьПоРолям', 'настройкаВидимости')) # group из дерева побеждает (если задан и непустой); явный group элемента — фолбэк if tree_group: grp = tree_group lines.append(f'{inner}\t<Item>') lines.append(f'{inner}\t\t<Command>{esc_xml(str(cmd))}</Command>') lines.append(f'{inner}\t\t<Type>{typ}</Type>') if attr: lines.append(f'{inner}\t\t<Attribute>{esc_xml(str(attr))}</Attribute>') if grp: lines.append(f'{inner}\t\t<CommandGroup>{esc_xml(str(grp))}</CommandGroup>') if idx is not None: lines.append(f'{inner}\t\t<Index>{idx}</Index>') if dv is not None: lines.append(f'{inner}\t\t<DefaultVisible>{"true" if dv else "false"}</DefaultVisible>') if vis is not None: emit_xr_flag(lines, 'Visible', vis, f'{inner}\t\t') lines.append(f'{inner}\t</Item>') lines.append(f'{inner}</{tag}>') lines.append(f'{indent}</CommandInterface>') # --- Properties emitter --- PROP_MAP = { "autoTitle": "AutoTitle", "windowOpeningMode": "WindowOpeningMode", "commandBarLocation": "CommandBarLocation", "saveDataInSettings": "SaveDataInSettings", "autoSaveDataInSettings": "AutoSaveDataInSettings", "autoTime": "AutoTime", "usePostingMode": "UsePostingMode", "repostOnWrite": "RepostOnWrite", "autoURL": "AutoURL", "autoFillCheck": "AutoFillCheck", "customizable": "Customizable", "enterKeyBehavior": "EnterKeyBehavior", "verticalScroll": "VerticalScroll", "scalingMode": "ScalingMode", "useForFoldersAndItems": "UseForFoldersAndItems", "reportResult": "ReportResult", "detailsData": "DetailsData", "reportFormType": "ReportFormType", "autoShowState": "AutoShowState", "width": "Width", "height": "Height", "group": "Group", } def emit_properties(lines, props, indent): if not props: return for p_name, p_value in props.items(): xml_name = PROP_MAP.get(p_name) if not xml_name: # Auto PascalCase xml_name = p_name[0].upper() + p_name[1:] # Пустая строка = суппресс-маркер (напр. autoTitle:"" — не эмитить и не додумывать) if isinstance(p_value, str) and p_value == '': continue # Convert boolean to lowercase if isinstance(p_value, bool): val = 'true' if p_value else 'false' else: val = str(p_value) lines.append(f'{indent}<{xml_name}>{val}</{xml_name}>') def detect_format_version(d): while d: cfg_path = os.path.join(d, "Configuration.xml") if os.path.isfile(cfg_path): with open(cfg_path, "r", encoding="utf-8-sig") as f: head = f.read(2000) m = re.search(r'<MetaDataObject[^>]+version="(\d+\.\d+)"', head) if m: return m.group(1) parent = os.path.dirname(d) if parent == d: break d = parent return "2.17" def _normalize_elements(defn): """Convert dict-style elements from --from-object generators to list-style expected by compiler. Generator format: elements = {"ИмяЭлемента": {"element": "input", "path": "..."}, ...} Compiler format: elements = [{"input": "ИмяЭлемента", "path": "..."}, ...] Also handles nested 'elements' in groups and 'columns' in tables recursively. """ def convert_elements(els): if isinstance(els, list): # Already list format — but may have nested dicts inside groups result = [] for el in els: if isinstance(el, dict): el = dict(el) # copy if 'elements' in el and isinstance(el['elements'], dict): el['elements'] = convert_elements(el['elements']) if 'columns' in el and isinstance(el['columns'], dict): el['columns'] = convert_columns(el['columns']) result.append(el) return result if isinstance(els, dict): result = [] for name, props in els.items(): if not isinstance(props, dict): continue new_el = {} el_type = props.get('element', 'input') # Map element type to the key name used in JSON DSL type_map = { 'input': 'input', 'check': 'check', 'labelField': 'labelField', 'table': 'table', 'group': 'group', 'pages': 'pages', 'page': 'page', 'label': 'label', 'button': 'button', 'checkBox': 'check', 'radioButton': 'radioButton', 'pictureField': 'pictureField', } mapped_type = type_map.get(el_type, el_type) new_el[mapped_type] = name for k, v in props.items(): if k == 'element': continue if k == 'elements' and isinstance(v, dict): new_el['elements'] = convert_elements(v) elif k == 'columns' and isinstance(v, dict): new_el['columns'] = convert_columns(v) elif k == 'groupType': # groupType → group property in DSL new_el['group'] = v elif k == 'showTitle': new_el['showTitle'] = v elif k == 'representation': new_el['representation'] = v elif k == 'autoCommandBar': new_el['autoCommandBar'] = v elif k == 'commandBarLocation': new_el['commandBarLocation'] = v else: new_el[k] = v result.append(new_el) return result return els def convert_columns(cols): if isinstance(cols, list): return cols if isinstance(cols, dict): result = [] for name, props in cols.items(): if not isinstance(props, dict): continue new_col = {} el_type = props.get('element', 'input') type_map = { 'input': 'input', 'check': 'check', 'labelField': 'labelField', 'checkBox': 'check', } mapped_type = type_map.get(el_type, el_type) new_col[mapped_type] = name for k, v in props.items(): if k == 'element': continue new_col[k] = v result.append(new_col) return result return cols if 'elements' in defn: defn['elements'] = convert_elements(defn['elements']) return defn def main(): sys.stdout.reconfigure(encoding="utf-8") sys.stderr.reconfigure(encoding="utf-8") global _next_id parser = argparse.ArgumentParser(description='Compile 1C managed form from JSON or object metadata', allow_abbrev=False) parser.add_argument('-JsonPath', type=str, default=None) parser.add_argument('-OutputPath', type=str, required=True) parser.add_argument('-FromObject', action='store_true', default=False) parser.add_argument('-ObjectPath', type=str, default=None) parser.add_argument('-Purpose', type=str, default=None) parser.add_argument('-Preset', type=str, default='erp-standard') parser.add_argument('-EmitDsl', type=str, default=None) args = parser.parse_args() # Form name -> purpose mapping _FORM_NAME_TO_PURPOSE = { '\u0424\u043e\u0440\u043c\u0430\u0414\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430': 'Item', # ФормаДокумента '\u0424\u043e\u0440\u043c\u0430\u042d\u043b\u0435\u043c\u0435\u043d\u0442\u0430': 'Item', # ФормаЭлемента '\u0424\u043e\u0440\u043c\u0430\u0421\u043f\u0438\u0441\u043a\u0430': 'List', # ФормаСписка '\u0424\u043e\u0440\u043c\u0430\u0412\u044b\u0431\u043e\u0440\u0430': 'Choice', # ФормаВыбора '\u0424\u043e\u0440\u043c\u0430\u0413\u0440\u0443\u043f\u043f\u044b': 'Folder', # ФормаГруппы '\u0424\u043e\u0440\u043c\u0430\u0417\u0430\u043f\u0438\u0441\u0438': 'Record', # ФормаЗаписи '\u0424\u043e\u0440\u043c\u0430\u0421\u0447\u0435\u0442\u0430': 'Item', # ФормаСчета '\u0424\u043e\u0440\u043c\u0430\u0423\u0437\u043b\u0430': 'Item', # ФормаУзла } # Mutual exclusion validation if args.FromObject and args.JsonPath: print("Cannot use both -JsonPath and -FromObject. Choose one mode.", file=sys.stderr) sys.exit(1) if not args.FromObject and not args.JsonPath: print("Either -JsonPath or -FromObject is required.", file=sys.stderr) sys.exit(1) # Normalize OutputPath in from-object mode: append /Ext/Form.xml if missing if args.FromObject: out_norm = args.OutputPath.rstrip('/\\') if not re.search(r'[/\\]Ext[/\\]Form\.xml$', out_norm): if re.search(r'[/\\]Ext$', out_norm): args.OutputPath = out_norm + '/Form.xml' else: args.OutputPath = out_norm + '/Ext/Form.xml' print(f"[resolved] OutputPath -> {args.OutputPath}") # --- Detect XML format version --- out_path_resolved = args.OutputPath if os.path.isabs(args.OutputPath) else os.path.join(os.getcwd(), args.OutputPath) assert_edit_allowed(out_path_resolved, "editable") format_version = detect_format_version(os.path.dirname(out_path_resolved)) # --- 0. From-object mode --- if args.FromObject: # Resolve object path and purpose from OutputPath convention: # .../TypePlural/ObjectName/Forms/FormName/Ext/Form.xml out_abs = out_path_resolved parts = re.split(r'[/\\]', out_abs) forms_idx = -1 for i in range(len(parts) - 1, -1, -1): if parts[i] == 'Forms': forms_idx = i break resolved_object_path = None resolved_purpose = None if forms_idx >= 2: form_name = parts[forms_idx + 1] object_name = parts[forms_idx - 1] type_plural_and_above = os.sep.join(parts[:forms_idx - 1]) if form_name in _FORM_NAME_TO_PURPOSE: resolved_purpose = _FORM_NAME_TO_PURPOSE[form_name] candidate = os.path.join(type_plural_and_above, f'{object_name}.xml') if os.path.exists(candidate): resolved_object_path = candidate # Apply: explicit -ObjectPath / -Purpose override resolved from_obj_path = None if args.ObjectPath: from_obj_path = args.ObjectPath if os.path.isabs(args.ObjectPath) else os.path.join(os.getcwd(), args.ObjectPath) if not from_obj_path.endswith('.xml'): from_obj_path += '.xml' elif resolved_object_path: from_obj_path = resolved_object_path print(f"[resolved] ObjectPath -> {from_obj_path}") else: print("Cannot derive object path from OutputPath. Use -ObjectPath explicitly.", file=sys.stderr) sys.exit(1) if not os.path.exists(from_obj_path): print(f"Object file not found: {from_obj_path}", file=sys.stderr) sys.exit(1) purpose = args.Purpose or resolved_purpose or 'Item' if resolved_purpose and not args.Purpose: print(f"[resolved] Purpose -> {purpose}") meta = parse_object_meta(from_obj_path) print(f"[from-object] Type={meta['Type']}, Name={meta['Name']}, Attrs={len(meta['Attributes'])}, TS={len(meta['TabularSections'])}") preset_data = load_preset(args.Preset, os.path.dirname(os.path.abspath(__file__)), out_path_resolved) supported = { 'Document': ['Item', 'List', 'Choice'], 'Catalog': ['Item', 'Folder', 'List', 'Choice'], 'InformationRegister': ['Record', 'List'], 'AccumulationRegister': ['List'], 'ChartOfCharacteristicTypes': ['Item', 'Folder', 'List', 'Choice'], 'ExchangePlan': ['Item', 'List', 'Choice'], 'ChartOfAccounts': ['Item', 'Folder', 'List', 'Choice'], } if meta['Type'] not in supported: print(f"Object type '{meta['Type']}' not supported. Supported: Document, Catalog, InformationRegister, AccumulationRegister, ChartOfCharacteristicTypes, ExchangePlan, ChartOfAccounts.", file=sys.stderr) sys.exit(1) if purpose not in supported[meta['Type']]: print(f"Purpose '{purpose}' not valid for {meta['Type']}. Valid: {', '.join(supported[meta['Type']])}", file=sys.stderr) sys.exit(1) dsl_dispatch = { 'Document': generate_document_dsl, 'Catalog': generate_catalog_dsl, 'InformationRegister': generate_information_register_dsl, 'AccumulationRegister': generate_accumulation_register_dsl, 'ChartOfCharacteristicTypes': generate_chart_of_characteristic_types_dsl, 'ExchangePlan': generate_exchange_plan_dsl, 'ChartOfAccounts': generate_chart_of_accounts_dsl, } dsl = dsl_dispatch[meta['Type']](meta, preset_data, purpose) if args.EmitDsl: dsl_path = args.EmitDsl if os.path.isabs(args.EmitDsl) else os.path.join(os.getcwd(), args.EmitDsl) os.makedirs(os.path.dirname(dsl_path) or '.', exist_ok=True) with open(dsl_path, 'w', encoding='utf-8') as f: json.dump(dsl, f, ensure_ascii=False, indent=2) print(f"[from-object] DSL saved: {dsl_path}") defn = json.loads(json.dumps(dsl)) # normalize OrderedDict to regular dict # Convert dict-style elements (from generators) to list-style (expected by compiler) defn = _normalize_elements(defn) else: # --- 1. Load and validate JSON --- json_path = args.JsonPath if not os.path.exists(json_path): print(f"File not found: {json_path}", file=sys.stderr) sys.exit(1) with open(json_path, 'r', encoding='utf-8-sig') as f: defn = json.load(f) global QUERY_BASE_DIR QUERY_BASE_DIR = os.path.dirname(os.path.abspath(json_path)) # --- 1b. Pre-pass: synonyms, main attribute inference, heuristics, autoCmdBar extraction --- def _normalize_synonyms(el): if not isinstance(el, dict): return # Companion-панели (объект/массив-значение) → commandBar/contextMenu normalize_panel_synonyms(el) # Тип-синонимы: commandBar/autoCommandBar → элемент-тип ТОЛЬКО при строковом значении synonyms = {'commandBar': 'cmdBar', 'autoCommandBar': 'autoCmdBar', 'extTooltip': 'extendedTooltip'} for src, dst in synonyms.items(): if src in el and dst not in el: if src in STR_ONLY_TYPE_SYNONYMS and not isinstance(el[src], str): continue el[dst] = el.pop(src) # Рекурсия в детей панелей (commandBar/contextMenu) for pk in ('commandBar', 'contextMenu'): pv = el.get(pk) kids = pv if isinstance(pv, list) else (pv.get('children') if isinstance(pv, dict) else None) if isinstance(kids, list): for child in kids: _normalize_synonyms(child) if isinstance(el.get('children'), list): for child in el['children']: _normalize_synonyms(child) if isinstance(el.get('columns'), list): for child in el['columns']: _normalize_synonyms(child) def _has_cmd_bar_recursive(el): if not isinstance(el, dict): return False if el.get('cmdBar') is not None: return True if isinstance(el.get('children'), list): for child in el['children']: if _has_cmd_bar_recursive(child): return True if isinstance(el.get('columns'), list): for child in el['columns']: if _has_cmd_bar_recursive(child): return True return False def _apply_dlist_table_heuristic(el, list_name, has_main_table): if not isinstance(el, dict): return if el.get('table') is not None and str(el.get('path', '')) == list_name: # Маркер дин-список-таблицы → emit_table эмитит блок свойств el['_dynList'] = True if 'tableAutofill' not in el: el['tableAutofill'] = False if 'commandBarLocation' not in el: el['commandBarLocation'] = 'None' # RowPictureDataPath: умный дефолт <Список>.DefaultPicture, если ключ ОТСУТСТВУЕТ. # Декомпилятор опускает при rpdp == smart-default; реальное отсутствие → ""-маркер (не # перезатирается). Гейт has_main_table снят: дин-список без mainTable тоже несёт RowPictureDataPath. if 'rowPictureDataPath' not in el: el['rowPictureDataPath'] = f'{list_name}.DefaultPicture' if isinstance(el.get('children'), list): for child in el['children']: _apply_dlist_table_heuristic(child, list_name, has_main_table) def _is_object_like_type(t): if not t: return False if t == 'DynamicList' or t == 'ConstantsSet': return True object_suffixes = ( 'CatalogObject', 'DocumentObject', 'DataProcessorObject', 'ReportObject', 'ExternalDataProcessorObject', 'ExternalReportObject', 'BusinessProcessObject', 'TaskObject', 'ChartOfAccountsObject', 'ChartOfCharacteristicTypesObject', 'ChartOfCalculationTypesObject', 'ExchangePlanObject', ) record_set_prefixes = ( 'InformationRegisterRecordSet', 'AccumulationRegisterRecordSet', 'AccountingRegisterRecordSet', 'CalculationRegisterRecordSet', 'InformationRegisterRecordManager', ) for s in object_suffixes: if t.startswith(s + '.'): return True for s in record_set_prefixes: if t.startswith(s + '.'): return True return False # 1b.1: Normalize synonyms recursively if isinstance(defn.get('elements'), list): for el in defn['elements']: _normalize_synonyms(el) # 1b.2: Extract autoCmdBar element from defn['elements'] main_acb_def = None if isinstance(defn.get('elements'), list): auto_bars = [el for el in defn['elements'] if isinstance(el, dict) and el.get('autoCmdBar') is not None] if len(auto_bars) > 1: print(f"form-compile: more than one autoCmdBar in def.elements (found {len(auto_bars)}); only one allowed.", file=sys.stderr) sys.exit(1) if len(auto_bars) == 1: main_acb_def = auto_bars[0] defn['elements'] = [el for el in defn['elements'] if el is not main_acb_def] # 1b.3: Infer main attribute if isinstance(defn.get('attributes'), list): has_explicit_main = any(a.get('main') is True for a in defn['attributes'] if isinstance(a, dict)) if not has_explicit_main: candidates = [] for a in defn['attributes']: if not isinstance(a, dict): continue if 'main' in a and a.get('main') is False: continue if _is_object_like_type(str(a.get('type', ''))): candidates.append(a) if len(candidates) == 1: candidates[0]['main'] = True print(f"[INFO] Inferred main attribute: {candidates[0].get('name')} ({candidates[0].get('type')})") elif len(candidates) > 1: names = ', '.join(c.get('name', '') for c in candidates) print(f"[WARN] Multiple main-attribute candidates: {names}; specify \"main\": true explicitly") # 1b.4: DynamicList → table heuristic (для ВСЕХ DynamicList-реквизитов, не только main) if isinstance(defn.get('attributes'), list) and isinstance(defn.get('elements'), list): for attr in defn['attributes']: if not isinstance(attr, dict) or str(attr.get('type', '')) != 'DynamicList': continue settings = attr.get('settings') or {} has_mt = bool(isinstance(settings, dict) and settings.get('mainTable')) for el in defn['elements']: _apply_dlist_table_heuristic(el, attr.get('name', ''), has_mt) # 1b.5: Compute main AutoCommandBar Autofill (B3) def _compute_main_acb_autofill(): if main_acb_def is not None: if 'autofill' in main_acb_def: return bool(main_acb_def.get('autofill')) return True if isinstance(defn.get('elements'), list): for el in defn['elements']: if _has_cmd_bar_recursive(el): return False return True # --- 2. Main compilation --- _next_id = 0 _seen_element_names.clear() # пул имён элементов (на случай повторного вызова в одном процессе) lines = [] lines.append('<?xml version="1.0" encoding="UTF-8"?>') lines.append(f'<Form xmlns="http://v8.1c.ru/8.3/xcf/logform" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:dcscor="http://v8.1c.ru/8.1/data-composition-system/core" xmlns:dcssch="http://v8.1c.ru/8.1/data-composition-system/schema" xmlns:dcsset="http://v8.1c.ru/8.1/data-composition-system/settings" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="{format_version}">') # Title form_title = defn.get('title') if not form_title and defn.get('properties') and defn['properties'].get('title'): form_title = defn['properties']['title'] if form_title: emit_mltext(lines, '\t', 'Title', form_title) # Properties (skip 'title' — handled above) # When form-level Title is set, default autoTitle=false (≈95% of ERP forms do this; # otherwise platform appends synonym → "Title: Synonym" double-titles). props_src = defn.get('properties') or {} props_clone = OrderedDict() if form_title and 'autoTitle' not in props_src: props_clone['autoTitle'] = False for k, v in props_src.items(): if k != 'title': props_clone[k] = v emit_properties(lines, props_clone, '\t') # CommandSet (excluded commands) if defn.get('excludedCommands') and len(defn['excludedCommands']) > 0: lines.append('\t<CommandSet>') for cmd in defn['excludedCommands']: lines.append(f'\t\t<ExcludedCommand>{cmd}</ExcludedCommand>') lines.append('\t</CommandSet>') # MobileDeviceCommandBarContent — форменный список имён командных панелей/кнопок # (Presentation пустой, CheckState=0, тип xs:string — константы; варьируется только имя-Value). # 12 форм корпуса несут один пустой item (Value="") — список присутствует, но не пуст по len. if defn.get('mobileCommandBarContent') is not None and len(defn['mobileCommandBarContent']) > 0: lines.append('\t<MobileDeviceCommandBarContent>') for nm in defn['mobileCommandBarContent']: lines.append('\t\t<xr:Item>') lines.append('\t\t\t<xr:Presentation/>') lines.append('\t\t\t<xr:CheckState>0</xr:CheckState>') # пустое значение → самозакрывающийся тег (зеркало платформы) if not str(nm): lines.append('\t\t\t<xr:Value xsi:type="xs:string"/>') else: lines.append(f'\t\t\t<xr:Value xsi:type="xs:string">{esc_xml(str(nm))}</xr:Value>') lines.append('\t\t</xr:Item>') lines.append('\t</MobileDeviceCommandBarContent>') # AutoCommandBar (always present, id=-1) acb_autofill = _compute_main_acb_autofill() acb_name = '\u0424\u043e\u0440\u043c\u0430\u041a\u043e\u043c\u0430\u043d\u0434\u043d\u0430\u044f\u041f\u0430\u043d\u0435\u043b\u044c' acb_halign = None if main_acb_def is not None: v = main_acb_def.get('autoCmdBar') if v: acb_name = str(v) if main_acb_def.get('name'): acb_name = str(main_acb_def['name']) if main_acb_def.get('horizontalAlign'): acb_halign = str(main_acb_def['horizontalAlign']) has_acb_children = bool(main_acb_def and isinstance(main_acb_def.get('children'), list) and len(main_acb_def['children']) > 0) # DisplayImportance форменной панели (адаптивная важность) — атрибут тега acb_di_attr = di_attr(main_acb_def) if main_acb_def is not None else '' has_inner = bool(acb_halign) or (not acb_autofill) or has_acb_children if has_inner: lines.append(f'\t<AutoCommandBar name="{acb_name}" id="-1"{acb_di_attr}>') if acb_halign: lines.append(f'\t\t<HorizontalAlign>{acb_halign}</HorizontalAlign>') if not acb_autofill: lines.append('\t\t<Autofill>false</Autofill>') if has_acb_children: lines.append('\t\t<ChildItems>') for child in main_acb_def['children']: emit_element(lines, child, '\t\t\t', in_cmd_bar=True) lines.append('\t\t</ChildItems>') lines.append('\t</AutoCommandBar>') else: lines.append(f'\t<AutoCommandBar name="{acb_name}" id="-1"{acb_di_attr}/>') # Events if defn.get('events'): for evt_name in defn['events']: if evt_name not in KNOWN_FORM_EVENTS: print(f"[WARN] Unknown form event '{evt_name}'. Known: {', '.join(KNOWN_FORM_EVENTS)}") lines.append('\t<Events>') for evt_name, evt_handler in defn['events'].items(): lines.append(f'\t\t<Event name="{evt_name}">{evt_handler}</Event>') lines.append('\t</Events>') # ChildItems (elements) if defn.get('elements') and len(defn['elements']) > 0: lines.append('\t<ChildItems>') for el in defn['elements']: emit_element(lines, el, '\t\t') lines.append('\t</ChildItems>') # Attributes emit_attributes(lines, defn.get('attributes'), '\t', conditional_appearance=defn.get('conditionalAppearance')) # Parameters emit_parameters(lines, defn.get('parameters'), '\t') # Commands emit_commands(lines, defn.get('commands'), '\t') # CommandInterface (командный интерфейс формы — последний дочерний Form) emit_command_interface(lines, defn.get('commandInterface'), '\t') # Close lines.append('</Form>') # --- 3. Write output --- out_path = args.OutputPath if not os.path.isabs(out_path): out_path = os.path.join(os.getcwd(), out_path) out_dir = os.path.dirname(out_path) if out_dir and not os.path.exists(out_dir): os.makedirs(out_dir, exist_ok=True) content = '\n'.join(lines) + '\n' write_utf8_bom(out_path, content) # --- 4. Auto-register form in parent object XML --- # Infer parent from OutputPath: .../TypePlural/ObjectName/Forms/FormName/Ext/Form.xml form_xml_dir = os.path.dirname(out_path) # Ext form_name_dir = os.path.dirname(form_xml_dir) # FormName forms_dir = os.path.dirname(form_name_dir) # Forms object_dir = os.path.dirname(forms_dir) # ObjectName type_plural_dir = os.path.dirname(object_dir) # TypePlural form_name = os.path.basename(form_name_dir) object_name = os.path.basename(object_dir) forms_leaf = os.path.basename(forms_dir) if forms_leaf == 'Forms': object_xml_path = os.path.join(type_plural_dir, f'{object_name}.xml') if os.path.exists(object_xml_path): with open(object_xml_path, 'r', encoding='utf-8-sig') as f: raw_text = f.read() # Check if already registered if f'<Form>{form_name}</Form>' not in raw_text: # Insert before </ChildObjects> if '</ChildObjects>' in raw_text: insert_line = f'\t\t\t<Form>{form_name}</Form>\n' raw_text = raw_text.replace('</ChildObjects>', insert_line + '\t\t</ChildObjects>', 1) elif '<ChildObjects/>' in raw_text: replacement = f'<ChildObjects>\n\t\t\t<Form>{form_name}</Form>\n\t\t</ChildObjects>' raw_text = raw_text.replace('<ChildObjects/>', replacement, 1) write_utf8_bom(object_xml_path, raw_text) print(f" Registered: <Form>{form_name}</Form> in {object_name}.xml") # --- 5. Summary --- el_count = _next_id print(f"[OK] Compiled: {args.OutputPath}") print(f" Elements+IDs: {el_count}") if defn.get('attributes'): print(f" Attributes: {len(defn['attributes'])}") if defn.get('commands'): print(f" Commands: {len(defn['commands'])}") if defn.get('parameters'): print(f" Parameters: {len(defn['parameters'])}") if __name__ == '__main__': main()