#!/usr/bin/env python3 # form-compile v1.21 — 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-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 # Apply ref defaults if is_ref and field_defaults and field_defaults.get('ref'): if field_defaults['ref'].get('choiceButton') is True: el['choiceButton'] = True # 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: ed_cols = [] ed_cols.append(OrderedDict([('input', '\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', '\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', ed_flag['Name']), ('path', f"\u041e\u0431\u044a\u0435\u043a\u0442.ExtDimensionTypes.{ed_flag['Name']}")])) elements.append(OrderedDict([ ('table', '\u0412\u0438\u0434\u044b\u0421\u0443\u0431\u043a\u043e\u043d\u0442\u043e'), ('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): return s.replace('&', '&').replace('<', '<').replace('>', '>').replace('"', '"') def emit_mltext(lines, indent, tag, text): if not text: lines.append(f"{indent}<{tag}/>") return lines.append(f"{indent}<{tag}>") lines.append(f"{indent}\t") lines.append(f"{indent}\t\tru") lines.append(f"{indent}\t\t{esc_xml(text)}") lines.append(f"{indent}\t") lines.append(f"{indent}") def new_uuid(): return str(uuid.uuid4()) def write_utf8_bom(path, content): with open(path, 'w', encoding='utf-8-sig', newline='') as f: f.write(content) # --- ID allocator --- _next_id = 0 def new_id(): global _next_id _next_id += 1 return _next_id # --- 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", "input", "check", "radio", "label", "labelField", "table", "pages", "page", "button", "picture", "picField", "calendar", "cmdBar", "popup", "showInHeader", "radioButtonType", "choiceList", "columnsCount", "name", "path", "title", "visible", "hidden", "enabled", "disabled", "readOnly", "userVisible", "on", "handlers", "titleLocation", "representation", "width", "height", "horizontalStretch", "verticalStretch", "autoMaxWidth", "autoMaxHeight", "maxWidth", "maxHeight", "multiLine", "passwordMode", "choiceButton", "clearButton", "spinButton", "dropListButton", "markIncomplete", "skipOnInput", "inputHint", "textEdit", "hyperlink", "showTitle", "united", "collapsed", "children", "columns", "changeRowSet", "changeRowOrder", "header", "footer", "commandBarLocation", "searchStringLocation", "pagesRepresentation", "type", "command", "stdCommand", "defaultButton", "locationInCommandBar", "src", "autofill", "choiceMode", "initialTreeView", "enableDrag", "enableStartDrag", "rowPictureDataPath", "tableAutofill", } TYPE_KEYS = ["columnGroup", "group", "input", "check", "radio", "label", "labelField", "table", "pages", "page", "button", "picture", "picField", "calendar", "cmdBar", "popup"] # 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", } # 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", } ENUM_VALUE_SYNONYMS = {"EnumValue", "ЗначениеПеречисления"} 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": ""} 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: 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 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, '')) def emit_events(lines, el, element_name, indent, type_key): if not el.get('on'): return # Validate event names if type_key and type_key in KNOWN_EVENTS: allowed = KNOWN_EVENTS[type_key] for evt in el['on']: if allowed and str(evt) not in allowed: print(f"[WARN] Unknown event '{evt}' for {type_key} '{element_name}'. Known: {', '.join(allowed)}") lines.append(f"{indent}") for evt in el['on']: evt_name = str(evt) handlers = el.get('handlers') if handlers and handlers.get(evt_name): handler = str(handlers[evt_name]) else: handler = get_handler_name(element_name, evt_name) lines.append(f'{indent}\t{handler}') lines.append(f"{indent}") def emit_companion(lines, tag, name, indent): cid = new_id() lines.append(f'{indent}<{tag} name="{name}" id="{cid}"/>') 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 False: lines.append(f"{indent}") lines.append(f"{indent}\tfalse") lines.append(f"{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") 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 = el.get('title') if not title and auto and name: title = title_from_name(name) if title: emit_mltext(lines, indent, 'Title', str(title)) # --- 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", } 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) # boolean if type_str == 'boolean': lines.append(f'{indent}xs:boolean') return # string or string(N) m = re.match(r'^string(\((\d+)\))?$', type_str) if m: length = m.group(2) if m.group(2) else '0' lines.append(f'{indent}xs:string') lines.append(f'{indent}') lines.append(f'{indent}\t{length}') lines.append(f'{indent}\tVariable') lines.append(f'{indent}') return # decimal(D,F) or decimal(D,F,nonneg) m = re.match(r'^decimal\((\d+),(\d+)(,nonneg)?\)$', type_str) if m: digits = m.group(1) fraction = m.group(2) sign = 'Nonnegative' if m.group(3) else 'Any' lines.append(f'{indent}xs:decimal') lines.append(f'{indent}') lines.append(f'{indent}\t{digits}') lines.append(f'{indent}\t{fraction}') lines.append(f'{indent}\t{sign}') lines.append(f'{indent}') return # date / dateTime / 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 # DynamicList if type_str == 'DynamicList': lines.append(f'{indent}cfg:DynamicList') return # cfg: references if CFG_REF_PATTERN.match(type_str): lines.append(f'{indent}cfg:{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]}") if '.' 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): if not type_str: lines.append(f'{indent}') return type_string = str(type_str) parts = [p.strip() for p in re.split(r'[|+]', type_string)] lines.append(f'{indent}') 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): # Silent synonyms: model often writes XML name or Russian (ПолеПереключателя/RadioButtonField → radio) for src, dst in ELEMENT_TYPE_SYNONYMS.items(): if src in el and dst not in el: el[dst] = el.pop(src) 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 for p_name in el.keys(): if p_name not in KNOWN_KEYS: 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) eid = new_id() emitters = { 'group': emit_group, 'columnGroup': emit_column_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, } 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 emit_group(lines, el, name, eid, indent): lines.append(f'{indent}') inner = f'{indent}\t' emit_title(lines, el, name, inner) # Group orientation group_val = str(el.get('group', '')) orientation_map = { 'horizontal': 'Horizontal', 'vertical': 'Vertical', 'alwaysHorizontal': 'AlwaysHorizontal', 'alwaysVertical': 'AlwaysVertical', } orientation = orientation_map.get(group_val) if orientation: lines.append(f'{inner}{orientation}') # Behavior if group_val == 'collapsible': lines.append(f'{inner}Vertical') lines.append(f'{inner}Collapsible') 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}') # ShowTitle if el.get('showTitle') is False: lines.append(f'{inner}false') # United if el.get('united') is False: lines.append(f'{inner}false') emit_common_flags(lines, el, inner) # 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) # 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', '')) orientation_map = { 'horizontal': 'Horizontal', 'vertical': 'Vertical', 'inCell': 'InCell', } orientation = orientation_map.get(group_val) if orientation: lines.append(f'{inner}{orientation}') if el.get('showTitle') is False: lines.append(f'{inner}false') if el.get('showInHeader') is not None: sh_val = 'true' if el['showInHeader'] else 'false' lines.append(f'{inner}{sh_val}') if el.get('width'): lines.append(f'{inner}{el["width"]}') emit_common_flags(lines, el, inner) emit_companion(lines, 'ExtendedTooltip', f'{name}РасширеннаяПодсказка', inner) 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 True: lines.append(f'{inner}true') if el.get('passwordMode') is True: lines.append(f'{inner}true') if el.get('choiceButton') is False: lines.append(f'{inner}false') if el.get('clearButton') is True: lines.append(f'{inner}true') if el.get('spinButton') is True: lines.append(f'{inner}true') if el.get('dropListButton') is True: lines.append(f'{inner}true') if el.get('markIncomplete') is True: lines.append(f'{inner}true') if el.get('textEdit') is False: lines.append(f'{inner}false') if el.get('skipOnInput') is True: lines.append(f'{inner}true') if 'autoMaxWidth' in el: if el['autoMaxWidth'] is False: lines.append(f'{inner}false') elif el.get('multiLine') is True: lines.append(f'{inner}false') if el.get('maxWidth') is not None: lines.append(f'{inner}{el["maxWidth"]}') if el.get('autoMaxHeight') is False: lines.append(f'{inner}false') if el.get('maxHeight') is not None: lines.append(f'{inner}{el["maxHeight"]}') if el.get('width'): lines.append(f'{inner}{el["width"]}') if el.get('height'): lines.append(f'{inner}{el["height"]}') if el.get('horizontalStretch') is True: lines.append(f'{inner}true') if el.get('verticalStretch') is True: lines.append(f'{inner}true') if el.get('inputHint'): emit_mltext(lines, inner, 'InputHint', str(el['inputHint'])) # Companions emit_companion(lines, 'ContextMenu', f'{name}\u041a\u043e\u043d\u0442\u0435\u043a\u0441\u0442\u043d\u043e\u0435\u041c\u0435\u043d\u044e', 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) 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) tl = el.get('titleLocation') or 'Right' lines.append(f'{inner}{tl}') # Companions emit_companion(lines, 'ContextMenu', f'{name}\u041a\u043e\u043d\u0442\u0435\u043a\u0441\u0442\u043d\u043e\u0435\u041c\u0435\u043d\u044e', 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) 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) tl_raw = el.get('titleLocation') if tl_raw: loc_map = {'none': 'None', 'left': 'Left', 'right': 'Right', 'top': 'Top', 'bottom': 'Bottom'} tl = loc_map.get(str(tl_raw), str(tl_raw)) else: tl = 'None' lines.append(f'{inner}{tl}') 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"]}') choice_list = el.get('choiceList') or [] if choice_list: lines.append(f'{inner}') item_indent = f'{inner}\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'))) norm = normalize_choice_value(val_raw) if not has_pres: if norm['xsi_type'] == 'xr:DesignTimeRef': tail = norm['text'].split('.')[-1] pres_raw = title_from_name(tail) else: pres_raw = norm['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') lines.append(f'{val_indent}\t{esc_xml(norm["text"])}') lines.append(f'{val_indent}') lines.append(f'{item_indent}') lines.append(f'{inner}') emit_companion(lines, 'ContextMenu', f'{name}КонтекстноеМеню', inner) emit_companion(lines, 'ExtendedTooltip', f'{name}РасширеннаяПодсказка', inner) emit_events(lines, el, name, inner, 'radio') lines.append(f'{indent}') def emit_label(lines, el, name, eid, indent): lines.append(f'{indent}') inner = f'{indent}\t' label_title = el.get('title') or title_from_name(name) if label_title: formatted = 'true' if el.get('hyperlink') is True else 'false' lines.append(f'{inner}') lines.append(f'{inner}\t<v8:item>') lines.append(f'{inner}\t\t<v8:lang>ru</v8:lang>') lines.append(f'{inner}\t\t<v8:content>{esc_xml(str(label_title))}</v8:content>') lines.append(f'{inner}\t</v8:item>') lines.append(f'{inner}') emit_common_flags(lines, el, inner) if el.get('hyperlink') is True: lines.append(f'{inner}true') if el.get('autoMaxWidth') is False: lines.append(f'{inner}false') if el.get('maxWidth') is not None: lines.append(f'{inner}{el["maxWidth"]}') if el.get('autoMaxHeight') is False: lines.append(f'{inner}false') if el.get('maxHeight') is not None: lines.append(f'{inner}{el["maxHeight"]}') if el.get('width'): lines.append(f'{inner}{el["width"]}') if el.get('height'): lines.append(f'{inner}{el["height"]}') # Companions emit_companion(lines, 'ContextMenu', f'{name}\u041a\u043e\u043d\u0442\u0435\u043a\u0441\u0442\u043d\u043e\u0435\u041c\u0435\u043d\u044e', 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) 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('hyperlink') is True: lines.append(f'{inner}true') # Companions emit_companion(lines, 'ContextMenu', f'{name}\u041a\u043e\u043d\u0442\u0435\u043a\u0441\u0442\u043d\u043e\u0435\u041c\u0435\u043d\u044e', 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) emit_events(lines, el, name, inner, 'labelField') lines.append(f'{indent}') def emit_table(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('representation'): lines.append(f'{inner}{el["representation"]}') if el.get('changeRowSet') is True: lines.append(f'{inner}true') if el.get('changeRowOrder') is True: lines.append(f'{inner}true') if el.get('height'): lines.append(f'{inner}{el["height"]}') 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') if el.get('initialTreeView'): lines.append(f'{inner}{el["initialTreeView"]}') if el.get('enableStartDrag') is True: lines.append(f'{inner}true') if el.get('enableDrag') is True: lines.append(f'{inner}true') if el.get('rowPictureDataPath'): lines.append(f'{inner}{el["rowPictureDataPath"]}') # Companions emit_companion(lines, 'ContextMenu', f'{name}\u041a\u043e\u043d\u0442\u0435\u043a\u0441\u0442\u043d\u043e\u0435\u041c\u0435\u043d\u044e', inner) # AutoCommandBar — with optional Autofill control if 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, 'SearchStringAddition', f'{name}\u0421\u0442\u0440\u043e\u043a\u0430\u041f\u043e\u0438\u0441\u043a\u0430', inner) emit_companion(lines, 'ViewStatusAddition', f'{name}\u0421\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0435\u041f\u0440\u043e\u0441\u043c\u043e\u0442\u0440\u0430', inner) emit_companion(lines, 'SearchControlAddition', f'{name}\u0423\u043f\u0440\u0430\u0432\u043b\u0435\u043d\u0438\u0435\u041f\u043e\u0438\u0441\u043a\u043e\u043c', inner) # 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' if el.get('pagesRepresentation'): lines.append(f'{inner}{el["pagesRepresentation"]}') emit_common_flags(lines, el, inner) # 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) 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) if el.get('group'): orientation_map = { 'horizontal': 'Horizontal', 'vertical': 'Vertical', 'alwaysHorizontal': 'AlwaysHorizontal', 'alwaysVertical': 'AlwaysVertical', } orientation = orientation_map.get(str(el['group'])) if orientation: lines.append(f'{inner}{orientation}') # 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) # 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_title(lines, el, name, inner) emit_common_flags(lines, el, inner) if el.get('picture') or el.get('src'): ref = str(el.get('src') or el.get('picture')) lines.append(f'{inner}') lines.append(f'{inner}\t{ref}') lines.append(f'{inner}\ttrue') lines.append(f'{inner}') if el.get('hyperlink') is True: lines.append(f'{inner}true') if el.get('width'): lines.append(f'{inner}{el["width"]}') if el.get('height'): lines.append(f'{inner}{el["height"]}') # Companions emit_companion(lines, 'ContextMenu', f'{name}\u041a\u043e\u043d\u0442\u0435\u043a\u0441\u0442\u043d\u043e\u0435\u041c\u0435\u043d\u044e', 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) 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('width'): lines.append(f'{inner}{el["width"]}') if el.get('height'): lines.append(f'{inner}{el["height"]}') # Companions emit_companion(lines, 'ContextMenu', f'{name}\u041a\u043e\u043d\u0442\u0435\u043a\u0441\u0442\u043d\u043e\u0435\u041c\u0435\u043d\u044e', 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) emit_events(lines, el, name, inner, 'picField') 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) # Companions emit_companion(lines, 'ContextMenu', f'{name}\u041a\u043e\u043d\u0442\u0435\u043a\u0441\u0442\u043d\u043e\u0435\u041c\u0435\u043d\u044e', 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) 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' if el.get('autofill') is True: lines.append(f'{inner}true') emit_common_flags(lines, el, inner) # 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) if el.get('picture'): lines.append(f'{inner}') lines.append(f'{inner}\t{el["picture"]}') lines.append(f'{inner}\ttrue') lines.append(f'{inner}') if el.get('representation'): lines.append(f'{inner}{el["representation"]}') # 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_attributes(lines, attrs, indent): if not attrs or len(attrs) == 0: return lines.append(f'{indent}') for attr in attrs: attr_id = new_id() attr_name = str(attr['name']) lines.append(f'{indent}\t') inner = f'{indent}\t\t' attr_title = attr.get('title') if not attr_title and attr.get('main') is not True: attr_title = title_from_name(attr_name) if attr_title: emit_mltext(lines, inner, 'Title', str(attr_title)) # Type if attr.get('type'): emit_type(lines, str(attr['type']), inner) else: lines.append(f'{inner}') if attr.get('main') is True: lines.append(f'{inner}true') 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) if attr.get('savedData') is True or main_saved: lines.append(f'{inner}true') if attr.get('fillChecking'): lines.append(f'{inner}{attr["fillChecking"]}') # Columns (for ValueTable/ValueTree) if attr.get('columns') and len(attr['columns']) > 0: lines.append(f'{inner}') for col in attr['columns']: col_id = new_id() lines.append(f'{inner}\t') if col.get('title'): emit_mltext(lines, f'{inner}\t\t', 'Title', str(col['title'])) emit_type(lines, str(col.get('type', '')), f'{inner}\t\t') lines.append(f'{inner}\t') lines.append(f'{inner}') # Settings (for DynamicList) if attr.get('settings'): s = attr['settings'] lines.append(f'{inner}') si = f'{inner}\t' if s.get('mainTable'): lines.append(f'{si}{s["mainTable"]}') mq = 'true' if s.get('manualQuery') else 'false' lines.append(f'{si}{mq}') ddr = 'true' if s.get('dynamicDataRead') else 'false' lines.append(f'{si}{ddr}') lines.append(f'{inner}') lines.append(f'{indent}\t') lines.append(f'{indent}') # --- Parameter emitter --- def emit_parameters(lines, params, indent): if not params or len(params) == 0: return lines.append(f'{indent}') for param in params: 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}') for cmd in cmds: cmd_id = new_id() lines.append(f'{indent}\t') inner = f'{indent}\t\t' cmd_title = cmd.get('title') or title_from_name(str(cmd['name'])) if cmd_title: emit_mltext(lines, inner, 'Title', str(cmd_title)) if cmd.get('action'): lines.append(f'{inner}{cmd["action"]}') if cmd.get('shortcut'): lines.append(f'{inner}{cmd["shortcut"]}') if cmd.get('picture'): lines.append(f'{inner}') lines.append(f'{inner}\t{cmd["picture"]}') lines.append(f'{inner}\ttrue') lines.append(f'{inner}') if cmd.get('representation'): lines.append(f'{inner}{cmd["representation"]}') lines.append(f'{indent}\t') lines.append(f'{indent}') # --- 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:] # 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}') 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']+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) 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) # --- 1b. Pre-pass: synonyms, main attribute inference, heuristics, autoCmdBar extraction --- def _normalize_synonyms(el): if not isinstance(el, dict): return synonyms = {'commandBar': 'cmdBar', 'autoCommandBar': 'autoCmdBar'} for src, dst in synonyms.items(): if src in el and dst not in el: el[dst] = el.pop(src) 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: if 'tableAutofill' not in el: el['tableAutofill'] = False if 'commandBarLocation' not in el: el['commandBarLocation'] = 'None' # DefaultPicture доступен только если у DynamicList есть основная таблица if has_main_table and not el.get('rowPictureDataPath'): 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 if isinstance(defn.get('attributes'), list) and isinstance(defn.get('elements'), list): main_attr = next((a for a in defn['attributes'] if isinstance(a, dict) and a.get('main') is True), None) if main_attr and str(main_attr.get('type', '')) == 'DynamicList': settings = main_attr.get('settings') or {} has_mt = bool(isinstance(settings, dict) and settings.get('mainTable')) for el in defn['elements']: _apply_dlist_table_heuristic(el, main_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 lines = [] lines.append('') lines.append(f'
') # 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', str(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') for cmd in defn['excludedCommands']: lines.append(f'\t\t{cmd}') lines.append('\t') # 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) has_inner = bool(acb_halign) or (not acb_autofill) or has_acb_children if has_inner: lines.append(f'\t') if acb_halign: lines.append(f'\t\t{acb_halign}') if not acb_autofill: lines.append('\t\tfalse') if has_acb_children: lines.append('\t\t') for child in main_acb_def['children']: emit_element(lines, child, '\t\t\t', in_cmd_bar=True) lines.append('\t\t') lines.append('\t') else: lines.append(f'\t') # 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') for evt_name, evt_handler in defn['events'].items(): lines.append(f'\t\t{evt_handler}') lines.append('\t') # ChildItems (elements) if defn.get('elements') and len(defn['elements']) > 0: lines.append('\t') for el in defn['elements']: emit_element(lines, el, '\t\t') lines.append('\t') # Attributes emit_attributes(lines, defn.get('attributes'), '\t') # Parameters emit_parameters(lines, defn.get('parameters'), '\t') # Commands emit_commands(lines, defn.get('commands'), '\t') # Close lines.append('') # --- 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_name}
' not in raw_text: # Insert before if '' in raw_text: insert_line = f'\t\t\t
{form_name}
\n' raw_text = raw_text.replace('', insert_line + '\t\t', 1) elif '' in raw_text: replacement = f'\n\t\t\t
{form_name}
\n\t\t
' raw_text = raw_text.replace('', replacement, 1) write_utf8_bom(object_xml_path, raw_text) print(f" Registered:
{form_name}
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()