From 668173121deedcbb25951716a13b2b5fbe49653a Mon Sep 17 00:00:00 2001 From: Nick Shirokov Date: Sat, 13 Jun 2026 13:03:16 +0300 Subject: [PATCH] =?UTF-8?q?feat(form-decompile,form-compile):=20userSettin?= =?UTF-8?q?gPresentation=20=D0=BA=D0=BE=D0=BD=D1=82=D0=B5=D0=B9=D0=BD?= =?UTF-8?q?=D0=B5=D1=80=D0=B0=20ListSettings=20(filter/order/CA)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Контейнер настроек компоновщика (//) может нести собственный — кастомную подпись пользовательской настройки (после userSettingID). Декомпилятор кодировал контейнер только как блок-мету "vu"/"u"/"v" (viewMode/userSettingID), теряя presentation; компилятор не эмитил. Дескриптор listSettings[tag] теперь — строка-код "vu" ИЛИ объект { meta:"vu", presentation:<текст/{ru,en}> }. Декомпилятор: Get-PresByType сохраняет форму по xsi:type (ru-only LocalString ≠ xs:string). Компилятор: новый параметр blockUserSettingPresentation в Emit-Filter/Order/ConditionalAppearance (+ в гейт hasBlockMeta — иначе контейнер только-с-presentation, без items/viewMode/userSettingID, не эмитился). Зеркало py. Корпус 8.3.24: 6 контейнеров-presentation в 6 формах. Выборка 6 форм (ОтветственныеЗаАктуализацию/ЗаПодписание acc+erp, ПравилаФормированияРезервов, СтавкиНДСНоменклатуры): match 0→6, TOTAL→0. ps1==py байт-в-байт. Регресс 43/43. Spec обновлён. Cert: раундтрип (формат платформы, позиция как в оригинале). Co-Authored-By: Claude Opus 4.8 (1M context) --- .../form-compile/scripts/form-compile.ps1 | 30 +- .../form-compile/scripts/form-compile.py | 12619 ++++++++-------- .../form-decompile/scripts/form-decompile.ps1 | 11 +- docs/form-dsl-spec.md | 2 +- 4 files changed, 6344 insertions(+), 6318 deletions(-) diff --git a/.claude/skills/form-compile/scripts/form-compile.ps1 b/.claude/skills/form-compile/scripts/form-compile.ps1 index 3af22bb0..a99fe19a 100644 --- a/.claude/skills/form-compile/scripts/form-compile.ps1 +++ b/.claude/skills/form-compile/scripts/form-compile.ps1 @@ -1,4 +1,4 @@ -# form-compile v1.151 — Compile 1C managed form from JSON or object metadata +# form-compile v1.152 — Compile 1C managed form from JSON or object metadata # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills param( [string]$JsonPath, @@ -1805,9 +1805,9 @@ function Emit-FilterItem { } function Emit-Filter { - param($items, [string]$indent, $blockViewMode = $null, $blockUserSettingID = $null) + param($items, [string]$indent, $blockViewMode = $null, $blockUserSettingID = $null, $blockUserSettingPresentation = $null) $hasItems = $items -and $items.Count -gt 0 - $hasBlockMeta = ($null -ne $blockViewMode) -or ($null -ne $blockUserSettingID) + $hasBlockMeta = ($null -ne $blockViewMode) -or ($null -ne $blockUserSettingID) -or ($null -ne $blockUserSettingPresentation) if (-not $hasItems -and -not $hasBlockMeta) { return } X "$indent" foreach ($item in $items) { @@ -1827,13 +1827,14 @@ function Emit-Filter { $uid = if ("$blockUserSettingID" -eq 'auto') { New-Guid-String } else { "$blockUserSettingID" } X "$indent`t$(Esc-Xml $uid)" } + if ($null -ne $blockUserSettingPresentation) { Emit-USPresentation -val $blockUserSettingPresentation -tag "dcsset:userSettingPresentation" -indent "$indent`t" } X "$indent" } function Emit-Order { - param($items, [string]$indent, [switch]$skipAuto, $blockViewMode = $null, $blockUserSettingID = $null) + param($items, [string]$indent, [switch]$skipAuto, $blockViewMode = $null, $blockUserSettingID = $null, $blockUserSettingPresentation = $null) $hasItems = $items -and $items.Count -gt 0 - $hasBlockMeta = ($null -ne $blockViewMode) -or ($null -ne $blockUserSettingID) + $hasBlockMeta = ($null -ne $blockViewMode) -or ($null -ne $blockUserSettingID) -or ($null -ne $blockUserSettingPresentation) if (-not $hasItems -and -not $hasBlockMeta) { return } X "$indent" foreach ($item in $items) { @@ -1867,6 +1868,7 @@ function Emit-Order { $uid = if ("$blockUserSettingID" -eq 'auto') { New-Guid-String } else { "$blockUserSettingID" } X "$indent`t$(Esc-Xml $uid)" } + if ($null -ne $blockUserSettingPresentation) { Emit-USPresentation -val $blockUserSettingPresentation -tag "dcsset:userSettingPresentation" -indent "$indent`t" } X "$indent" } @@ -1963,9 +1965,9 @@ function Emit-AppearanceValue { } function Emit-ConditionalAppearance { - param($items, [string]$indent, $blockViewMode = $null, $blockUserSettingID = $null, [string]$wrapTag = 'dcsset:conditionalAppearance') + param($items, [string]$indent, $blockViewMode = $null, $blockUserSettingID = $null, [string]$wrapTag = 'dcsset:conditionalAppearance', $blockUserSettingPresentation = $null) $hasItems = $items -and $items.Count -gt 0 - $hasBlockMeta = ($null -ne $blockViewMode) -or ($null -ne $blockUserSettingID) + $hasBlockMeta = ($null -ne $blockViewMode) -or ($null -ne $blockUserSettingID) -or ($null -ne $blockUserSettingPresentation) if (-not $hasItems -and -not $hasBlockMeta) { return } X "$indent<$wrapTag>" foreach ($ca in $items) { @@ -2020,6 +2022,7 @@ function Emit-ConditionalAppearance { $uid = if ("$blockUserSettingID" -eq 'auto') { New-Guid-String } else { "$blockUserSettingID" } X "$indent`t$(Esc-Xml $uid)" } + if ($null -ne $blockUserSettingPresentation) { Emit-USPresentation -val $blockUserSettingPresentation -tag "dcsset:userSettingPresentation" -indent "$indent`t" } X "$indent" } @@ -5727,12 +5730,17 @@ function Emit-Attributes { # Частичная/минимальная форма скелета — эмитим ТОЛЬКО указанные части с их блок-метой. # meta: 'v'=viewMode, 'u'=userSettingID (контейнеры); itemsViewMode/itemsUserSettingID → present. foreach ($prop in $st.listSettings.PSObject.Properties) { - $tag = $prop.Name; $meta = "$($prop.Value)" + $tag = $prop.Name; $pv = $prop.Value + # Значение дескриптора: строка-код "vu" ИЛИ объект { meta:"vu", presentation:<текст/ML> } + # (контейнер несёт собственный userSettingPresentation — кастомную подпись настройки). + if (($pv -is [PSCustomObject]) -or ($pv -is [System.Collections.IDictionary])) { + $meta = "$(Get-Prop $pv 'meta')"; $bpres = Get-Prop $pv 'presentation' + } else { $meta = "$pv"; $bpres = $null } $bvm = if ($meta -match 'v') { 'Normal' } else { $null } switch ($tag) { - 'filter' { $bus = if ($meta -match 'u') { $script:CANON_FILTER_ID } else { $null }; Emit-Filter -items $st.filter -indent $lsi -blockViewMode $bvm -blockUserSettingID $bus } - 'order' { $bus = if ($meta -match 'u') { $script:CANON_ORDER_ID } else { $null }; Emit-Order -items $st.order -indent $lsi -blockViewMode $bvm -blockUserSettingID $bus } - 'conditionalAppearance' { $bus = if ($meta -match 'u') { $script:CANON_CA_ID } else { $null }; Emit-ConditionalAppearance -items $st.conditionalAppearance -indent $lsi -blockViewMode $bvm -blockUserSettingID $bus } + 'filter' { $bus = if ($meta -match 'u') { $script:CANON_FILTER_ID } else { $null }; Emit-Filter -items $st.filter -indent $lsi -blockViewMode $bvm -blockUserSettingID $bus -blockUserSettingPresentation $bpres } + 'order' { $bus = if ($meta -match 'u') { $script:CANON_ORDER_ID } else { $null }; Emit-Order -items $st.order -indent $lsi -blockViewMode $bvm -blockUserSettingID $bus -blockUserSettingPresentation $bpres } + 'conditionalAppearance' { $bus = if ($meta -match 'u') { $script:CANON_CA_ID } else { $null }; Emit-ConditionalAppearance -items $st.conditionalAppearance -indent $lsi -blockViewMode $bvm -blockUserSettingID $bus -blockUserSettingPresentation $bpres } 'itemsViewMode' { X "$lsiNormal" } 'itemsUserSettingID' { X "$lsi$($script:CANON_ITEMS_ID)" } 'structure' { Emit-ListGrouping (Get-ListGroupingValue $st) $lsi } diff --git a/.claude/skills/form-compile/scripts/form-compile.py b/.claude/skills/form-compile/scripts/form-compile.py index 8b122b96..61c34e17 100644 --- a/.claude/skills/form-compile/scripts/form-compile.py +++ b/.claude/skills/form-compile/scripts/form-compile.py @@ -1,6304 +1,6315 @@ -#!/usr/bin/env python3 -# form-compile v1.151 — 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 - - # (ChoiceButton у ref-полей платформа выводит сама; компилятор эмитит true по StartChoice-эвристике. - # Явный choiceButton из декомпиляции эмитится verbatim. Дефолт-«true» здесь НЕ ставим, чтобы - # from-object вывод совпадал с сертифицированным и не плодил ChoiceButton на каждом ref-поле.) - - # Extra props - if extra_props: - for k in extra_props: - el[k] = extra_props[k] - - return el - - -# --- Catalog DSL generators --- - -def generate_catalog_dsl(meta, preset_data, purpose): - purpose_key = f"catalog.{purpose.lower()}" - p = preset_data.get(purpose_key, {}) - fd = p.get('fieldDefaults', {}) - - dispatch = { - 'Folder': lambda: generate_catalog_folder_dsl(meta, p), - 'List': lambda: generate_catalog_list_dsl(meta, p), - 'Choice': lambda: generate_catalog_choice_dsl(meta, p, preset_data), - 'Item': lambda: generate_catalog_item_dsl(meta, p, fd), - } - return dispatch[purpose]() - - -def generate_catalog_folder_dsl(meta, p): - elements = [] - # Code (if CodeLength > 0) - if meta.get('CodeLength', 0) > 0: - elements.append(OrderedDict([('input', '\u041a\u043e\u0434'), ('path', '\u041e\u0431\u044a\u0435\u043a\u0442.Code')])) - # Description - elements.append(OrderedDict([('input', '\u041d\u0430\u0438\u043c\u0435\u043d\u043e\u0432\u0430\u043d\u0438\u0435'), ('path', '\u041e\u0431\u044a\u0435\u043a\u0442.Description')])) - # Parent - parent_title = p.get('parent', {}).get('title') - parent_el = OrderedDict([('input', '\u0420\u043e\u0434\u0438\u0442\u0435\u043b\u044c'), ('path', '\u041e\u0431\u044a\u0435\u043a\u0442.Parent')]) - if parent_title: - parent_el['title'] = parent_title - elements.append(parent_el) - - props = OrderedDict([('windowOpeningMode', 'LockOwnerWindow')]) - if p.get('properties'): - for k in p['properties']: - props[k] = p['properties'][k] - - form_props = OrderedDict([('useForFoldersAndItems', 'Folders')]) - for k in props: - form_props[k] = props[k] - - return OrderedDict([ - ('title', meta['Synonym']), - ('properties', form_props), - ('elements', elements), - ('attributes', [ - OrderedDict([('name', '\u041e\u0431\u044a\u0435\u043a\u0442'), ('type', f"CatalogObject.{meta['Name']}"), ('main', True)]) - ]), - ]) - - -def generate_catalog_list_dsl(meta, p): - columns = [] - # Description always first - columns.append(OrderedDict([('labelField', '\u041d\u0430\u0438\u043c\u0435\u043d\u043e\u0432\u0430\u043d\u0438\u0435'), ('path', '\u0421\u043f\u0438\u0441\u043e\u043a.Description')])) - # Code if present - if meta.get('CodeLength', 0) > 0: - columns.append(OrderedDict([('labelField', '\u041a\u043e\u0434'), ('path', '\u0421\u043f\u0438\u0441\u043e\u043a.Code')])) - # Custom attributes - for attr in meta['Attributes']: - if not is_displayable_type(attr['Type']): - continue - columns.append(OrderedDict([('labelField', attr['Name']), ('path', f"\u0421\u043f\u0438\u0441\u043e\u043a.{attr['Name']}")])) - # Hidden ref - if p.get('hiddenRef', True) is not False: - columns.append(OrderedDict([('labelField', '\u0421\u0441\u044b\u043b\u043a\u0430'), ('path', '\u0421\u043f\u0438\u0441\u043e\u043a.Ref'), ('userVisible', False)])) - - table_el = OrderedDict([ - ('table', '\u0421\u043f\u0438\u0441\u043e\u043a'), ('path', '\u0421\u043f\u0438\u0441\u043e\u043a'), - ('rowPictureDataPath', '\u0421\u043f\u0438\u0441\u043e\u043a.DefaultPicture'), - ('commandBarLocation', 'None'), - ('tableAutofill', False), - ('columns', columns), - ]) - # Hierarchical properties - if meta.get('Hierarchical'): - table_el['initialTreeView'] = 'ExpandTopLevel' - table_el['enableStartDrag'] = True - table_el['enableDrag'] = True - - form_props = OrderedDict() - if p.get('properties'): - for k in p['properties']: - form_props[k] = p['properties'][k] - - return OrderedDict([ - ('title', meta['Synonym']), - ('properties', form_props), - ('elements', [table_el]), - ('attributes', [ - OrderedDict([ - ('name', '\u0421\u043f\u0438\u0441\u043e\u043a'), ('type', 'DynamicList'), ('main', True), - ('settings', OrderedDict([('mainTable', f"Catalog.{meta['Name']}"), ('dynamicDataRead', True)])), - ]) - ]), - ]) - - -def generate_catalog_choice_dsl(meta, p, preset_data): - # Start from list - list_key = 'catalog.list' - lp = preset_data.get(list_key, {}) - dsl = generate_catalog_list_dsl(meta, lp) - - # Add choice-specific properties - dsl['properties']['windowOpeningMode'] = 'LockOwnerWindow' - if p.get('properties'): - for k in p['properties']: - dsl['properties'][k] = p['properties'][k] - - # Set ChoiceMode on table - dsl['elements'][0]['choiceMode'] = True - - return dsl - - -def generate_catalog_item_dsl(meta, p, fd): - header_children = [] - - # Owner (if subordinate) - if meta.get('Owners') and len(meta['Owners']) > 0: - owner_el = OrderedDict([('input', '\u0412\u043b\u0430\u0434\u0435\u043b\u0435\u0446'), ('path', '\u041e\u0431\u044a\u0435\u043a\u0442.Owner'), ('readOnly', True)]) - header_children.append(owner_el) - - # Code + Description - cd_layout = (p.get('codeDescription') or {}).get('layout', 'horizontal') - cd_order = (p.get('codeDescription') or {}).get('order', 'descriptionFirst') - has_code = meta.get('CodeLength', 0) > 0 - - if cd_layout == 'horizontal' and has_code: - cd_children = [] - desc_el = OrderedDict([('input', '\u041d\u0430\u0438\u043c\u0435\u043d\u043e\u0432\u0430\u043d\u0438\u0435'), ('path', '\u041e\u0431\u044a\u0435\u043a\u0442.Description')]) - code_el = OrderedDict([('input', '\u041a\u043e\u0434'), ('path', '\u041e\u0431\u044a\u0435\u043a\u0442.Code')]) - if cd_order == 'descriptionFirst': - cd_children = [desc_el, code_el] - else: - cd_children = [code_el, desc_el] - header_children.append(OrderedDict([ - ('group', 'horizontal'), ('name', '\u0413\u0440\u0443\u043f\u043f\u0430\u041a\u043e\u0434\u041d\u0430\u0438\u043c\u0435\u043d\u043e\u0432\u0430\u043d\u0438\u0435'), ('showTitle', False), - ('representation', 'none'), ('children', cd_children), - ])) - else: - # Vertical or no code - header_children.append(OrderedDict([('input', '\u041d\u0430\u0438\u043c\u0435\u043d\u043e\u0432\u0430\u043d\u0438\u0435'), ('path', '\u041e\u0431\u044a\u0435\u043a\u0442.Description')])) - if has_code: - header_children.append(OrderedDict([('input', '\u041a\u043e\u0434'), ('path', '\u041e\u0431\u044a\u0435\u043a\u0442.Code')])) - - # Parent (for hierarchical catalogs) - parent_pos = (p.get('parent') or {}).get('position', 'afterCodeDescription') - parent_title = (p.get('parent') or {}).get('title') - if meta.get('Hierarchical'): - parent_el = OrderedDict([('input', '\u0420\u043e\u0434\u0438\u0442\u0435\u043b\u044c'), ('path', '\u041e\u0431\u044a\u0435\u043a\u0442.Parent')]) - if parent_title: - parent_el['title'] = parent_title - if parent_pos == 'beforeCodeDescription': - insert_idx = 1 if (meta.get('Owners') and len(meta['Owners']) > 0) else 0 - header_children.insert(insert_idx, parent_el) - else: - # afterCodeDescription (default) - header_children.append(parent_el) - - # Custom attributes -> header - footer_field_names = (p.get('footer') or {}).get('fields', []) - - for attr in meta['Attributes']: - if attr['Name'] in footer_field_names: - continue - if not is_displayable_type(attr['Type']): - continue - header_children.append(new_field_element(attr['Name'], f"\u041e\u0431\u044a\u0435\u043a\u0442.{attr['Name']}", attr['Type'], fd)) - - # Build root elements - root_elements = [] - - # ГруппаШапка - root_elements.append(OrderedDict([ - ('group', 'vertical'), ('name', '\u0413\u0440\u0443\u043f\u043f\u0430\u0428\u0430\u043f\u043a\u0430'), ('showTitle', False), - ('representation', 'none'), ('children', header_children), - ])) - - # Tabular sections - ts_exclude = ['\u0414\u043e\u043f\u043e\u043b\u043d\u0438\u0442\u0435\u043b\u044c\u043d\u044b\u0435\u0420\u0435\u043a\u0432\u0438\u0437\u0438\u0442\u044b', '\u041f\u0440\u0435\u0434\u0441\u0442\u0430\u0432\u043b\u0435\u043d\u0438\u044f'] - if (p.get('tabularSections') or {}).get('exclude'): - ts_exclude = p['tabularSections']['exclude'] - ts_line_number = (p.get('tabularSections') or {}).get('lineNumber', True) - - visible_ts = [ts for ts in meta['TabularSections'] if ts['Name'] not in ts_exclude] - - for ts in visible_ts: - ts_cols = [] - if ts_line_number: - ts_cols.append(OrderedDict([('labelField', f"{ts['Name']}\u041d\u043e\u043c\u0435\u0440\u0421\u0442\u0440\u043e\u043a\u0438"), ('path', f"\u041e\u0431\u044a\u0435\u043a\u0442.{ts['Name']}.LineNumber")])) - for col in ts['Columns']: - ts_cols.append(new_field_element(f"{ts['Name']}{col['Name']}", f"\u041e\u0431\u044a\u0435\u043a\u0442.{ts['Name']}.{col['Name']}", col['Type'], fd)) - root_elements.append(OrderedDict([('table', ts['Name']), ('path', f"\u041e\u0431\u044a\u0435\u043a\u0442.{ts['Name']}"), ('columns', ts_cols)])) - - # Footer fields - for fn in footer_field_names: - f_attr = next((a for a in meta['Attributes'] if a['Name'] == fn), None) - if f_attr: - root_elements.append(new_field_element(f_attr['Name'], f"\u041e\u0431\u044a\u0435\u043a\u0442.{f_attr['Name']}", f_attr['Type'], fd)) - - # BSP group - bsp_group = (p.get('additional') or {}).get('bspGroup', True) - if bsp_group: - root_elements.append(OrderedDict([('group', 'vertical'), ('name', '\u0413\u0440\u0443\u043f\u043f\u0430\u0414\u043e\u043f\u043e\u043b\u043d\u0438\u0442\u0435\u043b\u044c\u043d\u044b\u0435\u0420\u0435\u043a\u0432\u0438\u0437\u0438\u0442\u044b')])) - - # Properties - form_props = OrderedDict() - if p.get('properties'): - for k in p['properties']: - form_props[k] = p['properties'][k] - # UseForFoldersAndItems - if meta.get('Hierarchical') and meta.get('HierarchyType') == 'HierarchyFoldersAndItems': - form_props['useForFoldersAndItems'] = 'Items' - - return OrderedDict([ - ('title', meta['Synonym']), - ('properties', form_props), - ('elements', root_elements), - ('attributes', [ - OrderedDict([('name', '\u041e\u0431\u044a\u0435\u043a\u0442'), ('type', f"CatalogObject.{meta['Name']}"), ('main', True)]) - ]), - ]) - - -# --- Document DSL generators --- - -def generate_document_dsl(meta, preset_data, purpose): - purpose_key = f"document.{purpose.lower()}" - p = preset_data.get(purpose_key, {}) - fd = p.get('fieldDefaults', {}) - - dispatch = { - 'List': lambda: generate_document_list_dsl(meta, p), - 'Choice': lambda: generate_document_choice_dsl(meta, p, preset_data), - 'Item': lambda: generate_document_item_dsl(meta, p, fd), - } - return dispatch[purpose]() - - -def generate_document_list_dsl(meta, p): - columns = [] - # Standard columns: Number + Date - columns.append(OrderedDict([('labelField', 'Номер'), ('path', 'Список.Number')])) - columns.append(OrderedDict([('labelField', 'Дата'), ('path', 'Список.Date')])) - # All custom attributes as labelField - for attr in meta['Attributes']: - if not is_displayable_type(attr['Type']): - continue - columns.append(OrderedDict([('labelField', attr['Name']), ('path', f"\u0421\u043f\u0438\u0441\u043e\u043a.{attr['Name']}")])) - # Hidden ref - if p.get('hiddenRef', True): - columns.append(OrderedDict([('labelField', '\u0421\u0441\u044b\u043b\u043a\u0430'), ('path', '\u0421\u043f\u0438\u0441\u043e\u043a.Ref'), ('userVisible', False)])) - - table_el = OrderedDict([ - ('table', '\u0421\u043f\u0438\u0441\u043e\u043a'), ('path', '\u0421\u043f\u0438\u0441\u043e\u043a'), - ('rowPictureDataPath', '\u0421\u043f\u0438\u0441\u043e\u043a.DefaultPicture'), - ('commandBarLocation', 'None'), - ('tableAutofill', False), - ('columns', columns), - ]) - - form_props = OrderedDict() - if p.get('properties'): - for k in p['properties']: - form_props[k] = p['properties'][k] - - return OrderedDict([ - ('title', meta['Synonym']), - ('properties', form_props), - ('elements', [table_el]), - ('attributes', [ - OrderedDict([ - ('name', '\u0421\u043f\u0438\u0441\u043e\u043a'), ('type', 'DynamicList'), ('main', True), - ('settings', OrderedDict([('mainTable', f"Document.{meta['Name']}"), ('dynamicDataRead', True)])), - ]) - ]), - ]) - - -def generate_document_choice_dsl(meta, p, preset_data): - list_key = 'document.list' - lp = preset_data.get(list_key, {}) - dsl = generate_document_list_dsl(meta, lp) - - dsl['properties']['windowOpeningMode'] = 'LockOwnerWindow' - if p.get('properties'): - for k in p['properties']: - dsl['properties'][k] = p['properties'][k] - - return dsl - - -def generate_document_item_dsl(meta, p, fd): - header_pos = (p.get('header') or {}).get('position', 'insidePage') - header_layout = (p.get('header') or {}).get('layout', '2col') - header_distribute = (p.get('header') or {}).get('distribute', 'even') - date_title = (p.get('header') or {}).get('dateTitle', '\u043e\u0442') - - footer_fields = (p.get('footer') or {}).get('fields', []) - footer_pos = (p.get('footer') or {}).get('position', 'insidePage') - - add_pos = (p.get('additional') or {}).get('position', 'page') - add_layout = (p.get('additional') or {}).get('layout', '2col') - add_bsp_group = (p.get('additional') or {}).get('bspGroup', True) - add_left = (p.get('additional') or {}).get('left', []) - add_right = (p.get('additional') or {}).get('right', []) - - header_right = (p.get('header') or {}).get('right', []) - - ts_exclude = ['\u0414\u043e\u043f\u043e\u043b\u043d\u0438\u0442\u0435\u043b\u044c\u043d\u044b\u0435\u0420\u0435\u043a\u0432\u0438\u0437\u0438\u0442\u044b'] - if (p.get('tabularSections') or {}).get('exclude'): - ts_exclude = p['tabularSections']['exclude'] - ts_line_number = (p.get('tabularSections') or {}).get('lineNumber', True) - - # Classify attributes - claimed = {} - for fn in footer_fields: - claimed[fn] = 'footer' - for fn in header_right: - claimed[fn] = 'header.right' - for fn in add_left: - claimed[fn] = 'additional.left' - for fn in add_right: - claimed[fn] = 'additional.right' - - unclaimed = [attr for attr in meta['Attributes'] if attr['Name'] not in claimed and is_displayable_type(attr['Type'])] - - # Distribute unclaimed - left_attrs = [] - right_extra_attrs = [] - if header_distribute == 'left': - left_attrs = unclaimed - elif header_distribute == 'right': - right_extra_attrs = unclaimed - else: # "even" - import math - half = math.ceil(len(unclaimed) / 2) if unclaimed else 0 - left_attrs = unclaimed[:half] - right_extra_attrs = unclaimed[half:] - - # Build ГруппаНомерДата - num_date_children = [ - OrderedDict([('input', '\u041d\u043e\u043c\u0435\u0440'), ('path', '\u041e\u0431\u044a\u0435\u043a\u0442.Number'), ('autoMaxWidth', False), ('width', 9)]), - OrderedDict([('input', '\u0414\u0430\u0442\u0430'), ('path', '\u041e\u0431\u044a\u0435\u043a\u0442.Date'), ('title', date_title)]), - ] - num_date_group = OrderedDict([ - ('group', 'horizontal'), ('name', '\u0413\u0440\u0443\u043f\u043f\u0430\u041d\u043e\u043c\u0435\u0440\u0414\u0430\u0442\u0430'), ('showTitle', False), ('children', num_date_children), - ]) - - # Build left column - left_children = [num_date_group] - for attr in left_attrs: - left_children.append(new_field_element(attr['Name'], f"\u041e\u0431\u044a\u0435\u043a\u0442.{attr['Name']}", attr['Type'], fd)) - - # Build right column - right_children = [] - for rn in header_right: - r_attr = next((a for a in meta['Attributes'] if a['Name'] == rn), None) - if r_attr: - right_children.append(new_field_element(r_attr['Name'], f"\u041e\u0431\u044a\u0435\u043a\u0442.{r_attr['Name']}", r_attr['Type'], fd)) - for attr in right_extra_attrs: - right_children.append(new_field_element(attr['Name'], f"\u041e\u0431\u044a\u0435\u043a\u0442.{attr['Name']}", attr['Type'], fd)) - - # Header group - if header_layout == '2col' and len(right_children) > 0: - header_group = OrderedDict([ - ('group', 'horizontal'), ('name', '\u0413\u0440\u0443\u043f\u043f\u0430\u0428\u0430\u043f\u043a\u0430'), ('showTitle', False), ('representation', 'none'), - ('children', [ - OrderedDict([('group', 'vertical'), ('name', '\u0413\u0440\u0443\u043f\u043f\u0430\u0428\u0430\u043f\u043a\u0430\u041b\u0435\u0432\u043e'), ('showTitle', False), ('children', left_children)]), - OrderedDict([('group', 'vertical'), ('name', '\u0413\u0440\u0443\u043f\u043f\u0430\u0428\u0430\u043f\u043a\u0430\u041f\u0440\u0430\u0432\u043e'), ('showTitle', False), ('children', right_children)]), - ]), - ]) - else: - # 1col or no right items - all_header_fields = left_children + right_children - header_group = OrderedDict([ - ('group', 'horizontal'), ('name', '\u0413\u0440\u0443\u043f\u043f\u0430\u0428\u0430\u043f\u043a\u0430'), ('showTitle', False), ('representation', 'none'), - ('children', [ - OrderedDict([('group', 'vertical'), ('name', '\u0413\u0440\u0443\u043f\u043f\u0430\u0428\u0430\u043f\u043a\u0430\u041b\u0435\u0432\u043e'), ('showTitle', False), ('children', all_header_fields)]), - ]), - ]) - - # Footer elements - footer_elements = [] - for fn in footer_fields: - f_attr = next((a for a in meta['Attributes'] if a['Name'] == fn), None) - if f_attr: - footer_elements.append(new_field_element(f_attr['Name'], f"\u041e\u0431\u044a\u0435\u043a\u0442.{f_attr['Name']}", f_attr['Type'], fd)) - - # Visible tabular sections - visible_ts = [ts for ts in meta['TabularSections'] if ts['Name'] not in ts_exclude] - - # Additional page content - additional_page = None - if add_pos == 'page': - add_left_els = [] - add_right_els = [] - for aln in add_left: - al_attr = next((a for a in meta['Attributes'] if a['Name'] == aln), None) - if al_attr: - add_left_els.append(new_field_element(al_attr['Name'], f"\u041e\u0431\u044a\u0435\u043a\u0442.{al_attr['Name']}", al_attr['Type'], fd)) - for arn in add_right: - ar_attr = next((a for a in meta['Attributes'] if a['Name'] == arn), None) - if ar_attr: - add_right_els.append(new_field_element(ar_attr['Name'], f"\u041e\u0431\u044a\u0435\u043a\u0442.{ar_attr['Name']}", ar_attr['Type'], fd)) - add_page_children = [] - if add_layout == '2col': - add_page_children.append(OrderedDict([ - ('group', 'horizontal'), ('name', '\u0413\u0440\u0443\u043f\u043f\u0430\u041f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b'), ('showTitle', False), - ('children', [ - OrderedDict([('group', 'vertical'), ('name', '\u0413\u0440\u0443\u043f\u043f\u0430\u041f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b\u041b\u0435\u0432\u043e'), ('showTitle', False), ('children', add_left_els)]), - OrderedDict([('group', 'vertical'), ('name', '\u0413\u0440\u0443\u043f\u043f\u0430\u041f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b\u041f\u0440\u0430\u0432\u043e'), ('showTitle', False), ('children', add_right_els)]), - ]), - ])) - else: - add_page_children.extend(add_left_els + add_right_els) - if add_bsp_group: - add_page_children.append(OrderedDict([('group', 'vertical'), ('name', '\u0413\u0440\u0443\u043f\u043f\u0430\u0414\u043e\u043f\u043e\u043b\u043d\u0438\u0442\u0435\u043b\u044c\u043d\u044b\u0435\u0420\u0435\u043a\u0432\u0438\u0437\u0438\u0442\u044b')])) - additional_page = OrderedDict([('page', '\u0413\u0440\u0443\u043f\u043f\u0430\u0414\u043e\u043f\u043e\u043b\u043d\u0438\u0442\u0435\u043b\u044c\u043d\u043e'), ('title', '\u0414\u043e\u043f\u043e\u043b\u043d\u0438\u0442\u0435\u043b\u044c\u043d\u043e'), ('children', add_page_children)]) - - # Build TS page elements - ts_pages = [] - for ts in visible_ts: - ts_cols = [] - if ts_line_number: - ts_cols.append(OrderedDict([('labelField', f"{ts['Name']}\u041d\u043e\u043c\u0435\u0440\u0421\u0442\u0440\u043e\u043a\u0438"), ('path', f"\u041e\u0431\u044a\u0435\u043a\u0442.{ts['Name']}.LineNumber")])) - for col in ts['Columns']: - ts_cols.append(new_field_element(f"{ts['Name']}{col['Name']}", f"\u041e\u0431\u044a\u0435\u043a\u0442.{ts['Name']}.{col['Name']}", col['Type'], fd)) - ts_pages.append(OrderedDict([ - ('page', f"\u0413\u0440\u0443\u043f\u043f\u0430{ts['Name']}"), ('title', ts['Synonym']), - ('children', [ - OrderedDict([('table', ts['Name']), ('path', f"\u041e\u0431\u044a\u0435\u043a\u0442.{ts['Name']}"), ('columns', ts_cols)]) - ]), - ])) - - # Assemble root elements - root_elements = [] - - if len(visible_ts) == 0: - # Simple form - no Pages - root_elements.append(header_group) - if footer_elements: - root_elements.extend(footer_elements) - if add_bsp_group and add_pos != 'none': - root_elements.append(OrderedDict([('group', 'vertical'), ('name', '\u0413\u0440\u0443\u043f\u043f\u0430\u0414\u043e\u043f\u043e\u043b\u043d\u0438\u0442\u0435\u043b\u044c\u043d\u044b\u0435\u0420\u0435\u043a\u0432\u0438\u0437\u0438\u0442\u044b')])) - else: - # Pages form - if header_pos == 'abovePages': - root_elements.append(header_group) - pages_children = list(ts_pages) - if additional_page: - pages_children.append(additional_page) - root_elements.append(OrderedDict([('pages', '\u0413\u0440\u0443\u043f\u043f\u0430\u0421\u0442\u0440\u0430\u043d\u0438\u0446\u044b'), ('children', pages_children)])) - else: - # insidePage (default) - osnovnoe_children = [header_group] - if footer_pos == 'insidePage' and footer_elements: - osnovnoe_children.extend(footer_elements) - pages_children = [] - pages_children.append(OrderedDict([('page', '\u0413\u0440\u0443\u043f\u043f\u0430\u041e\u0441\u043d\u043e\u0432\u043d\u043e\u0435'), ('title', '\u041e\u0441\u043d\u043e\u0432\u043d\u043e\u0435'), ('children', osnovnoe_children)])) - pages_children.extend(ts_pages) - if additional_page: - pages_children.append(additional_page) - root_elements.append(OrderedDict([('pages', '\u0413\u0440\u0443\u043f\u043f\u0430\u0421\u0442\u0440\u0430\u043d\u0438\u0446\u044b'), ('children', pages_children)])) - - # Footer below pages - if footer_pos == 'belowPages' and footer_elements: - root_elements.extend(footer_elements) - - # Properties - form_props = OrderedDict([('autoTitle', False)]) - if p.get('properties'): - for k in p['properties']: - form_props[k] = p['properties'][k] - - return OrderedDict([ - ('title', meta['Synonym']), - ('properties', form_props), - ('elements', root_elements), - ('attributes', [ - OrderedDict([('name', '\u041e\u0431\u044a\u0435\u043a\u0442'), ('type', f"DocumentObject.{meta['Name']}"), ('main', True)]) - ]), - ]) - - -# --- InformationRegister DSL generators --- - -def generate_information_register_dsl(meta, preset_data, purpose): - p_key = f"informationRegister.{purpose.lower()}" - p = preset_data.get(p_key, {}) - fd = p.get('fieldDefaults') or {'ref': {'choiceButton': True}, 'boolean': {'element': 'check'}} - dispatch = { - 'Record': lambda: generate_information_register_record_dsl(meta, p, fd), - 'List': lambda: generate_information_register_list_dsl(meta, p), - } - return dispatch[purpose]() - - -def generate_information_register_record_dsl(meta, p, fd): - elements = OrderedDict() - is_periodic = meta.get('Periodicity') and meta['Periodicity'] != 'Nonperiodical' - - # Period first (if periodic) - if is_periodic: - elements['\u041f\u0435\u0440\u0438\u043e\u0434'] = {'element': 'input', 'path': '\u0417\u0430\u043f\u0438\u0441\u044c.Period'} - # Dimensions - for dim in meta.get('Dimensions', []): - if not is_displayable_type(dim['Type']): - continue - elements[dim['Name']] = new_field_element(dim['Name'], f"\u0417\u0430\u043f\u0438\u0441\u044c.{dim['Name']}", dim['Type'], fd) - # Resources - for res in meta.get('Resources', []): - if not is_displayable_type(res['Type']): - continue - elements[res['Name']] = new_field_element(res['Name'], f"\u0417\u0430\u043f\u0438\u0441\u044c.{res['Name']}", res['Type'], fd) - # Attributes - for attr in meta['Attributes']: - if not is_displayable_type(attr['Type']): - continue - elements[attr['Name']] = new_field_element(attr['Name'], f"\u0417\u0430\u043f\u0438\u0441\u044c.{attr['Name']}", attr['Type'], fd) - - props = OrderedDict([('windowOpeningMode', 'LockOwnerWindow')]) - if p.get('properties'): - for k in p['properties']: - props[k] = p['properties'][k] - - return OrderedDict([ - ('title', meta['Synonym']), - ('properties', props), - ('elements', elements), - ('attributes', [ - {'name': '\u0417\u0430\u043f\u0438\u0441\u044c', 'type': f"InformationRegisterRecordManager.{meta['Name']}", 'main': True, 'savedData': True} - ]), - ]) - - -def generate_information_register_list_dsl(meta, p): - is_periodic = meta.get('Periodicity') and meta['Periodicity'] != 'Nonperiodical' - is_recorder_subordinate = meta.get('WriteMode') == 'RecorderSubordinate' - - columns_list = [] - # Period - if is_periodic: - columns_list.append(OrderedDict([('labelField', '\u041f\u0435\u0440\u0438\u043e\u0434'), ('path', '\u0421\u043f\u0438\u0441\u043e\u043a.Period')])) - # Recorder/LineNumber for subordinate registers - if is_recorder_subordinate: - columns_list.append(OrderedDict([('labelField', '\u0420\u0435\u0433\u0438\u0441\u0442\u0440\u0430\u0442\u043e\u0440'), ('path', '\u0421\u043f\u0438\u0441\u043e\u043a.Recorder')])) - columns_list.append(OrderedDict([('labelField', '\u041d\u043e\u043c\u0435\u0440\u0421\u0442\u0440\u043e\u043a\u0438'), ('path', '\u0421\u043f\u0438\u0441\u043e\u043a.LineNumber')])) - # Dimensions - for dim in meta.get('Dimensions', []): - if not is_displayable_type(dim['Type']): - continue - columns_list.append(OrderedDict([('labelField', dim['Name']), ('path', f"\u0421\u043f\u0438\u0441\u043e\u043a.{dim['Name']}")])) - # Resources - for res in meta.get('Resources', []): - if not is_displayable_type(res['Type']): - continue - el_key = 'check' if re.match(r'^xs:boolean$|^Boolean$', res['Type']) else 'labelField' - columns_list.append(OrderedDict([(el_key, res['Name']), ('path', f"\u0421\u043f\u0438\u0441\u043e\u043a.{res['Name']}")])) - # Attributes - for attr in meta['Attributes']: - if not is_displayable_type(attr['Type']): - continue - el_key = 'check' if re.match(r'^xs:boolean$|^Boolean$', attr['Type']) else 'labelField' - columns_list.append(OrderedDict([(el_key, attr['Name']), ('path', f"\u0421\u043f\u0438\u0441\u043e\u043a.{attr['Name']}")])) - - table_el = OrderedDict([ - ('table', '\u0421\u043f\u0438\u0441\u043e\u043a'), - ('path', '\u0421\u043f\u0438\u0441\u043e\u043a'), - ('rowPictureDataPath', '\u0421\u043f\u0438\u0441\u043e\u043a.DefaultPicture'), - ('commandBarLocation', 'None'), - ('tableAutofill', False), - ('columns', columns_list), - ]) - - props = OrderedDict() - if p.get('properties'): - for k in p['properties']: - props[k] = p['properties'][k] - - return OrderedDict([ - ('title', meta['Synonym']), - ('properties', props), - ('elements', [table_el]), - ('attributes', [ - {'name': '\u0421\u043f\u0438\u0441\u043e\u043a', 'type': 'DynamicList', 'main': True, 'settings': {'mainTable': f"InformationRegister.{meta['Name']}", 'dynamicDataRead': True}} - ]), - ]) - - -# --- AccumulationRegister DSL generators --- - -def generate_accumulation_register_dsl(meta, preset_data, purpose): - p_key = f"accumulationRegister.{purpose.lower()}" - p = preset_data.get(p_key, {}) - dispatch = { - 'List': lambda: generate_accumulation_register_list_dsl(meta, p), - } - return dispatch[purpose]() - - -def generate_accumulation_register_list_dsl(meta, p): - columns_list = [] - # AccumulationRegisters always have Period, Recorder, LineNumber - columns_list.append(OrderedDict([('labelField', '\u041f\u0435\u0440\u0438\u043e\u0434'), ('path', '\u0421\u043f\u0438\u0441\u043e\u043a.Period')])) - columns_list.append(OrderedDict([('labelField', '\u0420\u0435\u0433\u0438\u0441\u0442\u0440\u0430\u0442\u043e\u0440'), ('path', '\u0421\u043f\u0438\u0441\u043e\u043a.Recorder')])) - columns_list.append(OrderedDict([('labelField', '\u041d\u043e\u043c\u0435\u0440\u0421\u0442\u0440\u043e\u043a\u0438'), ('path', '\u0421\u043f\u0438\u0441\u043e\u043a.LineNumber')])) - # Dimensions - for dim in meta.get('Dimensions', []): - if not is_displayable_type(dim['Type']): - continue - columns_list.append(OrderedDict([('labelField', dim['Name']), ('path', f"\u0421\u043f\u0438\u0441\u043e\u043a.{dim['Name']}")])) - # Resources - for res in meta.get('Resources', []): - if not is_displayable_type(res['Type']): - continue - el_key = 'check' if re.match(r'^xs:boolean$|^Boolean$', res['Type']) else 'labelField' - columns_list.append(OrderedDict([(el_key, res['Name']), ('path', f"\u0421\u043f\u0438\u0441\u043e\u043a.{res['Name']}")])) - # Attributes - for attr in meta['Attributes']: - if not is_displayable_type(attr['Type']): - continue - el_key = 'check' if re.match(r'^xs:boolean$|^Boolean$', attr['Type']) else 'labelField' - columns_list.append(OrderedDict([(el_key, attr['Name']), ('path', f"\u0421\u043f\u0438\u0441\u043e\u043a.{attr['Name']}")])) - - table_el = OrderedDict([ - ('table', '\u0421\u043f\u0438\u0441\u043e\u043a'), - ('path', '\u0421\u043f\u0438\u0441\u043e\u043a'), - ('rowPictureDataPath', '\u0421\u043f\u0438\u0441\u043e\u043a.DefaultPicture'), - ('commandBarLocation', 'None'), - ('tableAutofill', False), - ('columns', columns_list), - ]) - - props = OrderedDict() - if p.get('properties'): - for k in p['properties']: - props[k] = p['properties'][k] - - return OrderedDict([ - ('title', meta['Synonym']), - ('properties', props), - ('elements', [table_el]), - ('attributes', [ - {'name': '\u0421\u043f\u0438\u0441\u043e\u043a', 'type': 'DynamicList', 'main': True, 'settings': {'mainTable': f"AccumulationRegister.{meta['Name']}", 'dynamicDataRead': True}} - ]), - ]) - - -# --- ChartOfCharacteristicTypes (delegates to Catalog) --- - -def generate_chart_of_characteristic_types_dsl(meta, preset_data, purpose): - # Delegate to Catalog generators -- meta already has CodeLength, DescriptionLength, etc. - dsl = generate_catalog_dsl(meta, preset_data, purpose) - - # Post-patch: replace Catalog types with ChartOfCharacteristicTypes types - cat_obj_type = f"CatalogObject.{meta['Name']}" - ccoct_obj_type = f"ChartOfCharacteristicTypesObject.{meta['Name']}" - cat_list_type = f"Catalog.{meta['Name']}" - ccoct_list_type = f"ChartOfCharacteristicTypes.{meta['Name']}" - - for a in dsl['attributes']: - if a.get('type') == cat_obj_type: - a['type'] = ccoct_obj_type - if a.get('type') == 'DynamicList' and a.get('settings') and a['settings'].get('mainTable') == cat_list_type: - a['settings']['mainTable'] = ccoct_list_type - - # For Item forms: inject ValueType field after Description/ГруппаКодНаименование - if purpose == 'Item' and dsl.get('elements'): - vt_el = OrderedDict([('input', '\u0422\u0438\u043f\u0417\u043d\u0430\u0447\u0435\u043d\u0438\u044f'), ('path', '\u041e\u0431\u044a\u0435\u043a\u0442.ValueType')]) - els = dsl['elements'] - if isinstance(els, list): - inserted = False - new_els = [] - for el in els: - new_els.append(el) - if not inserted and isinstance(el, dict): - name = el.get('input') or el.get('group') or '' - if name in ('\u041d\u0430\u0438\u043c\u0435\u043d\u043e\u0432\u0430\u043d\u0438\u0435', '\u0413\u0440\u0443\u043f\u043f\u0430\u041a\u043e\u0434\u041d\u0430\u0438\u043c\u0435\u043d\u043e\u0432\u0430\u043d\u0438\u0435'): - new_els.append(vt_el) - inserted = True - if not inserted: - new_els.append(vt_el) - dsl['elements'] = new_els - - return dsl - - -# --- ExchangePlan (delegates to Catalog) --- - -def generate_exchange_plan_dsl(meta, preset_data, purpose): - # ExchangePlans are not hierarchical and have no Folder form - dsl = generate_catalog_dsl(meta, preset_data, purpose) - - # Post-patch: replace Catalog types with ExchangePlan types - cat_obj_type = f"CatalogObject.{meta['Name']}" - ep_obj_type = f"ExchangePlanObject.{meta['Name']}" - cat_list_type = f"Catalog.{meta['Name']}" - ep_list_type = f"ExchangePlan.{meta['Name']}" - - for a in dsl['attributes']: - if a.get('type') == cat_obj_type: - a['type'] = ep_obj_type - if a.get('type') == 'DynamicList' and a.get('settings') and a['settings'].get('mainTable') == cat_list_type: - a['settings']['mainTable'] = ep_list_type - - # For Item forms: inject SentNo, ReceivedNo after Code/Description - if purpose == 'Item' and dsl.get('elements'): - sent_el = OrderedDict([('input', '\u041d\u043e\u043c\u0435\u0440\u041e\u0442\u043f\u0440\u0430\u0432\u043b\u0435\u043d\u043d\u043e\u0433\u043e'), ('path', '\u041e\u0431\u044a\u0435\u043a\u0442.SentNo'), ('readOnly', True)]) - recv_el = OrderedDict([('input', '\u041d\u043e\u043c\u0435\u0440\u041f\u0440\u0438\u043d\u044f\u0442\u043e\u0433\u043e'), ('path', '\u041e\u0431\u044a\u0435\u043a\u0442.ReceivedNo'), ('readOnly', True)]) - els = dsl['elements'] - if isinstance(els, list): - inserted = False - new_els = [] - for el in els: - new_els.append(el) - if not inserted and isinstance(el, dict): - name = el.get('input') or el.get('group') or '' - if name in ('\u041d\u0430\u0438\u043c\u0435\u043d\u043e\u0432\u0430\u043d\u0438\u0435', '\u0413\u0440\u0443\u043f\u043f\u0430\u041a\u043e\u0434\u041d\u0430\u0438\u043c\u0435\u043d\u043e\u0432\u0430\u043d\u0438\u0435'): - new_els.append(sent_el) - new_els.append(recv_el) - inserted = True - if not inserted: - new_els.append(sent_el) - new_els.append(recv_el) - dsl['elements'] = new_els - - return dsl - - -# --- ChartOfAccounts DSL generators --- - -def generate_chart_of_accounts_dsl(meta, preset_data, purpose): - p_key = f"chartOfAccounts.{purpose.lower()}" - p = preset_data.get(p_key, {}) - fd = p.get('fieldDefaults') or {'ref': {'choiceButton': True}, 'boolean': {'element': 'check'}} - dispatch = { - 'Item': lambda: generate_chart_of_accounts_item_dsl(meta, p, fd, preset_data), - 'Folder': lambda: generate_chart_of_accounts_folder_dsl(meta, p), - 'List': lambda: generate_chart_of_accounts_list_dsl(meta, preset_data), - 'Choice': lambda: generate_chart_of_accounts_choice_dsl(meta, preset_data), - } - return dispatch[purpose]() - - -def generate_chart_of_accounts_item_dsl(meta, p, fd, preset_data): - elements = [] - - # Header: Code + Parent - header_left_children = [] - if meta.get('CodeLength', 0) > 0: - header_left_children.append(OrderedDict([('input', '\u041a\u043e\u0434'), ('path', '\u041e\u0431\u044a\u0435\u043a\u0442.Code')])) - header_right_children = [] - if meta.get('Hierarchical'): - parent_title = (p.get('parent') or {}).get('title', '\u041f\u043e\u0434\u0447\u0438\u043d\u0435\u043d \u0441\u0447\u0435\u0442\u0443') - header_right_children.append(OrderedDict([('input', '\u0420\u043e\u0434\u0438\u0442\u0435\u043b\u044c'), ('path', '\u041e\u0431\u044a\u0435\u043a\u0442.Parent'), ('title', parent_title)])) - - if len(header_right_children) > 0: - elements.append(OrderedDict([ - ('group', 'horizontal'), ('name', '\u0413\u0440\u0443\u043f\u043f\u0430\u0428\u0430\u043f\u043a\u0430'), ('showTitle', False), ('representation', 'none'), - ('children', [ - OrderedDict([('group', 'vertical'), ('name', '\u0413\u0440\u0443\u043f\u043f\u0430\u0428\u0430\u043f\u043a\u0430\u041b\u0435\u0432\u043e'), ('showTitle', False), ('children', header_left_children)]), - OrderedDict([('group', 'vertical'), ('name', '\u0413\u0440\u0443\u043f\u043f\u0430\u0428\u0430\u043f\u043a\u0430\u041f\u0440\u0430\u0432\u043e'), ('showTitle', False), ('children', header_right_children)]), - ]), - ])) - elif len(header_left_children) > 0: - elements.extend(header_left_children) - - # Description - if meta.get('DescriptionLength', 0) > 0: - elements.append(OrderedDict([('input', '\u041d\u0430\u0438\u043c\u0435\u043d\u043e\u0432\u0430\u043d\u0438\u0435'), ('path', '\u041e\u0431\u044a\u0435\u043a\u0442.Description')])) - - # OffBalance - elements.append(OrderedDict([('check', '\u0417\u0430\u0431\u0430\u043b\u0430\u043d\u0441\u043e\u0432\u044b\u0439'), ('path', '\u041e\u0431\u044a\u0435\u043a\u0442.OffBalance')])) - - # AccountingFlags as checkboxes - if meta.get('AccountingFlags') and len(meta['AccountingFlags']) > 0: - flag_children = [] - for flag in meta['AccountingFlags']: - flag_children.append(OrderedDict([('check', flag['Name']), ('path', f"\u041e\u0431\u044a\u0435\u043a\u0442.{flag['Name']}")])) - elements.append(OrderedDict([ - ('group', 'vertical'), ('name', '\u0413\u0440\u0443\u043f\u043f\u0430\u041f\u0440\u0438\u0437\u043d\u0430\u043a\u0438\u0423\u0447\u0435\u0442\u0430'), ('title', '\u041f\u0440\u0438\u0437\u043d\u0430\u043a\u0438 \u0443\u0447\u0435\u0442\u0430'), - ('children', flag_children), - ])) - - # ExtDimensionTypes table - if meta.get('MaxExtDimensionCount', 0) > 0: - # Column names are prefixed with the table name (like the generic TS path and stock 1C), - # else a subconto flag column collides with a same-named account accounting-flag checkbox. - ed_table = '\u0412\u0438\u0434\u044b\u0421\u0443\u0431\u043a\u043e\u043d\u0442\u043e' - ed_cols = [] - ed_cols.append(OrderedDict([('input', f"{ed_table}\u0412\u0438\u0434\u0421\u0443\u0431\u043a\u043e\u043d\u0442\u043e"), ('path', '\u041e\u0431\u044a\u0435\u043a\u0442.ExtDimensionTypes.ExtDimensionType')])) - ed_cols.append(OrderedDict([('check', f"{ed_table}\u0422\u043e\u043b\u044c\u043a\u043e\u041e\u0431\u043e\u0440\u043e\u0442\u044b"), ('path', '\u041e\u0431\u044a\u0435\u043a\u0442.ExtDimensionTypes.TurnoversOnly')])) - if meta.get('ExtDimensionAccountingFlags'): - for ed_flag in meta['ExtDimensionAccountingFlags']: - ed_cols.append(OrderedDict([('check', f"{ed_table}{ed_flag['Name']}"), ('path', f"\u041e\u0431\u044a\u0435\u043a\u0442.ExtDimensionTypes.{ed_flag['Name']}")])) - elements.append(OrderedDict([ - ('table', ed_table), - ('path', '\u041e\u0431\u044a\u0435\u043a\u0442.ExtDimensionTypes'), - ('columns', ed_cols), - ])) - - # Custom attributes - for attr in meta['Attributes']: - if not is_displayable_type(attr['Type']): - continue - elements.append(new_field_element(attr['Name'], f"\u041e\u0431\u044a\u0435\u043a\u0442.{attr['Name']}", attr['Type'], fd)) - - # Tabular sections - ts_exclude = ['\u0414\u043e\u043f\u043e\u043b\u043d\u0438\u0442\u0435\u043b\u044c\u043d\u044b\u0435\u0420\u0435\u043a\u0432\u0438\u0437\u0438\u0442\u044b', '\u041f\u0440\u0435\u0434\u0441\u0442\u0430\u0432\u043b\u0435\u043d\u0438\u044f'] - for ts in meta['TabularSections']: - if ts['Name'] in ts_exclude: - continue - ts_cols = [] - for col in ts['Columns']: - if not is_displayable_type(col['Type']): - continue - ts_cols.append(new_field_element(f"{ts['Name']}{col['Name']}", f"\u041e\u0431\u044a\u0435\u043a\u0442.{ts['Name']}.{col['Name']}", col['Type'], fd)) - elements.append(OrderedDict([('table', ts['Name']), ('path', f"\u041e\u0431\u044a\u0435\u043a\u0442.{ts['Name']}"), ('columns', ts_cols)])) - - props = OrderedDict() - if p.get('properties'): - for k in p['properties']: - props[k] = p['properties'][k] - - return OrderedDict([ - ('title', meta['Synonym']), - ('properties', props), - ('elements', elements), - ('attributes', [ - {'name': '\u041e\u0431\u044a\u0435\u043a\u0442', 'type': f"ChartOfAccountsObject.{meta['Name']}", 'main': True, 'savedData': True} - ]), - ]) - - -def generate_chart_of_accounts_folder_dsl(meta, p): - elements = [] - if meta.get('CodeLength', 0) > 0: - elements.append(OrderedDict([('input', '\u041a\u043e\u0434'), ('path', '\u041e\u0431\u044a\u0435\u043a\u0442.Code')])) - if meta.get('DescriptionLength', 0) > 0: - elements.append(OrderedDict([('input', '\u041d\u0430\u0438\u043c\u0435\u043d\u043e\u0432\u0430\u043d\u0438\u0435'), ('path', '\u041e\u0431\u044a\u0435\u043a\u0442.Description')])) - if meta.get('Hierarchical'): - parent_title = (p.get('parent') or {}).get('title', '\u041f\u043e\u0434\u0447\u0438\u043d\u0435\u043d \u0441\u0447\u0435\u0442\u0443') - elements.append(OrderedDict([('input', '\u0420\u043e\u0434\u0438\u0442\u0435\u043b\u044c'), ('path', '\u041e\u0431\u044a\u0435\u043a\u0442.Parent'), ('title', parent_title)])) - - props = OrderedDict([('windowOpeningMode', 'LockOwnerWindow')]) - if p.get('properties'): - for k in p['properties']: - props[k] = p['properties'][k] - - return OrderedDict([ - ('title', meta['Synonym']), - ('useForFoldersAndItems', 'Folders'), - ('properties', props), - ('elements', elements), - ('attributes', [ - {'name': '\u041e\u0431\u044a\u0435\u043a\u0442', 'type': f"ChartOfAccountsObject.{meta['Name']}", 'main': True, 'savedData': True} - ]), - ]) - - -def generate_chart_of_accounts_list_dsl(meta, preset_data): - # Delegate to Catalog List and patch types - dsl = generate_catalog_dsl(meta, preset_data, 'List') - for a in dsl['attributes']: - if a.get('type') == 'DynamicList' and a.get('settings') and a['settings'].get('mainTable') == f"Catalog.{meta['Name']}": - a['settings']['mainTable'] = f"ChartOfAccounts.{meta['Name']}" - return dsl - - -def generate_chart_of_accounts_choice_dsl(meta, preset_data): - dsl = generate_catalog_dsl(meta, preset_data, 'Choice') - for a in dsl['attributes']: - if a.get('type') == 'DynamicList' and a.get('settings') and a['settings'].get('mainTable') == f"Catalog.{meta['Name']}": - a['settings']['mainTable'] = f"ChartOfAccounts.{meta['Name']}" - return dsl - - -# ═══════════════════════════════════════════════════════════════════════════ -# END OF FROM-OBJECT MODE FUNCTIONS -# ═══════════════════════════════════════════════════════════════════════════ - - -def esc_xml(s): - # Экранирование ТЕКСТА элемента (, ): только & < > . - # Кавычки/апострофы в тексте 1С не экранирует (пишет литерально) — " ломал бы раундтрип. - return s.replace('&', '&').replace('<', '<').replace('>', '>') - - -def di_attr(el): - # DisplayImportance — атрибут открывающего тега элемента (адаптивная важность). "" если нет. - if isinstance(el, dict) and el.get('displayImportance'): - return f' DisplayImportance="{esc_xml(str(el["displayImportance"]))}"' - return '' - - -# Базовая директория для @file-ссылок в query динсписка (устанавливается в main) -QUERY_BASE_DIR = None - - -def resolve_query_value(val, base_dir): - if not val.startswith('@'): - return val - file_path = val[1:] - if os.path.isabs(file_path): - candidates = [file_path] - else: - candidates = [os.path.join(base_dir or os.getcwd(), file_path), os.path.join(os.getcwd(), file_path)] - for c in candidates: - if os.path.exists(c): - with open(c, 'r', encoding='utf-8-sig') as f: - return f.read().rstrip() - print(f"Query file not found: {file_path} (searched: {', '.join(candidates)})", file=sys.stderr) - sys.exit(1) - - -def emit_ml_items(lines, indent, val): - # строка → один ru-элемент; объект {lang: text} → по элементу на язык - if isinstance(val, dict): - for k, v in val.items(): - lines.append(f"{indent}") - lines.append(f"{indent}\t{k}") - lines.append(f"{indent}\t{esc_xml(str(v))}") - lines.append(f"{indent}") - else: - lines.append(f"{indent}") - lines.append(f"{indent}\tru") - lines.append(f"{indent}\t{esc_xml(str(val))}") - lines.append(f"{indent}") - - -def emit_mltext(lines, indent, tag, text, xsi_type=None): - attr = f' xsi:type="{xsi_type}"' if xsi_type else '' - if not text: - lines.append(f"{indent}<{tag}{attr}/>") - return - lines.append(f"{indent}<{tag}{attr}>") - emit_ml_items(lines, f"{indent}\t", text) - lines.append(f"{indent}") - - -def emit_us_presentation(lines, indent, tag, val): - # : плоская строка → xsi:type="xs:string"; мультиязычный → v8:LocalStringType - if val is None: - return - if isinstance(val, str): - lines.append(f'{indent}<{tag} xsi:type="xs:string">{esc_xml(val)}') - else: - emit_mltext(lines, indent, tag, val, xsi_type='v8:LocalStringType') - - -# Каноничные GUID пустых контейнеров ListSettings (умолчание платформы, ~90% форм). -CANON_FILTER_ID = 'dfcece9d-5077-440b-b6b3-45a5cb4538eb' -CANON_ORDER_ID = '88619765-ccb3-46c6-ac52-38e9c992ebd4' -CANON_CA_ID = 'b75fecce-942b-4aed-abc9-e6a02e460fb3' -CANON_ITEMS_ID = '911b6018-f537-43e8-a417-da56b22f9aec' - - -def new_uuid(): - return str(uuid.uuid4()) - - -# ───────────────────────────────────────────────────────────────────────────── -# Настройки компоновщика ListSettings: filter/order/conditionalAppearance. -# Грамматика DSL и эмиссия dcsset скопированы из skd-compile (навыки автономны). -# ───────────────────────────────────────────────────────────────────────────── -COMPARISON_TYPES = { - '=': 'Equal', '<>': 'NotEqual', - '>': 'Greater', '>=': 'GreaterOrEqual', - '<': 'Less', '<=': 'LessOrEqual', - 'in': 'InList', 'notIn': 'NotInList', - 'inHierarchy': 'InHierarchy', 'inListByHierarchy': 'InListByHierarchy', - 'contains': 'Contains', 'notContains': 'NotContains', - 'beginsWith': 'BeginsWith', 'notBeginsWith': 'NotBeginsWith', - 'like': 'Like', 'notLike': 'NotLike', - 'подобно': 'Like', 'неподобно': 'NotLike', # рус. синоним - 'filled': 'Filled', 'notFilled': 'NotFilled', -} -# Регистронезависимый лукап (зеркало PS-хэша): Like/LIKE/ПОДОБНО → канон -_COMPARISON_TYPES_CI = {k.lower(): v for k, v in COMPARISON_TYPES.items()} - -_REF_TYPE_RE = re.compile( - r'^(Перечисление|Справочник|ПланСчетов|Документ|ПланВидовХарактеристик|ПланВидовРасчета|' - r'БизнесПроцесс|Задача|РегистрСведений|ПланОбмена|Catalog|Enum|Document|ChartOfAccounts|' - r'ChartOfCharacteristicTypes|ChartOfCalculationTypes|BusinessProcess|Task|' - r'InformationRegister|ExchangePlan)\.') - - -def parse_filter_shorthand(s): - result = {'field': '', 'op': 'Equal', 'value': None, 'use': True, - 'userSettingID': None, 'viewMode': None, 'presentation': None} - if re.search(r'@user', s): - result['userSettingID'] = 'auto' - s = re.sub(r'\s*@user', '', s) - if re.search(r'@off', s): - result['use'] = False - s = re.sub(r'\s*@off', '', s) - if re.search(r'@quickAccess', s): - result['viewMode'] = 'QuickAccess' - s = re.sub(r'\s*@quickAccess', '', s) - if re.search(r'@normal', s): - result['viewMode'] = 'Normal' - s = re.sub(r'\s*@normal', '', s) - if re.search(r'@inaccessible', s): - result['viewMode'] = 'Inaccessible' - s = re.sub(r'\s*@inaccessible', '', s) - s = s.strip() - op_patterns = ['<>', '>=', '<=', '=', '>', '<', - r'notIn\b', r'in\b', r'inHierarchy\b', r'inListByHierarchy\b', - r'notContains\b', r'contains\b', r'notBeginsWith\b', r'beginsWith\b', - r'notLike\b', r'like\b', r'неподобно\b', r'подобно\b', - r'notFilled\b', r'filled\b'] - op_joined = '|'.join(op_patterns) - m = re.match(r'^(.+?)\s+(' + op_joined + r')\s*(.*)?$', s, re.IGNORECASE) - if m: - result['field'] = m.group(1).strip() - result['op'] = m.group(2).strip() - val_part = m.group(3).strip() if m.group(3) else '' - if val_part and val_part != '_': - if val_part == 'true' or val_part == 'false': - result['value'] = (val_part == 'true') - result['valueType'] = 'xs:boolean' - elif re.match(r'^\d{4}-\d{2}-\d{2}T', val_part): - # дата без valueType → emit_filter_item выведет StandardBeginningDate Custom (дефолт даты в фильтре) - result['value'] = val_part - elif re.match(r'^\d+(\.\d+)?$', val_part): - result['value'] = val_part - result['valueType'] = 'xs:decimal' - elif re.match(r'^(Перечисление|Справочник|ПланСчетов|Документ|ПланВидовХарактеристик|ПланВидовРасчета)\.', val_part): - result['value'] = val_part - result['valueType'] = 'dcscor:DesignTimeValue' - else: - result['value'] = val_part - result['valueType'] = 'xs:string' - else: - result['field'] = s - return result - - -def _value_type_for(v, explicit=None): - if explicit: - return explicit - if isinstance(v, bool): - return 'xs:boolean' - if isinstance(v, (int, float)): - return 'xs:decimal' - vs = str(v) - if re.match(r'^\d{4}-\d{2}-\d{2}T', vs): - return 'xs:dateTime' - if re.match(r'^-?\d+(\.\d+)?$', vs): - return 'xs:decimal' - if _REF_TYPE_RE.match(vs): - return 'dcscor:DesignTimeValue' - return 'xs:string' - - -# Значение типа v8:Type (напр. тип «Неопределено» = :Undefined) ссылается на тип -# платформы из namespace http://v8.1c.ru/8.2/data/types — платформа объявляет его ЛОКАЛЬНО -# на теге значения (префикс авто: d6p1/d8p1/dN…). Без объявления QName битый. -def _value_type_ns_attr(value_type, value): - if value_type == 'v8:Type': - m = re.match(r'^([A-Za-z]\w*):', str(value)) - if m and m.group(1) not in ('xs', 'cfg', 'v8', 'v8ui', 'ent', 'dcscor', 'dcsset', 'dcssch'): - return f' xmlns:{m.group(1)}="http://v8.1c.ru/8.2/data/types"' - return '' - - -def emit_filter_item(lines, item, indent): - if item.get('group'): - g = str(item['group']) - group_type = {'And': 'AndGroup', 'Or': 'OrGroup', 'Not': 'NotGroup'}.get(g, g + 'Group') - lines.append(f'{indent}') - lines.append(f'{indent}\t{group_type}') - if item.get('items'): - for sub in item['items']: - if isinstance(sub, str): - parsed = parse_filter_shorthand(sub) - obj = {'field': parsed['field'], 'op': parsed['op']} - if parsed['use'] is False: - obj['use'] = False - if parsed['value'] is not None: - obj['value'] = parsed['value'] - if parsed.get('valueType'): - obj['valueType'] = parsed['valueType'] - if parsed.get('userSettingID'): - obj['userSettingID'] = parsed['userSettingID'] - if parsed.get('viewMode'): - obj['viewMode'] = parsed['viewMode'] - sub = obj - emit_filter_item(lines, sub, f'{indent}\t') - if item.get('presentation'): - emit_us_presentation(lines, f'{indent}\t', 'dcsset:presentation', item['presentation']) - if item.get('viewMode'): - lines.append(f'{indent}\t{esc_xml(str(item["viewMode"]))}') - if item.get('userSettingID'): - guid = new_uuid() if str(item['userSettingID']) == 'auto' else str(item['userSettingID']) - lines.append(f'{indent}\t{esc_xml(guid)}') - if item.get('userSettingPresentation'): - emit_us_presentation(lines, f'{indent}\t', 'dcsset:userSettingPresentation', item['userSettingPresentation']) - lines.append(f'{indent}') - return - - lines.append(f'{indent}') - if item.get('use') is False: - lines.append(f'{indent}\tfalse') - lines.append(f'{indent}\t{esc_xml(str(item.get("field", "")))}') - # Регистронезависимый лукап (зеркало PS): Like/LIKE/ПОДОБНО → канон; иначе — как есть - comp_type = _COMPARISON_TYPES_CI.get(str(item.get('op')).lower()) - if not comp_type: - comp_type = str(item.get('op')) - lines.append(f'{indent}\t{esc_xml(comp_type)}') - val = item.get('value') - if isinstance(val, list): - if len(val) == 0: - lines.append(f'{indent}\t') - lines.append(f'{indent}\t\t') - lines.append(f'{indent}\t\t-1') - lines.append(f'{indent}\t') - else: - for v in val: - vt = _value_type_for(v, item.get('valueType')) - v_str = str(v).lower() if isinstance(v, bool) else esc_xml(str(v)) - ns_attr = _value_type_ns_attr(vt, v) - lines.append(f'{indent}\t{v_str}') - elif val is not None and ( - re.search(r'Standard(Beginning|End)Date$', str(item.get('valueType') or '')) or - (not item.get('valueType') and isinstance(val, str) and re.match(r'^\d{4}-\d{2}-\d{2}T', val))): - # Стандартная дата начала/окончания. Формы: объект {variant, date?} (Custom несёт ); - # строка-вариант "BeginningOfThisDay" (именованный без даты); голая ISO-дата без valueType — - # шорткат для Custom+date (дата в фильтре почти всегда SBD Custom, корпус 268 vs 2 xs:dateTime). - sd_type = re.sub(r'^v8:', '', str(item['valueType'])) if item.get('valueType') else 'StandardBeginningDate' - if isinstance(val, dict): - variant = str(val.get('variant', '')); date_v = val.get('date') - elif isinstance(val, str) and re.match(r'^\d{4}-\d{2}-\d{2}T', val): - variant = 'Custom'; date_v = val - else: - variant = str(val); date_v = None - lines.append(f'{indent}\t') - lines.append(f'{indent}\t\t{esc_xml(variant)}') - if date_v is not None: - lines.append(f'{indent}\t\t{esc_xml(str(date_v))}') - lines.append(f'{indent}\t') - elif str(val) == '_': - # "_" — маркер пустого значения: платформа эмитит пустой self-closing - # (напр. — сравнение с незаданным полем). - vt = str(item['valueType']) if item.get('valueType') else 'xs:string' - lines.append(f'{indent}\t') - elif val is not None: - vt = _value_type_for(val, item.get('valueType')) - v_str = str(val).lower() if isinstance(val, bool) else esc_xml(str(val)) - ns_attr = _value_type_ns_attr(vt, val) - lines.append(f'{indent}\t{v_str}') - if item.get('presentation'): - emit_us_presentation(lines, f'{indent}\t', 'dcsset:presentation', item['presentation']) - if item.get('viewMode'): - lines.append(f'{indent}\t{esc_xml(str(item["viewMode"]))}') - if item.get('userSettingID'): - uid = new_uuid() if str(item['userSettingID']) == 'auto' else str(item['userSettingID']) - lines.append(f'{indent}\t{esc_xml(uid)}') - if item.get('userSettingPresentation'): - emit_us_presentation(lines, f'{indent}\t', 'dcsset:userSettingPresentation', item['userSettingPresentation']) - lines.append(f'{indent}') - - -def emit_filter(lines, items, indent, block_view_mode=None, block_user_setting_id=None): - has_items = bool(items) and len(items) > 0 - has_block_meta = (block_view_mode is not None) or (block_user_setting_id is not None) - if not has_items and not has_block_meta: - return - lines.append(f'{indent}') - for item in (items or []): - if isinstance(item, str): - parsed = parse_filter_shorthand(item) - obj = {'field': parsed['field'], 'op': parsed['op']} - if parsed['use'] is False: - obj['use'] = False - if parsed['value'] is not None: - obj['value'] = parsed['value'] - if parsed.get('valueType'): - obj['valueType'] = parsed['valueType'] - if parsed.get('userSettingID'): - obj['userSettingID'] = parsed['userSettingID'] - if parsed.get('viewMode'): - obj['viewMode'] = parsed['viewMode'] - emit_filter_item(lines, obj, f'{indent}\t') - else: - emit_filter_item(lines, item, f'{indent}\t') - if block_view_mode is not None: - lines.append(f'{indent}\t{esc_xml(str(block_view_mode))}') - if block_user_setting_id is not None: - uid = new_uuid() if str(block_user_setting_id) == 'auto' else str(block_user_setting_id) - lines.append(f'{indent}\t{esc_xml(uid)}') - lines.append(f'{indent}') - - -def emit_order(lines, items, indent, skip_auto=False, block_view_mode=None, block_user_setting_id=None): - has_items = bool(items) and len(items) > 0 - has_block_meta = (block_view_mode is not None) or (block_user_setting_id is not None) - if not has_items and not has_block_meta: - return - lines.append(f'{indent}') - for item in (items or []): - if isinstance(item, str): - if item == 'Auto': - if not skip_auto: - lines.append(f'{indent}\t') - else: - parts = re.split(r'\s+', item) - field = parts[0] - direction = 'Asc' - if len(parts) > 1 and re.match(r'(?i)^(desc|убыв)', parts[1]): - direction = 'Desc' - elif len(parts) > 1 and re.match(r'(?i)^(asc|возр)', parts[1]): - direction = 'Asc' - lines.append(f'{indent}\t') - lines.append(f'{indent}\t\t{esc_xml(field)}') - lines.append(f'{indent}\t\t{direction}') - lines.append(f'{indent}\t') - else: - if item.get('field') == 'Auto' or item.get('type') == 'auto': - if not skip_auto: - lines.append(f'{indent}\t') - continue - direction = str(item['direction']) if item.get('direction') else 'Asc' - if re.match(r'(?i)^(desc|убыв)', direction): - direction = 'Desc' - elif re.match(r'(?i)^(asc|возр)', direction): - direction = 'Asc' - lines.append(f'{indent}\t') - if item.get('use') is False: - lines.append(f'{indent}\t\tfalse') - lines.append(f'{indent}\t\t{esc_xml(str(item.get("field", "")))}') - lines.append(f'{indent}\t\t{direction}') - if item.get('viewMode'): - lines.append(f'{indent}\t\t{esc_xml(str(item["viewMode"]))}') - lines.append(f'{indent}\t') - if block_view_mode is not None: - lines.append(f'{indent}\t{esc_xml(str(block_view_mode))}') - if block_user_setting_id is not None: - uid = new_uuid() if str(block_user_setting_id) == 'auto' else str(block_user_setting_id) - lines.append(f'{indent}\t{esc_xml(uid)}') - lines.append(f'{indent}') - - -def emit_appearance_value(lines, key, val, indent): - lines.append(f'{indent}') - - def _has_key(o, k): - return isinstance(o, dict) and (k in o) - - def _get(o, k): - return o.get(k) if isinstance(o, dict) else None - - is_top_level_line = _has_key(val, '@type') and (str(_get(val, '@type')) == 'Line') - use_wrapper = False - inner_val = val - nested_items = None - if is_top_level_line: - if _has_key(val, 'use') and (_get(val, 'use') is False): - use_wrapper = True - if _has_key(val, 'items'): - nested_items = _get(val, 'items') - elif _has_key(val, 'value') and isinstance(val, dict): - inner_val = _get(val, 'value') - if _has_key(val, 'use') and (_get(val, 'use') is False): - use_wrapper = True - if _has_key(val, 'items'): - nested_items = _get(val, 'items') - if use_wrapper: - lines.append(f'{indent}\tfalse') - lines.append(f'{indent}\t{esc_xml(key)}') - - is_font_dict = isinstance(inner_val, dict) and inner_val.get('@type') is not None and str(inner_val.get('@type')) == 'Font' - is_line_dict = _has_key(inner_val, '@type') and (str(_get(inner_val, '@type')) == 'Line') - is_dict = isinstance(inner_val, dict) - if is_line_dict: - lw = _get(inner_val, 'width') if _has_key(inner_val, 'width') else 0 - lg = ('true' if _get(inner_val, 'gap') else 'false') if _has_key(inner_val, 'gap') else 'false' - ls = str(_get(inner_val, 'style')) if _has_key(inner_val, 'style') else 'None' - lines.append(f'{indent}\t') - lines.append(f'{indent}\t\t{esc_xml(ls)}') - lines.append(f'{indent}\t') - elif is_font_dict: - attr_parts = [] - for attr_name in ('ref', 'faceName', 'height', 'bold', 'italic', 'underline', 'strikeout', 'kind', 'scale'): - if attr_name in inner_val: - av = inner_val[attr_name] - if av is not None: - attr_parts.append(f'{attr_name}="{esc_xml(str(av))}"') - lines.append(f'{indent}\t') - elif is_dict and _has_key(inner_val, 'field'): - # Ссылка на поле (dcscor:Field) — значение параметра оформления = поле компоновки - lines.append(f'{indent}\t{esc_xml(str(_get(inner_val, "field")))}') - elif is_dict: - # Локализуемый текст параметра оформления: платформа объявляет xsi:type на dcscor:value - emit_mltext(lines, f'{indent}\t', 'dcscor:value', inner_val, xsi_type='v8:LocalStringType') - else: - actual_val = str(inner_val) - key_type_map = { - 'Размещение': 'dcscor:DataCompositionTextPlacementType', - 'ГоризонтальноеПоложение': 'v8ui:HorizontalAlign', - 'ВертикальноеПоложение': 'v8ui:VerticalAlign', - 'ОриентацияТекста': 'xs:decimal', - 'РасположениеИтогов': 'dcscor:DataCompositionTotalPlacement', - 'ТипМакета': 'dcsset:DataCompositionGroupTemplateType', - } - key_type = key_type_map.get(key) - if key_type: - lines.append(f'{indent}\t{esc_xml(actual_val)}') - elif re.match(r'^(style|web|win):', actual_val): - lines.append(f'{indent}\t{esc_xml(actual_val)}') - elif actual_val == 'true' or actual_val == 'false': - lines.append(f'{indent}\t{actual_val}') - elif key == 'Текст' or key == 'Заголовок' or key == 'Формат': - # Голая строка = плоский xs:string (нелокализованный литерал). Локализуемый → объект {ru,en}. - # Пустая строка → самозакрывающийся тег (как у платформы). - if actual_val == '': - lines.append(f'{indent}\t') - else: - lines.append(f'{indent}\t{esc_xml(actual_val)}') - elif re.match(r'^-?\d+(\.\d+)?$', actual_val): - lines.append(f'{indent}\t{actual_val}') - elif key == 'ЦветТекста' or key == 'ЦветФона' or key == 'ЦветГраницы': - lines.append(f'{indent}\t{esc_xml(actual_val)}') - else: - lines.append(f'{indent}\t{esc_xml(actual_val)}') - if nested_items: - if isinstance(nested_items, dict): - for nk, nv in nested_items.items(): - emit_appearance_value(lines, nk, nv, f'{indent}\t') - lines.append(f'{indent}') - - -# === Группировка строк динамического списка (DCS-структура ListSettings) === -# Линейная цепочка (каждый уровень = одно поле в groupItems; -# вложенность — через дочерний ). Плоская модель уровней (список всегда линеен). -def get_list_grouping_value(s): - for k in ('grouping', 'structure', 'группировка'): - if s.get(k): - return s[k] - return None - - -def parse_list_grouping(grouping): - # Шорткат "A > B > C" → массив имён; массив строк/объектов → как есть. - if not grouping: - return [] - if isinstance(grouping, str): - return [p.strip() for p in re.split(r'\s*>\s*', grouping) if p.strip()] - return list(grouping) - - -def emit_group_item_field(lines, level, indent): - if isinstance(level, str): - field, gt, pat = level, 'Items', 'None' - pab = pae = '0001-01-01T00:00:00' - else: - field = str(level.get('field', '')) - gt = str(level.get('groupType') or 'Items') - pat = str(level.get('periodAdditionType') or 'None') - pab = str(level.get('periodAdditionBegin') or '0001-01-01T00:00:00') - pae = str(level.get('periodAdditionEnd') or '0001-01-01T00:00:00') - lines.append(f'{indent}') - lines.append(f'{indent}\t{esc_xml(field)}') - lines.append(f'{indent}\t{esc_xml(gt)}') - lines.append(f'{indent}\t{esc_xml(pat)}') - # Авто-детект: ISO-дата → xs:dateTime, иначе путь → dcscor:Field. - pab_t = 'xs:dateTime' if re.match(r'^\d{4}-\d{2}-\d{2}T', pab) else 'dcscor:Field' - pae_t = 'xs:dateTime' if re.match(r'^\d{4}-\d{2}-\d{2}T', pae) else 'dcscor:Field' - lines.append(f'{indent}\t{esc_xml(pab)}') - lines.append(f'{indent}\t{esc_xml(pae)}') - lines.append(f'{indent}') - - -def emit_list_grouping_levels(lines, levels, i, indent): - lines.append(f'{indent}') - lines.append(f'{indent}\t') - emit_group_item_field(lines, levels[i], f'{indent}\t\t') - lines.append(f'{indent}\t') - if i < len(levels) - 1: - emit_list_grouping_levels(lines, levels, i + 1, f'{indent}\t') - lines.append(f'{indent}') - - -def emit_list_grouping(lines, grouping, indent): - levels = parse_list_grouping(grouping) - if not levels: - return - emit_list_grouping_levels(lines, levels, 0, indent) - - -# === Вычисляемые поля DataSet динамического списка () === -# Зеркало skd calculatedFields: shorthand "Имя [Заголовок]: тип = Выражение #noField #noFilter -# #noGroup #noOrder" или объект. Форм-специфика: dcssch:-теги + presentationExpression/orderExpression. -_CALC_RESTRICT_MAP = {'noField': 'field', 'noFilter': 'condition', 'noCondition': 'condition', - 'noGroup': 'group', 'noOrder': 'order'} -_DCS_COMMON_NS = 'http://v8.1c.ru/8.1/data-composition-system/common' - - -def parse_calc_shorthand(s): - restrict = re.findall(r'#(noField|noFilter|noCondition|noGroup|noOrder)\b', s) - s = re.sub(r'\s*#(?:noField|noFilter|noCondition|noGroup|noOrder)\b', '', s) - eq = s.find('=') - lhs, rhs = (s[:eq], s[eq + 1:].strip()) if eq > 0 else (s, '') - title = '' - m = re.search(r'\[([^\]]+)\]', lhs) - if m: - title = m.group(1) - lhs = re.sub(r'\s*\[[^\]]+\]', '', lhs) - lhs = lhs.strip() - typ, data_path = '', lhs - if ':' in lhs: - data_path, t = lhs.split(':', 1) - data_path, typ = data_path.strip(), resolve_type_str(t.strip()) - return {'dataPath': data_path, 'expression': rhs, 'type': typ, 'title': title, 'restrict': restrict} - - -def emit_calc_fields(lines, calc_fields, indent): - if not calc_fields: - return - for cf in calc_fields: - if isinstance(cf, str): - p = parse_calc_shorthand(cf) - data_path, expression, title = p['dataPath'], p['expression'], p['title'] - type_str = p['type'] - restrict = [_CALC_RESTRICT_MAP[r] for r in p['restrict'] if r in _CALC_RESTRICT_MAP] - pres_expr = order_expr = None - else: - data_path = str(cf.get('dataPath') or cf.get('field') or cf.get('name', '')) - expression = str(cf.get('expression', '')) - title = cf.get('title') - type_str = cf.get('valueType') or cf.get('type') - type_str = str(type_str) if type_str else None - ur = cf.get('useRestriction') or cf.get('restrict') - if isinstance(ur, dict): - restrict = [k for k in ('field', 'condition', 'group', 'order') if ur.get(k)] - elif isinstance(ur, str): - restrict = [_CALC_RESTRICT_MAP.get(t.strip().lstrip('#'), t.strip().lstrip('#')) for t in ur.split() if t.strip()] - elif isinstance(ur, list): - restrict = [_CALC_RESTRICT_MAP.get(str(r), str(r)) for r in ur] - else: - restrict = [] - pres_expr = cf.get('presentationExpression') - order_expr = cf.get('orderExpression') - ci = f'{indent}\t' - lines.append(f'{indent}') - lines.append(f'{ci}{esc_xml(data_path)}') - lines.append(f'{ci}{esc_xml(expression)}') - if title: - emit_mltext(lines, ci, 'dcssch:title', title, xsi_type='v8:LocalStringType') - if restrict: - lines.append(f'{ci}') - for r in ('field', 'condition', 'group', 'order'): - if r in restrict: - lines.append(f'{ci}\ttrue') - lines.append(f'{ci}') - if pres_expr: - lines.append(f'{ci}{esc_xml(str(pres_expr))}') - if order_expr: - for oe in (order_expr if isinstance(order_expr, list) else [order_expr]): - if isinstance(oe, str): - expr_v, otype, auto = oe, 'Asc', 'false' - else: - expr_v = str(oe.get('expression', '')) - otype = str(oe.get('orderType', 'Asc')) - auto = 'true' if oe.get('autoOrder') else 'false' - lines.append(f'{ci}') - lines.append(f'{ci}\t{esc_xml(expr_v)}') - lines.append(f'{ci}\t{otype}') - lines.append(f'{ci}\t{auto}') - lines.append(f'{ci}') - if type_str: - emit_dl_value_type(lines, type_str, ci) - lines.append(f'{indent}') - - -# Ограничения использования поля/вычисляемого поля (useRestriction / attributeUseRestriction). -# Значение: объект {field?,condition?,group?,order?} | флаг-строка "#noField …" | массив. -def parse_restrict(ur): - if not ur: - return [] - if isinstance(ur, dict): - return [k for k in ('field', 'condition', 'group', 'order') if ur.get(k)] - if isinstance(ur, str): - return [_CALC_RESTRICT_MAP.get(t.strip().lstrip('#'), t.strip().lstrip('#')) for t in ur.split() if t.strip()] - if isinstance(ur, list): - return [_CALC_RESTRICT_MAP.get(str(r), str(r)) for r in ur] - return [] - - -def emit_restrict_block(lines, tag, ur, indent): - r = parse_restrict(ur) - if not r: - return - lines.append(f'{indent}') - for k in ('field', 'condition', 'group', 'order'): - if k in r: - lines.append(f'{indent}\ttrue') - lines.append(f'{indent}') - - -def emit_conditional_appearance(lines, items, indent, block_view_mode=None, block_user_setting_id=None, wrap_tag='dcsset:conditionalAppearance'): - has_items = bool(items) and len(items) > 0 - has_block_meta = (block_view_mode is not None) or (block_user_setting_id is not None) - if not has_items and not has_block_meta: - return - lines.append(f'{indent}<{wrap_tag}>') - for ca in (items or []): - lines.append(f'{indent}\t') - if ca.get('use') is False: - lines.append(f'{indent}\t\tfalse') - if ca.get('selection') and len(ca['selection']) > 0: - lines.append(f'{indent}\t\t') - for sel in ca['selection']: - lines.append(f'{indent}\t\t\t') - lines.append(f'{indent}\t\t\t\t{esc_xml(str(sel))}') - lines.append(f'{indent}\t\t\t') - lines.append(f'{indent}\t\t') - else: - lines.append(f'{indent}\t\t') - if ca.get('filter') and len(ca['filter']) > 0: - emit_filter(lines, ca['filter'], f'{indent}\t\t') - else: - lines.append(f'{indent}\t\t') - if ca.get('appearance'): - lines.append(f'{indent}\t\t') - for k, v in ca['appearance'].items(): - emit_appearance_value(lines, k, v, f'{indent}\t\t\t') - lines.append(f'{indent}\t\t') - if ca.get('presentation'): - if isinstance(ca['presentation'], dict): - # Мультиязык → LocalStringType (платформа объявляет тип у локализованного presentation) - lines.append(f'{indent}\t\t') - emit_ml_items(lines, f'{indent}\t\t\t', ca['presentation']) - lines.append(f'{indent}\t\t') - else: - lines.append(f'{indent}\t\t{esc_xml(str(ca["presentation"]))}') - if ca.get('viewMode'): - lines.append(f'{indent}\t\t{esc_xml(str(ca["viewMode"]))}') - if ca.get('userSettingID'): - uid = new_uuid() if str(ca['userSettingID']) == 'auto' else str(ca['userSettingID']) - lines.append(f'{indent}\t\t{esc_xml(uid)}') - if ca.get('userSettingPresentation'): - emit_us_presentation(lines, f'{indent}\t\t', 'dcsset:userSettingPresentation', ca['userSettingPresentation']) - if ca.get('useInDontUse') and len(ca['useInDontUse']) > 0: - use_in_order = ['group', 'hierarchicalGroup', 'overall', 'fieldsHeader', 'header', - 'parameters', 'filter', 'resourceFieldsHeader', 'overallHeader', - 'overallResourceFieldsHeader'] - sset = {str(n): True for n in ca['useInDontUse']} - for n in use_in_order: - if n in sset: - tag = 'useIn' + n[0].upper() + n[1:] - lines.append(f'{indent}\t\tDontUse') - lines.append(f'{indent}\t') - if block_view_mode is not None: - lines.append(f'{indent}\t{esc_xml(str(block_view_mode))}') - if block_user_setting_id is not None: - uid = new_uuid() if str(block_user_setting_id) == 'auto' else str(block_user_setting_id) - lines.append(f'{indent}\t{esc_xml(uid)}') - lines.append(f'{indent}') - - -def write_utf8_bom(path, content): - with open(path, 'w', encoding='utf-8-sig', newline='') as f: - f.write(content) - - -# --- ID allocator --- -_next_id = 0 - -def new_id(): - global _next_id - _next_id += 1 - return _next_id - - -# Уникальность имён внутри коллекции (1С: элементы/реквизиты/команды/параметры/колонки — каждое своё -# пространство имён). Дубль → битый XML, форма не открывается, поэтому fail-fast. -_seen_element_names = set() # пул имён элементов (глобально по всей форме) - -def _ensure_unique(name, seen, kind): - if name in seen: - print(f"[ERROR] Duplicate {kind} name '{name}' — names must be unique within their collection in a 1C form (set a unique 'name')", file=sys.stderr) - sys.exit(1) - seen.add(name) - - -# --- Event handler name generator --- - -EVENT_SUFFIX_MAP = { - "OnChange": "\u041f\u0440\u0438\u0418\u0437\u043c\u0435\u043d\u0435\u043d\u0438\u0438", - "StartChoice": "\u041d\u0430\u0447\u0430\u043b\u043e\u0412\u044b\u0431\u043e\u0440\u0430", - "ChoiceProcessing": "\u041e\u0431\u0440\u0430\u0431\u043e\u0442\u043a\u0430\u0412\u044b\u0431\u043e\u0440\u0430", - "AutoComplete": "\u0410\u0432\u0442\u043e\u041f\u043e\u0434\u0431\u043e\u0440", - "Clearing": "\u041e\u0447\u0438\u0441\u0442\u043a\u0430", - "Opening": "\u041e\u0442\u043a\u0440\u044b\u0442\u0438\u0435", - "Click": "\u041d\u0430\u0436\u0430\u0442\u0438\u0435", - "OnActivateRow": "\u041f\u0440\u0438\u0410\u043a\u0442\u0438\u0432\u0438\u0437\u0430\u0446\u0438\u0438\u0421\u0442\u0440\u043e\u043a\u0438", - "BeforeAddRow": "\u041f\u0435\u0440\u0435\u0434\u041d\u0430\u0447\u0430\u043b\u043e\u043c\u0414\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0438\u044f", - "BeforeDeleteRow": "\u041f\u0435\u0440\u0435\u0434\u0423\u0434\u0430\u043b\u0435\u043d\u0438\u0435\u043c", - "BeforeRowChange": "\u041f\u0435\u0440\u0435\u0434\u041d\u0430\u0447\u0430\u043b\u043e\u043c\u0418\u0437\u043c\u0435\u043d\u0435\u043d\u0438\u044f", - "OnStartEdit": "\u041f\u0440\u0438\u041d\u0430\u0447\u0430\u043b\u0435\u0420\u0435\u0434\u0430\u043a\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044f", - "OnEndEdit": "\u041f\u0440\u0438\u041e\u043a\u043e\u043d\u0447\u0430\u043d\u0438\u0438\u0420\u0435\u0434\u0430\u043a\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044f", - "Selection": "\u0412\u044b\u0431\u043e\u0440\u0421\u0442\u0440\u043e\u043a\u0438", - "OnCurrentPageChange": "\u041f\u0440\u0438\u0421\u043c\u0435\u043d\u0435\u0421\u0442\u0440\u0430\u043d\u0438\u0446\u044b", - "TextEditEnd": "\u041e\u043a\u043e\u043d\u0447\u0430\u043d\u0438\u0435\u0412\u0432\u043e\u0434\u0430\u0422\u0435\u043a\u0441\u0442\u0430", - "URLProcessing": "\u041e\u0431\u0440\u0430\u0431\u043e\u0442\u043a\u0430\u041d\u0430\u0432\u0438\u0433\u0430\u0446\u0438\u043e\u043d\u043d\u043e\u0439\u0421\u0441\u044b\u043b\u043a\u0438", - "DragStart": "\u041d\u0430\u0447\u0430\u043b\u043e\u041f\u0435\u0440\u0435\u0442\u0430\u0441\u043a\u0438\u0432\u0430\u043d\u0438\u044f", - "Drag": "\u041f\u0435\u0440\u0435\u0442\u0430\u0441\u043a\u0438\u0432\u0430\u043d\u0438\u0435", - "DragCheck": "\u041f\u0440\u043e\u0432\u0435\u0440\u043a\u0430\u041f\u0435\u0440\u0435\u0442\u0430\u0441\u043a\u0438\u0432\u0430\u043d\u0438\u044f", - "Drop": "\u041f\u043e\u043c\u0435\u0449\u0435\u043d\u0438\u0435", - "AfterDeleteRow": "\u041f\u043e\u0441\u043b\u0435\u0423\u0434\u0430\u043b\u0435\u043d\u0438\u044f", -} - -KNOWN_EVENTS = { - "input": ["OnChange", "StartChoice", "ChoiceProcessing", "AutoComplete", "TextEditEnd", "Clearing", "Creating", "EditTextChange"], - "check": ["OnChange"], - "radio": ["OnChange"], - "label": ["Click", "URLProcessing"], - "labelField": ["OnChange", "StartChoice", "ChoiceProcessing", "Click", "URLProcessing", "Clearing"], - "table": ["Selection", "BeforeAddRow", "AfterDeleteRow", "BeforeDeleteRow", "OnActivateRow", "OnEditEnd", "OnStartEdit", "BeforeRowChange", "BeforeEditEnd", "ValueChoice", "OnActivateCell", "OnActivateField", "Drag", "DragStart", "DragCheck", "DragEnd", "OnGetDataAtServer", "BeforeLoadUserSettingsAtServer", "OnUpdateUserSettingSetAtServer", "OnChange"], - "pages": ["OnCurrentPageChange"], - "page": ["OnCurrentPageChange"], - "button": ["Click"], - "picField": ["OnChange", "StartChoice", "ChoiceProcessing", "Click", "Clearing"], - "calendar": ["OnChange", "OnActivate"], - "picture": ["Click"], - "cmdBar": [], - "popup": [], - "group": [], -} - -KNOWN_FORM_EVENTS = [ - "OnCreateAtServer", "OnOpen", "BeforeClose", "OnClose", "NotificationProcessing", - "ChoiceProcessing", "OnReadAtServer", "AfterWriteAtServer", "BeforeWriteAtServer", - "AfterWrite", "BeforeWrite", "OnWriteAtServer", "FillCheckProcessingAtServer", - "OnLoadDataFromSettingsAtServer", "BeforeLoadDataFromSettingsAtServer", - "OnSaveDataInSettingsAtServer", "ExternalEvent", "OnReopen", "Opening", -] - -KNOWN_KEYS = { - "group", "columnGroup", "buttonGroup", "input", "check", "radio", "label", "labelField", "table", "pages", "page", - "button", "picture", "picField", "calendar", "cmdBar", "popup", - "showInHeader", - "radioButtonType", "choiceList", "columnsCount", "checkBoxType", "editMode", - "name", "path", "title", "tooltip", "tooltipRepresentation", "extendedTooltip", - "visible", "hidden", "enabled", "disabled", "readOnly", "userVisible", - "events", "on", "handlers", - "selectionMode", "showCurrentDate", "widthInMonths", "heightInMonths", "showMonthsPanel", - "titleLocation", "representation", "width", "height", - "horizontalStretch", "verticalStretch", "autoMaxWidth", "autoMaxHeight", - "maxWidth", "maxHeight", - "groupHorizontalAlign", "groupVerticalAlign", "horizontalAlign", - "multiLine", "passwordMode", "choiceButton", "clearButton", - "spinButton", "dropListButton", "markIncomplete", "skipOnInput", "inputHint", - "textEdit", "choiceList", - "wrap", "openButton", "listChoiceMode", "showInHeader", "showInFooter", - "extendedEditMultipleValues", "chooseType", "autoCellHeight", - "choiceButtonRepresentation", "footerHorizontalAlign", "headerHorizontalAlign", - "headerDataPath", "headerFormat", - "format", "editFormat", "choiceParameters", "choiceParameterLinks", "typeLink", - "hyperlink", "formatted", - "collapsedTitle", "showTitle", "united", "collapsed", "behavior", - "children", "columns", - "changeRowSet", "changeRowOrder", "autoInsertNewRow", "rowFilter", "header", "footer", - "commandBarLocation", "searchStringLocation", "viewStatusLocation", "searchControlLocation", - "excludedCommands", - "pagesRepresentation", - "type", "command", "commandName", "stdCommand", "parameter", "defaultButton", "locationInCommandBar", "displayImportance", - "commandBar", "contextMenu", "commandSource", - "src", "valuesPicture", "loadTransparent", "headerPicture", "footerPicture", - "autofill", - "choiceMode", "initialTreeView", "enableDrag", "enableStartDrag", - "rowSelectionMode", "verticalLines", "horizontalLines", - "rowPictureDataPath", "tableAutofill", "heightInTableRows", - "multipleChoice", "searchOnInput", "shortcut", - # dynamic-list table block - "defaultItem", "useAlternationRowColor", "fileDragMode", "autoRefresh", - "autoRefreshPeriod", "choiceFoldersAndItems", "restoreCurrentRow", "showRoot", - "allowRootChoice", "updateOnDataChange", "allowGettingCurrentRowURL", - "userSettingsGroup", "rowsPicture", - # AutoCommandBar-маркер (autofill heuristic) на элементе/таблице - "autoCmdBar", - # дополнения командной панели таблицы (тип-ключи + свойства) - "searchString", "viewStatus", "searchControl", "source", "horizontalLocation", "additions", - # generic-скаляры (pass-through) - "verticalAlign", "throughAlign", "enableContentChange", "pictureSize", "titleHeight", - "childItemsWidth", "showLeftMargin", "cellHyperlink", "viewMode", "verticalScrollBar", - "rowInputMode", "mask", "createButton", "fixingInTable", "verticalSpacing", - # InputField choice-скаляры - "choiceListButton", "quickChoice", "autoChoiceIncomplete", - "choiceForm", "choiceHistoryOnInput", "footerDataPath", "minValue", "maxValue", - # Button — пометка toggle-кнопки - "checked", - # спец-поля (документ/датчик/диаграмма) — тип-ключи + типоспец. скаляры - "spreadsheet", "html", "textDoc", "formattedDoc", "progressBar", "trackBar", - "chart", "ganttChart", "graphicalSchema", "planner", "periodField", "dendrogram", "ganttTable", - "showPercent", "largeStep", "markingStep", "step", - "horizontalScrollBar", "viewScalingMode", "output", "selectionShowMode", "protection", - "edit", "showGrid", "showGroups", "showHeaders", "showRowAndColumnNames", "showCellNames", - "pointerType", "drawingSelectionShowMode", "warningOnEditRepresentation", "markingAppearance", - # report-form контекст (generic-скаляры элементов) - "horizontalSpacing", "representationInContextMenu", "settingsNamedItemDetailedRepresentation", - # хвост: высота элемента списка / ширина выпадающего списка / картинка кнопки выбора / прозрачный пиксель - "itemHeight", "dropListWidth", "choiceButtonPicture", "transparentPixel", - # хвост CI-форм: динамический заголовок / расширенное редактирование / высота таблицы - "titleDataPath", "extendedEdit", "maxRowsCount", "autoMaxRowsCount", "heightControlVariant", - "warningOnEdit", "nonselectedPictureText", "editTextUpdate", "footerText", -} - -# picture/picField — НИЗКИЙ приоритет: 'picture' это и тип (PictureDecoration), и свойство-иконка -# у popup/button/cmdBar. Тип-ключ владельца (popup/button/…) должен выиграть. -# pages/page ПЕРЕД group: у Page/Pages ключ 'group' — это направление раскладки детей -# (Horizontal), а не тип UsualGroup. Реальная UsualGroup ключа page/pages не несёт. -TYPE_KEYS = ["columnGroup", "buttonGroup", "pages", "page", "group", "input", "check", "radio", "label", "labelField", "table", - "button", "calendar", "cmdBar", "popup", "searchString", "viewStatus", "searchControl", "picField", "picture", - "spreadsheet", "html", "textDoc", "formattedDoc", "progressBar", "trackBar", - "chart", "ganttChart", "graphicalSchema", "planner", "periodField", "dendrogram"] - -# Synonyms: model often writes XML name or Russian (ПолеПереключателя/RadioButtonField → radio) -ELEMENT_TYPE_SYNONYMS = { - "commandBar": "cmdBar", - "autoCommandBar": "autoCmdBar", - "КоманднаяПанель": "cmdBar", - "InputField": "input", - "ПолеВвода": "input", - "CheckBoxField": "check", - "ПолеФлажка": "check", - "RadioButtonField": "radio", - "ПолеПереключателя": "radio", - "radioButton": "radio", - "PictureField": "picField", - "ПолеКартинки": "picField", - "LabelField": "labelField", - "ПолеНадписи": "labelField", - "CalendarField": "calendar", - "ПолеКалендаря": "calendar", - "LabelDecoration": "label", - "Надпись": "label", - "PictureDecoration": "picture", - "Картинка": "picture", - "UsualGroup": "group", - "Группа": "group", - "ОбычнаяГруппа": "group", - "ColumnGroup": "columnGroup", - "ГруппаКолонок": "columnGroup", - "Pages": "pages", - "ГруппаСтраниц": "pages", - "Page": "page", - "Страница": "page", - "Table": "table", - "Таблица": "table", - "Button": "button", - "Кнопка": "button", - "Popup": "popup", - "ВсплывающееМеню": "popup", - # дополнения командной панели таблицы — forgiving: XML-тег/Type/рус.имя → канон - "SearchStringAddition": "searchString", - "SearchStringRepresentation": "searchString", - "строкаПоиска": "searchString", - "отображениеСтрокиПоиска": "searchString", - "Отображение строки поиска": "searchString", - "ViewStatusAddition": "viewStatus", - "ViewStatusRepresentation": "viewStatus", - "состояниеПросмотра": "viewStatus", - "Состояние просмотра": "viewStatus", - "SearchControlAddition": "searchControl", - "SearchControl": "searchControl", - "управлениеПоиском": "searchControl", - "Управление поиском": "searchControl", - # Спец-поля (документ/датчик) — XML-имя/рус. → канон - "SpreadSheetDocumentField": "spreadsheet", - "ПолеТабличногоДокумента": "spreadsheet", - "HTMLDocumentField": "html", - "ПолеHTMLДокумента": "html", - "TextDocumentField": "textDoc", - "ПолеТекстовогоДокумента": "textDoc", - "FormattedDocumentField": "formattedDoc", - "ПолеФорматированногоДокумента": "formattedDoc", - "ProgressBarField": "progressBar", - "ПолеИндикатора": "progressBar", - "TrackBarField": "trackBar", - "ПолеПолосыРегулирования": "trackBar", - "ChartField": "chart", - "ПолеДиаграммы": "chart", - "GanttChartField": "ganttChart", - "ПолеДиаграммыГанта": "ganttChart", - "GraphicalSchemaField": "graphicalSchema", - "ПолеГрафическойСхемы": "graphicalSchema", - "PlannerField": "planner", - "ПолеПланировщика": "planner", - "PeriodField": "periodField", - "ПолеПериода": "periodField", - "DendrogramField": "dendrogram", - "ПолеДендрограммы": "dendrogram", -} - -# Тип-синонимы, применяемые ТОЛЬКО к строковому значению (имя элемента); объект/массив -# у того же слова — companion-панель (свойство), см. normalize_panel_synonyms. -STR_ONLY_TYPE_SYNONYMS = {"commandBar", "autoCommandBar", "КоманднаяПанель"} - -# Companion-панели как СВОЙСТВА (значение объект/массив): синоним → каноника. -PANEL_SYNONYMS = { - 'commandBar': ['commandBar', 'autoCommandBar', 'AutoCommandBar', 'autoCmdBar', 'cmdBar', 'КоманднаяПанель'], - 'contextMenu': ['contextMenu', 'ContextMenu', 'КонтекстноеМеню'], -} - - -def normalize_panel_synonyms(el): - if not isinstance(el, dict): - return - for canon, syns in PANEL_SYNONYMS.items(): - for syn in syns: - if syn in el and isinstance(el[syn], (list, dict)): - if syn != canon and canon not in el: - el[canon] = el.pop(syn) - break - - -# Maps Russian/English root of typed reference path to canonical English root -REF_ROOT_SYNONYMS = { - "Перечисление": "Enum", - "Справочник": "Catalog", - "Документ": "Document", - "ПланСчетов": "ChartOfAccounts", - "ПланВидовХарактеристик": "ChartOfCharacteristicTypes", - "ПланВидовРасчета": "ChartOfCalculationTypes", - "ПланВидовРасчёта": "ChartOfCalculationTypes", - "ПланОбмена": "ExchangePlan", - "БизнесПроцесс": "BusinessProcess", - "Задача": "Task", - "РегистрСведений": "InformationRegister", - "РегистрНакопления": "AccumulationRegister", - "РегистрБухгалтерии": "AccountingRegister", - "РегистрРасчета": "CalculationRegister", - "РегистрРасчёта": "CalculationRegister", - "ЖурналДокументов": "DocumentJournal", - "КритерийОтбора": "FilterCriterion", -} -ENUM_VALUE_SYNONYMS = {"EnumValue", "ЗначениеПеречисления"} - - -def normalize_meta_type_ref(ref): - # "Справочник.Контрагенты" → "Catalog.Контрагенты"; уже англ — без изменений - if not ref: - return ref - dot = ref.find('.') - if dot < 1: - return ref - root = ref[:dot] - if root in REF_ROOT_SYNONYMS: - return REF_ROOT_SYNONYMS[root] + ref[dot:] - return ref - - -def normalize_choice_value(value): - """Returns dict {xsi_type, text} for a choiceList item value.""" - if isinstance(value, bool): - return {"xsi_type": "xs:boolean", "text": "true" if value else "false"} - if isinstance(value, (int, float)): - return {"xsi_type": "xs:decimal", "text": str(value)} - - s = "" if value is None else str(value) - if not s: - return {"xsi_type": "xs:string", "text": ""} - - # ISO datetime ("2020-01-01T00:00:00") → xs:dateTime - if re.fullmatch(r'\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}', s): - return {"xsi_type": "xs:dateTime", "text": s} - - parts = s.split(".") - if len(parts) >= 2: - root = parts[0] - canon_root = None - if root in REF_ROOT_SYNONYMS: - canon_root = REF_ROOT_SYNONYMS[root] - elif root in REF_ROOT_SYNONYMS.values(): - canon_root = root - - if canon_root: - type_name = parts[1] - normalized = None - if canon_root == "Enum": - if len(parts) == 3 and parts[2] == 'EmptyRef': - # "Enum.X.EmptyRef" — пустая ссылка, НЕ значение перечисления (без .EnumValue.) - normalized = f"Enum.{type_name}.EmptyRef" - elif len(parts) == 3: - normalized = f"Enum.{type_name}.EnumValue.{parts[2]}" - elif len(parts) >= 4: - member = parts[2] - if member in ENUM_VALUE_SYNONYMS: - rest = ".".join(parts[3:]) - else: - rest = ".".join(parts[2:]) - normalized = f"Enum.{type_name}.EnumValue.{rest}" - else: - if len(parts) >= 3: - tail = ".".join(parts[1:]) - normalized = f"{canon_root}.{tail}" - - if normalized: - return {"xsi_type": "xr:DesignTimeRef", "text": normalized} - - return {"xsi_type": "xs:string", "text": s} - - -def emit_choice_presentation(lines, pres, indent): - """Accepts None/empty → ; str → ru only; dict → multi-lang.""" - if pres is None or (isinstance(pres, str) and pres == ""): - lines.append(f"{indent}") - return - - if isinstance(pres, str): - pairs = [("ru", pres)] - elif isinstance(pres, dict): - pairs = [(str(k), str(v)) for k, v in pres.items()] - else: - pairs = [("ru", str(pres))] - - lines.append(f"{indent}") - for lang, content in pairs: - lines.append(f"{indent}\t") - lines.append(f"{indent}\t\t{lang}") - lines.append(f"{indent}\t\t{esc_xml(content)}") - lines.append(f"{indent}\t") - lines.append(f"{indent}") - - -def choice_value_tag(norm): - # для choiceList/choiceParameters: пустой текст → самозакрывающийся тег (зеркало платформы). - if not norm["text"]: - return f'' - return f'{esc_xml(norm["text"])}' - - -def emit_choice_list(lines, el, indent): - # — у RadioButtonField и InputField. Элемент: { value, presentation?/title? }. - choice_list = el.get('choiceList') or [] - if not choice_list: - return - lines.append(f'{indent}') - item_indent = f'{indent}\t' - for item in choice_list: - if not isinstance(item, dict): - continue - val_raw = item.get('value', item.get('значение')) - has_pres = any(k in item for k in ('presentation', 'представление', 'title')) - pres_raw = item.get('presentation', item.get('представление', item.get('title'))) - - # valueType: явный xsi:type значения (системное перечисление ent:*, иной не-примитив) — - # переопределяет авто-детект (normalize_choice_value вывела бы xs:string). - vt_raw = item.get('valueType') - if vt_raw: - norm = {'xsi_type': str(vt_raw), 'text': '' if val_raw is None else str(val_raw)} - else: - norm = normalize_choice_value(val_raw) - - if not has_pres: - if norm['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{choice_value_tag(norm)}') - lines.append(f'{val_indent}') - lines.append(f'{item_indent}') - lines.append(f'{indent}') - - -def get_el_prop(obj, names): - # Читает свойство из dict по списку синонимов (первый найденный, иначе None). - if not isinstance(obj, dict): - return None - for n in names: - if n in obj: - return obj[n] - return None - - -def to_scalar_literal(s): - # Литерал shorthand → тип: true/false → bool, целое/дробное → число, иначе строка. - t = str(s).strip() - if t.lower() == 'true': - return True - if t.lower() == 'false': - return False - if re.fullmatch(r'-?\d+', t): - return int(t) - if re.fullmatch(r'-?\d+\.\d+', t): - return float(t) - return t - - -def from_choice_param_shorthand(s): - # "name=value" либо "name=v1, v2, …" (запятые → массив). → {name, value}. - eq = s.find('=') - if eq < 0: - return {'name': s.strip()} - name = s[:eq].strip() - rest = s[eq + 1:] - if ',' in rest: - return {'name': name, 'value': [to_scalar_literal(p) for p in rest.split(',')]} - return {'name': name, 'value': to_scalar_literal(rest)} - - -def from_choice_param_link_shorthand(s): - # "name=dataPath" либо "name=dataPath:DontChange". → {name, dataPath, valueChange?}. - eq = s.find('=') - if eq < 0: - return {'name': s.strip()} - o = {'name': s[:eq].strip()} - rest = s[eq + 1:].strip() - m = re.fullmatch(r'(.*):(Clear|DontChange|очистить|неизменять)', rest, re.IGNORECASE) - if m: - o['dataPath'] = m.group(1).strip() - o['valueChange'] = m.group(2) - else: - o['dataPath'] = rest - return o - - -def from_type_link_shorthand(s): - # "dataPath" либо "dataPath#linkItem". → {dataPath, linkItem}. - m = re.fullmatch(r'(.*)#(\d+)', str(s)) - if m: - return {'dataPath': m.group(1).strip(), 'linkItem': int(m.group(2))} - return {'dataPath': str(s).strip()} - - -def emit_choice_param_value(lines, value, indent): - # Внутреннее значение параметра выбора (FormChoiceListDesTimeValue): + . - # Скаляр → один Value; массив → v8:FixedArray из вложенных FormChoiceListDesTimeValue. - lines.append(f'{indent}') - if isinstance(value, (list, tuple)): - lines.append(f'{indent}') - for v in value: - norm = normalize_choice_value(v) - lines.append(f'{indent}\t') - lines.append(f'{indent}\t\t') - lines.append(f'{indent}\t\t{choice_value_tag(norm)}') - lines.append(f'{indent}\t') - lines.append(f'{indent}') - else: - norm = normalize_choice_value(value) - lines.append(f'{indent}{choice_value_tag(norm)}') - - -def emit_choice_parameters(lines, el, indent): - # (параметры выбора поля ввода) — [{name, value}]. value через - # normalize_choice_value; массив значений → FixedArray. Рус. синонимы имя/значение. - cp = el.get('choiceParameters') or [] - if not cp: - return - lines.append(f'{indent}') - for item in cp: - if isinstance(item, str): - item = from_choice_param_shorthand(item) - name = get_el_prop(item, ('name', 'имя')) - has_val = isinstance(item, dict) and ('value' in item or 'значение' in item) - val = get_el_prop(item, ('value', 'значение')) - name_s = '' if name is None else str(name) - lines.append(f'{indent}\t') - # Параметр выбора без значения → (платформа, 13 в корпусе); - # со значением (в т.ч. пустой строкой) → FormChoiceListDesTimeValue. - if not has_val: - lines.append(f'{indent}\t\t') - else: - lines.append(f'{indent}\t\t') - emit_choice_param_value(lines, val, f'{indent}\t\t\t') - lines.append(f'{indent}\t\t') - lines.append(f'{indent}\t') - lines.append(f'{indent}') - - -def emit_choice_parameter_links(lines, el, indent): - # (связи параметров выбора) — [{name, dataPath, valueChange?}]. - # valueChange всегда эмитится, дефолт Clear; forgiving Clear/DontChange + рус. синонимы. - cpl = el.get('choiceParameterLinks') or [] - if not cpl: - return - lines.append(f'{indent}') - for lk in cpl: - if isinstance(lk, str): - lk = from_choice_param_link_shorthand(lk) - name = get_el_prop(lk, ('name', 'имя')) - dp = get_el_prop(lk, ('dataPath', 'path', 'путь')) - vc_raw = get_el_prop(lk, ('valueChange', 'режимИзменения')) - vc = 'Clear' - if vc_raw: - s = str(vc_raw).lower() - if s in ('clear', 'очистить', 'очистка'): - vc = 'Clear' - elif s in ('dontchange', 'неизменять', 'неменять', 'нет'): - vc = 'DontChange' - else: - vc = str(vc_raw) - name_s = '' if name is None else str(name) - dp_s = '' if dp is None else str(dp) - lines.append(f'{indent}\t') - lines.append(f'{indent}\t\t{esc_xml(name_s)}') - lines.append(f'{indent}\t\t{esc_xml(dp_s)}') - lines.append(f'{indent}\t\t{vc}') - lines.append(f'{indent}\t') - lines.append(f'{indent}') - - -def emit_type_link(lines, el, indent): - # (связь по типу) — {dataPath, linkItem}. linkItem дефолт 0. - tl = el.get('typeLink') - if not tl: - return - if isinstance(tl, str): - tl = from_type_link_shorthand(tl) - dp = get_el_prop(tl, ('dataPath', 'path', 'путь')) - li = get_el_prop(tl, ('linkItem', 'элементСвязи')) - if li is None: - li = 0 - dp_s = '' if dp is None else str(dp) - lines.append(f'{indent}') - lines.append(f'{indent}\t{esc_xml(dp_s)}') - lines.append(f'{indent}\t{li}') - lines.append(f'{indent}') - - -def normalize_radio_button_type(raw): - if not raw: - return "Auto" - s = str(raw).strip().lower() - if s in ("auto", "авто"): - return "Auto" - if s in ("radiobutton", "radiobuttons", "переключатель", "радио"): - return "RadioButtons" - if s in ("tumbler", "тумблер"): - return "Tumbler" - return str(raw).strip() - - -def get_handler_name(element_name, event_name): - suffix = EVENT_SUFFIX_MAP.get(event_name) - if suffix: - return f"{element_name}{suffix}" - return f"{element_name}{event_name}" - - -def get_element_name(el, type_key): - if el.get('name'): - return str(el['name']) - return str(el.get(type_key, '')) - - -# Собрать упорядоченный список событий элемента (имя, обработчик) из DSL. -# Основной формат: el['events'] = { Событие: ИмяОбработчика } (None/"" → авто-имя по конвенции). -# Legacy (принимается ради совместимости): el['on'] (массив) + el['handlers'] (переопределение имён). -def get_event_pairs(el, element_name): - pairs = [] - events = el.get('events') - if events: - for ev_name, val in events.items(): - handler = '' if val is None else str(val) - if not handler: - handler = get_handler_name(element_name, ev_name) - pairs.append((ev_name, handler)) - elif el.get('on'): - handlers = el.get('handlers') or {} - for evt in el['on']: - evt_name = str(evt) - if handlers.get(evt_name): - handler = str(handlers[evt_name]) - else: - handler = get_handler_name(element_name, evt_name) - pairs.append((evt_name, handler)) - return pairs - - -# Проверить, подключено ли событие к элементу (в любом из форматов). -def test_element_event(el, event_name): - events = el.get('events') - if events and event_name in events: - return True - return event_name in (el.get('on') or []) - - -def emit_events(lines, el, element_name, indent, type_key): - pairs = get_event_pairs(el, element_name) - if not pairs: - return - - # Validate event names - if type_key and type_key in KNOWN_EVENTS: - allowed = KNOWN_EVENTS[type_key] - for ev_name, _ in pairs: - if allowed and str(ev_name) not in allowed: - print(f"[WARN] Unknown event '{ev_name}' for {type_key} '{element_name}'. Known: {', '.join(allowed)}") - - lines.append(f"{indent}") - for ev_name, handler in pairs: - lines.append(f'{indent}\t{handler}') - lines.append(f"{indent}") - - -# Детектор «настоящей» inline-разметки (1С: ///… и ). Должен быть -# идентичен form-decompile/form-compile.ps1, иначе гибрид-раундтрип поедет. -_FMT_MARKUP_RE = re.compile(r'|<\s*(?:link|b|i|u|s|color|colorStyle|bgColor|bgColorStyle|font|fontSize|fontStyle|img)(?:\s|>)', re.I) - - -def _has_real_markup(text): - if text is None: - return False - vals = list(text.values()) if isinstance(text, dict) else [text] - return any(_FMT_MARKUP_RE.search(str(v)) for v in vals) - - -def resolve_ml_formatted(val): - # {text, formatted} = явный override; строка/мапа → авто-детект formatted - if isinstance(val, dict) and 'text' in val: - return val['text'], bool(val.get('formatted')) - return val, _has_real_markup(val) - - -# ExtendedTooltip — это LabelDecoration: own-content (layout/оформление/флаги/hyperlink) ±текст. -# Признак структурированной формы: объект с любым НЕ-текстовым ключом ({text,formatted}/{ru,en} → текст). -COMPANION_STRUCT_KEYS = { - 'width', 'autoMaxWidth', 'maxWidth', 'height', 'autoMaxHeight', 'maxHeight', 'verticalAlign', 'titleHeight', - 'horizontalStretch', 'verticalStretch', 'horizontalAlign', 'groupHorizontalAlign', 'groupVerticalAlign', - 'visible', 'hidden', 'enabled', 'disabled', 'hyperlink', 'events', 'tooltip', - 'textColor', 'backColor', 'borderColor', 'font', 'border', 'цветтекста', 'цветфона', 'цветрамки', 'шрифт', 'рамка', -} - - -def emit_companion_title(lines, content, indent): - text, fmt = resolve_ml_formatted(content) - lines.append(f'{indent}') - emit_ml_items(lines, f'{indent}\t', text) - lines.append(f'{indent}') - - -def emit_companion(lines, tag, name, indent, content=None): - cid = new_id() - has_content = content is not None and not (isinstance(content, str) and content == '') - if not has_content: - lines.append(f'{indent}<{tag} name="{name}" id="{cid}"/>') - return - inner = f'{indent}\t' - lines.append(f'{indent}<{tag} name="{name}" id="{cid}">') - if isinstance(content, dict) and any(k in content for k in COMPANION_STRUCT_KEYS): - # own-content ПЕРЕД Title (в корпусе layout-first 582 vs 10). - emit_common_flags(lines, content, inner) - if content.get('hyperlink') is True: - lines.append(f'{inner}true') - emit_layout(lines, content, inner) - emit_appearance(lines, content, inner, 'decoration') - if 'text' in content: - emit_companion_title(lines, content, inner) - # ToolTip компаньона (подсказка самой расширенной подсказки) — после Title (порядок схемы LabelDecoration) - if content.get('tooltip'): - emit_mltext(lines, inner, 'ToolTip', content['tooltip']) - # События компаньона (ExtendedTooltip = LabelDecoration: напр. URLProcessing у hyperlink-подсказки) - emit_events(lines, content, name, inner, 'label') - else: - emit_companion_title(lines, content, inner) - lines.append(f'{indent}') - - -def emit_companion_panel(lines, tag, name, indent, panel): - # Companion-командная-панель (ContextMenu/AutoCommandBar) с контентом: { autofill?, horizontalAlign?, children?[] } - # или массив = shorthand для { children }. Пусто/нет → self-closing. - cid = new_id() - autofill = None - halign = None - children = None - if isinstance(panel, list): - children = panel - elif panel is not None: - if panel.get('autofill') is not None: - autofill = bool(panel.get('autofill')) - if panel.get('horizontalAlign'): - halign = str(panel.get('horizontalAlign')) - children = panel.get('children') - has_children = bool(children) and len(children) > 0 - # Платформа пишет только при false; true = дефолт (тег опускается). - emit_af_false = (autofill is False) - if not emit_af_false and not has_children and not halign: - lines.append(f'{indent}<{tag} name="{name}" id="{cid}"/>') - return - lines.append(f'{indent}<{tag} name="{name}" id="{cid}">') - if halign: - lines.append(f'{indent}\t{halign}') - if emit_af_false: - lines.append(f'{indent}\tfalse') - if has_children: - lines.append(f'{indent}\t') - for c in children: - emit_element(lines, c, f'{indent}\t\t', in_cmd_bar=True) - lines.append(f'{indent}\t') - lines.append(f'{indent}') - - -# Дополнения командной панели таблицы: тип DSL → XML-тег + AdditionSource.Type + суффикс имени. -ADDITION_TYPE_MAP = { - 'searchString': {'tag': 'SearchStringAddition', 'type': 'SearchStringRepresentation', 'suffix': 'СтрокаПоиска'}, - 'viewStatus': {'tag': 'ViewStatusAddition', 'type': 'ViewStatusRepresentation', 'suffix': 'СостояниеПросмотра'}, - 'searchControl': {'tag': 'SearchControlAddition', 'type': 'SearchControl', 'suffix': 'УправлениеПоиском'}, -} -ADDITION_KEY_SYNONYMS = { - 'searchString': ['SearchStringAddition', 'SearchStringRepresentation', 'строкаПоиска', 'отображениеСтрокиПоиска'], - 'viewStatus': ['ViewStatusAddition', 'ViewStatusRepresentation', 'состояниеПросмотра'], - 'searchControl': ['SearchControlAddition', 'SearchControl', 'управлениеПоиском'], -} -# Имя текущей таблицы — дефолт source для кастомных дополнений в commandBar. -_current_table_name = {'name': None} - - -def get_hlocation(el): - # HorizontalLocation: auto (дефолт, опускаем) / left / right; forgiving + рус. - if not isinstance(el, dict): - return None - v = el.get('horizontalLocation') - if not v: - return None - s = str(v).lower() - if s in ('auto', 'авто'): - return None - if s in ('left', 'слева', 'лево'): - return 'Left' - if s in ('right', 'справа', 'право'): - return 'Right' - if s in ('center', 'центр', 'по центру'): - return 'Center' - return str(v) - - -def emit_addition_body(lines, props, source, src_type, add_name, indent): - # Тело дополнения: AdditionSource + свойства (как у поля) + companions. props может быть None. - inner = f'{indent}\t' - lines.append(f'{inner}') - lines.append(f'{inner}\t{source}') - lines.append(f'{inner}\t{src_type}') - lines.append(f'{inner}') - if props: - if props.get('title'): - emit_mltext(lines, inner, 'Title', props['title']) - emit_common_flags(lines, props, inner) - if props.get('tooltip'): - emit_mltext(lines, inner, 'ToolTip', props['tooltip']) - if props.get('tooltipRepresentation'): - lines.append(f'{inner}{props["tooltipRepresentation"]}') - hl = get_hlocation(props) - if hl: - lines.append(f'{inner}{hl}') - emit_layout(lines, props, inner) - emit_appearance(lines, props, inner, 'field') - emit_companion(lines, 'ContextMenu', f'{add_name}КонтекстноеМеню', inner) - emit_companion(lines, 'ExtendedTooltip', f'{add_name}РасширеннаяПодсказка', inner) - - -def emit_addition(lines, el, name, eid, type_key, indent): - # Кастомное дополнение (тип-элемент в commandBar): source дефолтит в текущую таблицу. - m = ADDITION_TYPE_MAP[type_key] - source = el.get('source') or _current_table_name['name'] or '' - lines.append(f'{indent}<{m["tag"]} name="{name}" id="{eid}"{di_attr(el)}>') - emit_addition_body(lines, el, source, m['type'], name, indent) - lines.append(f'{indent}') - - -def emit_table_addition(lines, type_key, table_name, indent, override=None): - # Стандартное табличное дополнение (авто-генерация). override — объект отклонений из карты additions. - m = ADDITION_TYPE_MAP[type_key] - add_name = f'{table_name}{m["suffix"]}' - aid = new_id() - lines.append(f'{indent}<{m["tag"]} name="{add_name}" id="{aid}">') - emit_addition_body(lines, override, table_name, m['type'], add_name, indent) - lines.append(f'{indent}') - - -def get_addition_override(additions, type_key): - # Прочитать override-объект для типа из per-table карты additions (с синонимами). - if not isinstance(additions, dict): - return None - for k in [type_key] + ADDITION_KEY_SYNONYMS[type_key]: - if k in additions: - return additions[k] - return None - - -# Role-adjustable boolean (xr:Common + 0..N xr:Value name="Role.X"). -# Единый механизм платформы: UserVisible (элементы), View/Edit (атрибуты), Use (команды/кнопки). -# Значение DSL: скаляр bool → только ; объект { common, roles:{ Имя: bool } } → +пер-ролевые исключения. -# Имя роли принимаем с/без префикса "Role." (forgiving); на выход всегда с префиксом. -def emit_xr_flag(lines, tag, val, indent): - if val is None: - return - if isinstance(val, bool): - lines.append(f"{indent}<{tag}>") - lines.append(f"{indent}\t{'true' if val else 'false'}") - lines.append(f"{indent}") - return - # объектная форма { common, roles } - common = bool(val.get('common')) if val.get('common') is not None else False - lines.append(f"{indent}<{tag}>") - lines.append(f"{indent}\t{'true' if common else 'false'}") - roles = val.get('roles') - if roles: - for rname, rval in roles.items(): - # Forgiving: имя без префикса, с "Role." или кириллическим "Роль." → нормализуем в "Role.". - # Роль по GUID (заимствованная/расширение — name="" без префикса) эмитим как есть. - rn = re.sub(r'^(Role|Роль)\.', '', rname) - if not re.match(r'^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$', rn): - rn = "Role." + rn - lines.append(f"{indent}\t{'true' if rval else 'false'}") - lines.append(f"{indent}") - - -def emit_common_flags(lines, el, indent): - if el.get('visible') is False or el.get('hidden') is True: - lines.append(f"{indent}false") - if el.get('userVisible') is not None: - emit_xr_flag(lines, 'UserVisible', el.get('userVisible'), indent) - if el.get('enabled') is False or el.get('disabled') is True: - lines.append(f"{indent}false") - if el.get('readOnly') is True: - lines.append(f"{indent}true") - - -# Общие свойства элемента (любой тип, включая Button/cmdBar): default/skip/drag. -def emit_common_element_props(lines, el, indent): - if el.get('defaultItem') is True: - lines.append(f"{indent}true") - if 'skipOnInput' in el and el['skipOnInput'] is not None: - siv = 'true' if el['skipOnInput'] is True else 'false' - lines.append(f"{indent}{siv}") - # EnableStartDrag — фактическое значение (платформа эмитит и явный false, напр. SpreadSheet) - if el.get('enableStartDrag') is not None: - lines.append(f'{indent}{"true" if el["enableStartDrag"] else "false"}') - if el.get('fileDragMode'): - lines.append(f"{indent}{el['fileDragMode']}") - # Cell-свойства поля в таблице (общие для Input/Label/Picture/CheckBox): захват «как есть» - for key, tag in (('showInHeader', 'ShowInHeader'), ('showInFooter', 'ShowInFooter'), ('autoCellHeight', 'AutoCellHeight')): - if el.get(key) is not None: - lines.append(f'{indent}<{tag}>{"true" if el[key] else "false"}') - # Динамический заголовок колонки-группы из данных (HeaderDataPath) — перед HeaderHorizontalAlign (порядок XSD) - if el.get('headerDataPath'): - lines.append(f"{indent}{esc_xml(str(el['headerDataPath']))}") - if el.get('footerHorizontalAlign'): - lines.append(f"{indent}{el['footerHorizontalAlign']}") - if el.get('headerHorizontalAlign'): - lines.append(f"{indent}{el['headerHorizontalAlign']}") - # Формат заголовка колонки-группы (ML-текст) — после HeaderHorizontalAlign (порядок XSD) - if el.get('headerFormat'): - emit_mltext(lines, indent, 'HeaderFormat', el['headerFormat']) - - -def emit_picture_ref(lines, val, pic_tag, indent): - """Картинка-ссылка с прозрачностью (HeaderPicture/FooterPicture/ValuesPicture/Page Picture). - Платформа ВСЕГДА эмитит → пишем всегда (false по умолчанию). - Значение: скаляр (Ref) ИЛИ объект {src, loadTransparent, transparentPixel}. - src с префиксом "abs:" → встроенная картинка ; иначе .""" - if not val: - return - tpx = None - if isinstance(val, str): - src, lt = val, False - else: - src = val.get('src') - lt = val.get('loadTransparent') is True - tpx = val.get('transparentPixel') - if not src: - return - src_str = str(src) - lines.append(f"{indent}<{pic_tag}>") - if src_str.startswith('abs:'): - lines.append(f"{indent}\t{esc_xml(src_str[4:])}") - else: - lines.append(f"{indent}\t{esc_xml(src_str)}") - lines.append(f'{indent}\t{"true" if lt else "false"}') - if tpx: - lines.append(f'{indent}\t') - lines.append(f"{indent}") - - -def emit_column_pics(lines, el, indent): - """Картинки заголовка/подвала колонки поля — по схеме сразу после , - перед тип-специфичными элементами и layout (порядок XDTO строгий именно здесь).""" - emit_picture_ref(lines, el.get('headerPicture'), 'HeaderPicture', indent) - emit_picture_ref(lines, el.get('footerPicture'), 'FooterPicture', indent) - - -def emit_command_picture(lines, pic, elem_lt, indent): - """ кнопки/попапа/команды. Дефолт LoadTransparent=true, отклонение false - (обратная конвенция относительно header/values-картинок). Прощающий ввод: - принимает скаляр (Ref) ИЛИ объект {src, loadTransparent} — на случай если модель - опишет картинку объектно по аналогии с headerPicture. elem_lt — legacy - элемент-уровневый ключ loadTransparent (если в объекте флаг не задан).""" - if not pic: - return - lt = None - tpx = None - if isinstance(pic, str): - src = pic - else: - src = pic.get('src') - if pic.get('loadTransparent') is not None: - lt = bool(pic.get('loadTransparent')) - tpx = pic.get('transparentPixel') - if not src: - return - if lt is None and elem_lt is not None: - lt = bool(elem_lt) - src_str = str(src) - lines.append(f'{indent}') - if src_str.startswith('abs:'): - lines.append(f'{indent}\t{esc_xml(src_str[4:])}') - else: - lines.append(f'{indent}\t{esc_xml(src_str)}') - lines.append(f'{indent}\t{"false" if lt is False else "true"}') - if tpx: - lines.append(f'{indent}\t') - lines.append(f'{indent}') - - -# --- Оформление элемента: цвета / шрифты / граница (зеркало form-compile.ps1 Emit-Appearance) --- -# Прямые свойства элемента (// + header/footer у полей). Ключи англ. -# camelCase 1:1 с тегами + приём рус. синонимов. Цвет — verbatim-строка (style:/web:/win:/#RRGGBB); -# шрифт — строка-ref/объект-атрибуты; граница — строка-ref/ -# объект {width,style}. Порядок тегов — XSD (профиль по базовому типу). -APPEARANCE_SPEC = { - 'titleTextColor': ('TitleTextColor', 'color'), - 'titleBackColor': ('TitleBackColor', 'color'), - 'titleFont': ('TitleFont', 'font'), - 'footerTextColor': ('FooterTextColor', 'color'), - 'footerBackColor': ('FooterBackColor', 'color'), - 'footerFont': ('FooterFont', 'font'), - 'textColor': ('TextColor', 'color'), - 'backColor': ('BackColor', 'color'), - 'borderColor': ('BorderColor', 'color'), - 'border': ('Border', 'border'), - 'font': ('Font', 'font'), -} -APPEARANCE_SYNONYMS = { - 'цветтекста': 'textColor', 'цветфона': 'backColor', 'цветрамки': 'borderColor', - 'цветтекстазаголовка': 'titleTextColor', 'цветфоназаголовка': 'titleBackColor', 'шрифтзаголовка': 'titleFont', - 'цветтекстаподвала': 'footerTextColor', 'цветфонаподвала': 'footerBackColor', 'шрифтподвала': 'footerFont', - 'шрифт': 'font', 'рамка': 'border', -} -# Синонимы ключей-свойств: русские имена свойств 1С (как в Конфигураторе) → канон. англ. ключ. -# Ключи нормализованы (lowercase, без пробелов); сопоставление в emit_element тоже. Англ. ключ -# работает всегда (доп. слой прощающего ввода). Видимость/Доступность НЕ включаем (hidden/disabled инвертирован). -PROP_SYNONYMS = { - 'пометка': 'checked', - 'кнопкавыбора': 'choiceButton', 'кнопкаочистки': 'clearButton', 'кнопкарегулирования': 'spinButton', - 'кнопкавыпадающегосписка': 'dropListButton', 'кнопкасписковоговыбора': 'choiceListButton', - 'кнопкаоткрытия': 'openButton', 'кнопкапоумолчанию': 'defaultButton', - 'быстрыйвыбор': 'quickChoice', 'формавыбора': 'choiceForm', 'историявыборапривводе': 'choiceHistoryOnInput', - 'выборгруппиэлементов': 'choiceFoldersAndItems', 'фиксациявтаблице': 'fixingInTable', - 'путькданнымподвала': 'footerDataPath', 'автоотметканезаполненного': 'markIncomplete', - 'многострочныйрежим': 'multiLine', 'режимпароля': 'passwordMode', 'переноспословам': 'wrap', - 'расположениезаголовка': 'titleLocation', 'пропускатьпривводе': 'skipOnInput', - 'заголовок': 'title', 'ширина': 'width', 'высота': 'height', 'подсказкаввода': 'inputHint', -} -APP_ORDER_FIELD =['titleTextColor', 'titleBackColor', 'titleFont', 'footerTextColor', 'footerBackColor', 'footerFont', 'textColor', 'backColor', 'borderColor', 'border', 'font'] -APP_ORDER_DECORATION = ['textColor', 'font', 'backColor', 'borderColor', 'border'] -APP_ORDER_BUTTON = ['textColor', 'backColor', 'borderColor', 'font'] - - -def get_appearance_value(el, canonical): - if not isinstance(el, dict): - return None - if canonical in el: - return el[canonical] - lowmap = {k.lower(): k for k in el.keys()} - if canonical.lower() in lowmap: - return el[lowmap[canonical.lower()]] - for syn, canon in APPEARANCE_SYNONYMS.items(): - if canon == canonical and syn in lowmap: - return el[lowmap[syn]] - return None - - -def emit_font_tag(lines, tag, val, indent): - if isinstance(val, str): - lines.append(f'{indent}<{tag} ref="{esc_xml(val)}" kind="StyleItem"/>') - return - attrs = [] - for a in ('ref', 'faceName', 'height', 'bold', 'italic', 'underline', 'strikeout', 'kind', 'scale'): - if a in val and val[a] is not None: - v = val[a] - if isinstance(v, bool): - v = 'true' if v else 'false' - attrs.append(f'{a}="{esc_xml(str(v))}"') - lines.append(f'{indent}<{tag} {" ".join(attrs)}/>') - - -def emit_border_tag(lines, val, indent): - if isinstance(val, str): - lines.append(f'{indent}') - return - if val.get('ref'): - lines.append(f'{indent}') - return - width = val['width'] if val.get('width') is not None else 1 - style = str(val['style']) if 'style' in val else None - lines.append(f'{indent}') - if style: - lines.append(f'{indent}\t{esc_xml(style)}') - lines.append(f'{indent}') - - -# ───────────────────────────────────────────────────────────────────────────── -# Planner design-time — зеркало Emit-PlannerSettings (ps1). -PLANNER_NS = 'http://v8.1c.ru/8.3/data/planner' -CHART_NS = 'http://v8.1c.ru/8.2/data/chart' - - -def _pl_get(o, k, default=None): - if isinstance(o, dict) and o.get(k) is not None: - return o[k] - return default - - -def _pl_bool(v): - if isinstance(v, bool): - return 'true' if v else 'false' - if str(v) == 'True': - return 'true' - if str(v) == 'False': - return 'false' - return str(v) - - -def emit_planner_color(lines, tag, o, key, ind): - lines.append(f'{ind}{esc_xml(str(_pl_get(o, key, "auto")))}') - - -def emit_planner_text(lines, tag, v, ind): - if v is None or str(v) == '': - lines.append(f'{ind}') - else: - lines.append(f'{ind}{esc_xml(str(v))}') - - -_PLANNER_REF_RE = re.compile( - r'^(Enum|Catalog|Document|ChartOfAccounts|ChartOfCalculationTypes|ChartOfCharacteristicTypes|ExchangePlan|BusinessProcess|Task)\.' - r'|\.EnumValue\.|EmptyRef$' - r'|^(Перечисление|Справочник|Документ|ПланСчетов|ПланВидовХарактеристик|ПланВидовРасчета|ПланОбмена|БизнесПроцесс|Задача)\.') - - -def test_planner_ref(v): - return bool(_PLANNER_REF_RE.search(str(v))) - - -def emit_planner_value(lines, v, ind): - if v is None or str(v) == '': - lines.append(f'{ind}') - return - t = 'xr:DesignTimeRef' if test_planner_ref(v) else 'xs:string' - lines.append(f'{ind}{esc_xml(str(v))}') - - -def emit_planner_font(lines, o, ind): - f = _pl_get(o, 'font') - if f is None: - lines.append(f'{ind}') - return - emit_font_tag(lines, 'pl:font', f, ind) - - -def emit_planner_border(lines, o, ind, key='border'): - b = _pl_get(o, key) - bw = _pl_get(b, 'width', 1) if b else 1 - bs = _pl_get(b, 'style', 'Single') if b else 'Single' - lines.append(f'{ind}') - lines.append(f'{ind}\t{esc_xml(str(bs))}') - lines.append(f'{ind}') - - -def emit_planner_level(lines, lv, cns, ind): - li = f'{ind}\t' - lines.append(f'{ind}') - lines.append(f'{li}{esc_xml(str(_pl_get(lv, "measure", "Hour")))}') - lines.append(f'{li}{_pl_get(lv, "interval", 1)}') - lines.append(f'{li}{_pl_bool(_pl_get(lv, "show", True))}') - line = _pl_get(lv, 'line') - lw = _pl_get(line, 'width', 1) if line else 1 - lg = _pl_get(line, 'gap', False) if line else False - lst = _pl_get(line, 'style', 'Solid') if line else 'Solid' - lines.append(f'{li}') - lines.append(f'{li}\t{esc_xml(str(lst))}') - lines.append(f'{li}') - lines.append(f'{li}{esc_xml(str(_pl_get(lv, "scaleColor", "auto")))}') - lines.append(f'{li}{esc_xml(str(_pl_get(lv, "dayFormatRule", "MonthDayWeekDay")))}') - fmt = _pl_get(lv, 'format') - if fmt is None: - fmt = {'#': 'DF="HH:mm"', 'ru': 'DF="HH:mm"'} - lines.append(f'{li}') - emit_ml_items(lines, f'{li}\t', fmt) - lines.append(f'{li}') - labels = _pl_get(lv, 'labels') - ticks = _pl_get(labels, 'ticks', 0) if labels else 0 - lines.append(f'{li}') - lines.append(f'{li}\t{ticks}') - lines.append(f'{li}') - lines.append(f'{li}{esc_xml(str(_pl_get(lv, "backColor", "auto")))}') - lines.append(f'{li}{esc_xml(str(_pl_get(lv, "textColor", "auto")))}') - lines.append(f'{li}{_pl_bool(_pl_get(lv, "showPereodicalLabels", True))}') - lines.append(f'{ind}') - - -def emit_planner_timescale(lines, ts, ind): - cns = CHART_NS - ci = f'{ind}\t' - lines.append(f'{ind}') - placement = _pl_get(ts, 'placement', 'Left') if ts else 'Left' - lines.append(f'{ci}{esc_xml(str(placement))}') - levels = _pl_get(ts, 'levels', []) if ts else [] - if not levels: - levels = [None] - for lv in levels: - emit_planner_level(lines, lv, cns, ci) - transp = _pl_get(ts, 'transparent', False) if ts else False - lines.append(f'{ci}{_pl_bool(transp)}') - tbc = _pl_get(ts, 'backColor', 'auto') if ts else 'auto' - ttc = _pl_get(ts, 'textColor', 'auto') if ts else 'auto' - tcl = _pl_get(ts, 'currentLevel', 0) if ts else 0 - lines.append(f'{ci}{esc_xml(str(tbc))}') - lines.append(f'{ci}{esc_xml(str(ttc))}') - lines.append(f'{ci}{tcl}') - lines.append(f'{ind}') - - -def emit_planner_item(lines, it, ind): - lines.append(f'{ind}') - ii = f'{ind}\t' - emit_planner_value(lines, _pl_get(it, 'value'), ii) - emit_planner_text(lines, 'text', _pl_get(it, 'text', ''), ii) - emit_planner_text(lines, 'tooltip', _pl_get(it, 'tooltip', ''), ii) - lines.append(f'{ii}{_pl_get(it, "begin", "0001-01-01T00:00:00")}') - lines.append(f'{ii}{_pl_get(it, "end", "0001-01-01T00:00:00")}') - emit_planner_color(lines, 'borderColor', it, 'borderColor', ii) - emit_planner_color(lines, 'backColor', it, 'backColor', ii) - emit_planner_color(lines, 'textColor', it, 'textColor', ii) - emit_planner_font(lines, it, ii) - lines.append(f'{ii}') - lines.append(f'{ii}{_pl_get(it, "replacementDate", "0001-01-01T00:00:00")}') - lines.append(f'{ii}{_pl_bool(_pl_get(it, "deleted", False))}') - iid = _pl_get(it, 'id') - if iid is None: - import uuid - iid = str(uuid.uuid4()) - lines.append(f'{ii}{iid}') - lines.append(f'{ii}{_pl_bool(_pl_get(it, "textFormatted", False))}') - emit_planner_border(lines, it, ii, 'border') - lines.append(f'{ii}{esc_xml(str(_pl_get(it, "editMode", "EnableEdit")))}') - lines.append(f'{ind}') - - -def emit_planner_dim_element(lines, el, ind): - lines.append(f'{ind}') - ii = f'{ind}\t' - emit_planner_value(lines, _pl_get(el, 'value'), ii) - emit_planner_text(lines, 'text', _pl_get(el, 'text', ''), ii) - emit_planner_color(lines, 'borderColor', el, 'borderColor', ii) - emit_planner_color(lines, 'backColor', el, 'backColor', ii) - emit_planner_color(lines, 'textColor', el, 'textColor', ii) - emit_planner_font(lines, el, ii) - for sub in _pl_get(el, 'elements', []): - emit_planner_dim_element(lines, sub, ii) - lines.append(f'{ii}{_pl_bool(_pl_get(el, "showOnlySubordinatesAreas", True))}') - lines.append(f'{ii}{_pl_bool(_pl_get(el, "textFormatted", False))}') - lines.append(f'{ind}') - - -def emit_planner_dimension(lines, d, ind): - lines.append(f'{ind}') - di = f'{ind}\t' - emit_planner_value(lines, _pl_get(d, 'value'), di) - emit_planner_text(lines, 'text', _pl_get(d, 'text', ''), di) - emit_planner_color(lines, 'borderColor', d, 'borderColor', di) - emit_planner_color(lines, 'backColor', d, 'backColor', di) - emit_planner_color(lines, 'textColor', d, 'textColor', di) - emit_planner_font(lines, d, di) - for el in _pl_get(d, 'elements', []): - emit_planner_dim_element(lines, el, di) - lines.append(f'{di}{_pl_bool(_pl_get(d, "textFormatted", False))}') - lines.append(f'{ind}') - - -def emit_planner_settings(lines, pl, ind): - lines.append(f'{ind}') - si = f'{ind}\t' - for it in _pl_get(pl, 'items', []): - emit_planner_item(lines, it, si) - for d in _pl_get(pl, 'dimensions', []): - emit_planner_dimension(lines, d, si) - emit_planner_color(lines, 'borderColor', pl, 'borderColor', si) - emit_planner_color(lines, 'backColor', pl, 'backColor', si) - emit_planner_color(lines, 'textColor', pl, 'textColor', si) - emit_planner_color(lines, 'lineColor', pl, 'lineColor', si) - emit_planner_font(lines, pl, si) - lines.append(f'{si}{_pl_get(pl, "beginOfRepresentationPeriod", "0001-01-01T00:00:00")}') - lines.append(f'{si}{_pl_get(pl, "endOfRepresentationPeriod", "0001-01-01T00:00:00")}') - lines.append(f'{si}{_pl_bool(_pl_get(pl, "alignElementsOfTimeScale", True))}') - lines.append(f'{si}{_pl_bool(_pl_get(pl, "displayTimeScaleWrapHeaders", True))}') - lines.append(f'{si}{_pl_bool(_pl_get(pl, "displayWrapHeaders", True))}') - wfmt = _pl_get(pl, 'timeScaleWrapHeadersFormat') - if wfmt is None: - wfmt = {'#': 'DLF="DD"', 'ru': 'DLF="DD"'} - emit_mltext(lines, si, 'pl:timeScaleWrapHeadersFormat', wfmt) - lines.append(f'{si}{esc_xml(str(_pl_get(pl, "periodicVariantUnit", "Day")))}') - lines.append(f'{si}{_pl_get(pl, "periodicVariantRepetition", 1)}') - lines.append(f'{si}{_pl_get(pl, "timeScaleWrapBeginIndent", 0)}') - lines.append(f'{si}{_pl_get(pl, "timeScaleWrapEndIndent", 0)}') - emit_planner_timescale(lines, _pl_get(pl, 'timeScale'), si) - period = _pl_get(pl, 'period') - if period: - lines.append(f'{si}') - lines.append(f'{si}\t{_pl_get(period, "begin", "0001-01-01T00:00:00")}') - lines.append(f'{si}\t{_pl_get(period, "end", "0001-01-01T00:00:00")}') - lines.append(f'{si}') - lines.append(f'{si}{_pl_bool(_pl_get(pl, "displayCurrentDate", True))}') - lines.append(f'{si}{esc_xml(str(_pl_get(pl, "itemsTimeRepresentation", "BeginTime")))}') - lines.append(f'{si}{esc_xml(str(_pl_get(pl, "itemsBehaviorWhenSpaceInsufficient", "CollapseItems")))}') - lines.append(f'{si}{_pl_bool(_pl_get(pl, "autoMinColumnWidth", True))}') - lines.append(f'{si}{_pl_bool(_pl_get(pl, "autoMinRowHeight", True))}') - lines.append(f'{si}{_pl_get(pl, "minColumnWidth", 0)}') - lines.append(f'{si}{_pl_get(pl, "minRowHeight", 0)}') - lines.append(f'{si}{esc_xml(str(_pl_get(pl, "fixDimensionsHeader", "auto")))}') - lines.append(f'{si}{esc_xml(str(_pl_get(pl, "fixTimeScaleHeader", "auto")))}') - emit_planner_border(lines, pl, si, 'border') - lines.append(f'{si}{esc_xml(str(_pl_get(pl, "newItemsTextType", "String")))}') - lines.append(f'{ind}') - - -# ───────────────────────────────────────────────────────────────────────────── -# Chart design-time — генерик-эмиттер (зеркало -# Build-ChartNode декомпилятора + Emit-ChartNode ps1). -CHART_ML_FIELDS = {'title', 'lbFormat', 'lbpFormat', 'vsFormat', 'dtFormat', 'dataSourceDescription', 'labelFormat', 'text'} -CHART_ATTR_FIELDS = {'gaugeQualityBands'} -CHART_FONT_KEYS = ('ref', 'faceName', 'height', 'bold', 'italic', 'underline', 'strikeout', 'kind', 'scale') - - -def emit_chart_node(lines, name, val, ind): - if name in CHART_ML_FIELDS: - if val is None or str(val) == '': - lines.append(f'{ind}') - return - lines.append(f'{ind}') - emit_ml_items(lines, f'{ind}\t', val) - lines.append(f'{ind}') - return - if isinstance(val, list): - for e in val: - emit_chart_node(lines, name, e, ind) - return - if isinstance(val, dict): - keys = list(val.keys()) - if name in CHART_ATTR_FIELDS: - attrs = ' '.join(f'{k}="{esc_xml(_pl_bool(val[k]) if isinstance(val[k], bool) else str(val[k]))}"' for k in keys) - lines.append(f'{ind}') - return - if 'gap' in val: - lines.append(f'{ind}') - lines.append(f'{ind}\t{esc_xml(str(val.get("style")))}') - lines.append(f'{ind}') - return - if 'style' in val and 'width' in val: - lines.append(f'{ind}') - lines.append(f'{ind}\t{esc_xml(str(val.get("style")))}') - lines.append(f'{ind}') - return - if any(fk in val for fk in CHART_FONT_KEYS): - attrs = ' '.join(f'{fk}="{esc_xml(_pl_bool(val[fk]) if isinstance(val[fk], bool) else str(val[fk]))}"' for fk in CHART_FONT_KEYS if fk in val) - lines.append(f'{ind}') - return - if not keys: - lines.append(f'{ind}') - return - lines.append(f'{ind}') - for k in keys: - emit_chart_node(lines, k, val[k], f'{ind}\t') - lines.append(f'{ind}') - return - if val is None or str(val) == '': - lines.append(f'{ind}') - return - if isinstance(val, bool): - lines.append(f'{ind}{_pl_bool(val)}') - return - lines.append(f'{ind}{esc_xml(str(val))}') - - -def emit_chart_settings(lines, chart, ind, ctype='d4p1:Chart'): - lines.append(f'{ind}') - for k in list(chart.keys()): - emit_chart_node(lines, k, chart[k], f'{ind}\t') - lines.append(f'{ind}') - - -def emit_appearance(lines, el, indent, profile='field'): - if not isinstance(el, dict): - return - order = {'decoration': APP_ORDER_DECORATION, 'button': APP_ORDER_BUTTON}.get(profile, APP_ORDER_FIELD) - for key in order: - val = get_appearance_value(el, key) - if val is None or (isinstance(val, str) and val == ''): - continue - tag, kind = APPEARANCE_SPEC[key] - if kind == 'color': - lines.append(f'{indent}<{tag}>{esc_xml(str(val))}') - elif kind == 'font': - emit_font_tag(lines, tag, val, indent) - else: - emit_border_tag(lines, val, indent) - - -# Простые скаляры элемента (pass-through, зеркало $script:genericScalars). kind bool/value. -GENERIC_SCALARS = [ - ('VerticalAlign', 'verticalAlign', 'value'), - ('ThroughAlign', 'throughAlign', 'value'), - ('EnableContentChange', 'enableContentChange', 'bool'), - ('PictureSize', 'pictureSize', 'value'), - ('TitleHeight', 'titleHeight', 'value'), - ('ChildItemsWidth', 'childItemsWidth', 'value'), - ('ShowLeftMargin', 'showLeftMargin', 'bool'), - ('CellHyperlink', 'cellHyperlink', 'bool'), - ('ViewMode', 'viewMode', 'value'), - ('VerticalScrollBar', 'verticalScrollBar', 'value'), - ('RowInputMode', 'rowInputMode', 'value'), - ('Mask', 'mask', 'value'), - ('CreateButton', 'createButton', 'bool'), - ('FixingInTable', 'fixingInTable', 'value'), - ('VerticalSpacing', 'verticalSpacing', 'value'), - # Spec-fields (document/gauge) - type-specific enum/bool scalars pass-through - ('HorizontalScrollBar', 'horizontalScrollBar', 'value'), - ('ViewScalingMode', 'viewScalingMode', 'value'), - ('Output', 'output', 'value'), - ('SelectionShowMode', 'selectionShowMode', 'value'), - ('PointerType', 'pointerType', 'value'), - ('DrawingSelectionShowMode', 'drawingSelectionShowMode', 'value'), - ('WarningOnEditRepresentation', 'warningOnEditRepresentation', 'value'), - ('MarkingAppearance', 'markingAppearance', 'value'), - ('Protection', 'protection', 'bool'), - ('Edit', 'edit', 'bool'), - ('ShowGrid', 'showGrid', 'bool'), - ('ShowGroups', 'showGroups', 'bool'), - ('ShowHeaders', 'showHeaders', 'bool'), - ('ShowRowAndColumnNames', 'showRowAndColumnNames', 'bool'), - ('ShowCellNames', 'showCellNames', 'bool'), - ('ShowPercent', 'showPercent', 'bool'), - # Report-form контекст: интервал группы / представление кнопки в контекстном меню / детальное представление настройки таблицы - ('HorizontalSpacing', 'horizontalSpacing', 'value'), - ('RepresentationInContextMenu', 'representationInContextMenu', 'value'), - ('SettingsNamedItemDetailedRepresentation', 'settingsNamedItemDetailedRepresentation', 'bool'), - # Хвост: высота элемента списка (radio) / ширина выпадающего списка (input) - ('ItemHeight', 'itemHeight', 'value'), - ('DropListWidth', 'dropListWidth', 'value'), - # Хвост CI-форм: динамический заголовок (Page/Group) / расширенное ред. (input) / высота таблицы по строкам - ('TitleDataPath', 'titleDataPath', 'value'), - ('ExtendedEdit', 'extendedEdit', 'bool'), - ('MaxRowsCount', 'maxRowsCount', 'value'), - ('AutoMaxRowsCount', 'autoMaxRowsCount', 'bool'), - ('HeightControlVariant', 'heightControlVariant', 'value'), - ('EditTextUpdate', 'editTextUpdate', 'value'), - # Корпусный хвост: свёртка группы / форма попапа / авто-добавление / выделение отрицательных / - # нач. позиция списка / высота списка выбора / три состояния / прокрутка страницы при сжатии - ('ControlRepresentation', 'controlRepresentation', 'value'), - ('ShapeRepresentation', 'shapeRepresentation', 'value'), - ('AutoAddIncomplete', 'autoAddIncomplete', 'bool'), - ('MarkNegatives', 'markNegatives', 'bool'), - ('InitialListView', 'initialListView', 'value'), - ('ChoiceListHeight', 'choiceListHeight', 'value'), - ('ThreeState', 'threeState', 'bool'), - ('ScrollOnCompress', 'scrollOnCompress', 'bool'), - # Сочетание клавиш — общее свойство (команда — отдельный путь) - ('Shortcut', 'shortcut', 'value'), - # Батч простых скаляров (input/radio/group/picDecoration/button; Table-специфичные — отдельно) - ('IncompleteChoiceMode', 'incompleteChoiceMode', 'value'), - ('EqualColumnsWidth', 'equalColumnsWidth', 'bool'), - ('ChildrenAlign', 'childrenAlign', 'value'), - ('ImageScale', 'imageScale', 'value'), - ('Zoomable', 'zoomable', 'bool'), - ('Shape', 'shape', 'value'), - ('PictureLocation', 'pictureLocation', 'value'), - # Равная ширина элементов (check/radio) / высота заголовка пункта (radio) - ('EqualItemsWidth', 'equalItemsWidth', 'bool'), - ('ItemTitleHeight', 'itemTitleHeight', 'value'), - # Спец-режим ввода текста (input, моб.: Email/PhoneNumber/...) — листовой enum-скаляр - ('SpecialTextInputMode', 'specialTextInputMode', 'value'), -] - - -def emit_generic_scalars(lines, el, indent): - for tag, key, kind in GENERIC_SCALARS: - if key not in el or el[key] is None: - continue - if kind == 'bool': - lines.append(f'{indent}<{tag}>{"true" if el[key] else "false"}') - else: - v = str(el[key]) - if v == '': - continue - lines.append(f'{indent}<{tag}>{esc_xml(v)}') - - -def emit_layout(lines, el, indent, skip_height=False, multi_line_default=False): - # Общие layout-свойства — применимы ко всем элементам. Порядок согласован - # с историческим выводом input/label, чтобы не сдвигать существующие снапшоты. - # skip_height: подавить (зарезервирован; Table теперь эмитит generic-ом + свой ). - # multi_line_default: input без явного autoMaxWidth при multiLine → AutoMaxWidth=false. - # CommandSet (отключённые команды редактора) — общее свойство поля; в схеме рано (после TitleLocation). - if el.get('excludedCommands') and len(el['excludedCommands']) > 0: - lines.append(f'{indent}') - for cmd in el['excludedCommands']: - lines.append(f'{indent}\t{cmd}') - lines.append(f'{indent}') - emit_common_element_props(lines, el, indent) - if 'autoMaxWidth' in el: - if el.get('autoMaxWidth') is False: - lines.append(f"{indent}false") - elif multi_line_default: - lines.append(f"{indent}false") - if el.get('maxWidth') is not None: - lines.append(f"{indent}{el['maxWidth']}") - if el.get('autoMaxHeight') is False: - lines.append(f"{indent}false") - if el.get('maxHeight') is not None: - lines.append(f"{indent}{el['maxHeight']}") - if el.get('width'): - lines.append(f"{indent}{el['width']}") - if not skip_height and el.get('height'): - lines.append(f"{indent}{el['height']}") - if el.get('horizontalStretch') is not None: - lines.append(f'{indent}{"true" if el["horizontalStretch"] else "false"}') - if el.get('verticalStretch') is not None: - lines.append(f'{indent}{"true" if el["verticalStretch"] else "false"}') - if el.get('groupHorizontalAlign'): - lines.append(f"{indent}{el['groupHorizontalAlign']}") - if el.get('groupVerticalAlign'): - lines.append(f"{indent}{el['groupVerticalAlign']}") - if el.get('horizontalAlign'): - lines.append(f"{indent}{el['horizontalAlign']}") - emit_generic_scalars(lines, el, indent) - - -def title_from_name(name): - """СуммаДокумента → 'Сумма документа'. НДСВключен → 'НДС включен'.""" - if not name: - return '' - s = re.sub(r'([А-ЯA-Z])([А-ЯA-Z][а-яa-z])', r'\1 \2', name) - s = re.sub(r'([а-яa-z0-9])([А-ЯA-Z])', r'\1 \2', s) - parts = s.split(' ') - if not parts: - return s - out = [parts[0]] - for p in parts[1:]: - out.append(p if (len(p) > 1 and p.isupper()) else p.lower()) - return ' '.join(out) - - -def emit_title(lines, el, name, indent, auto=False): - # Нет ключа title → авто-вывод из имени (помощь модели). - # Явный title "" (или None) → подавить. Явный непустой → как есть. - if 'title' in el: - if el.get('title'): - emit_mltext(lines, indent, 'Title', el['title']) - elif auto and name: - emit_mltext(lines, indent, 'Title', title_from_name(name)) - # ToolTip элемента (всплывающая подсказка) — по схеме сразу после Title. - if el.get('tooltip'): - emit_mltext(lines, indent, 'ToolTip', el['tooltip']) - # ToolTipRepresentation — режим показа подсказки (None/Button/ShowBottom/…), после ToolTip. - if el.get('tooltipRepresentation'): - lines.append(f'{indent}{el["tooltipRepresentation"]}') - - -_TITLE_LOC_MAP = {'none': 'None', 'left': 'Left', 'right': 'Right', 'top': 'Top', 'bottom': 'Bottom', 'auto': 'Auto'} - - -def map_title_loc(v): - return _TITLE_LOC_MAP.get(str(v).lower(), str(v)) - - -def emit_title_location(lines, el, indent, smart_default): - # Нет ключа → умный дефолт (Right/None), эмитится. "" → подавить (дефолт платформы). - # Значение → эмитить с маппингом регистра. - if 'titleLocation' in el: - if el.get('titleLocation'): - lines.append(f"{indent}{map_title_loc(el['titleLocation'])}") - elif smart_default: - lines.append(f"{indent}{smart_default}") - - -# --- Type emitter --- - -V8_TYPES = { - "ValueTable": "v8:ValueTable", - "ValueTree": "v8:ValueTree", - "ValueList": "v8:ValueListType", - "TypeDescription": "v8:TypeDescription", - "Universal": "v8:Universal", - "FixedArray": "v8:FixedArray", - "FixedStructure": "v8:FixedStructure", -} - -UI_TYPES = { - "FormattedString": "v8ui:FormattedString", - "Picture": "v8ui:Picture", - "Color": "v8ui:Color", - "Font": "v8ui:Font", -} - -DCS_MAP = { - "DataCompositionSettings": "dcsset:DataCompositionSettings", - "DataCompositionSchema": "dcssch:DataCompositionSchema", - "DataCompositionComparisonType": "dcscor:DataCompositionComparisonType", -} - -CFG_REF_PATTERN = re.compile( - r'^(CatalogRef|CatalogObject|DocumentRef|DocumentObject|EnumRef|' - r'ChartOfAccountsRef|ChartOfAccountsObject|ChartOfCharacteristicTypesRef|ChartOfCharacteristicTypesObject|' - r'ChartOfCalculationTypesRef|ChartOfCalculationTypesObject|' - r'ExchangePlanRef|ExchangePlanObject|BusinessProcessRef|BusinessProcessObject|TaskRef|TaskObject|' - r'InformationRegisterRecordSet|InformationRegisterRecordManager|' - r'AccumulationRegisterRecordSet|AccountingRegisterRecordSet|' - r'ConstantsSet|DataProcessorObject|ReportObject)\.' -) - -KNOWN_INVALID_TYPES = { - 'FormDataStructure': 'Runtime type. Use object type without cfg: prefix (e.g. CatalogObject.Контрагенты, DocumentObject.Приход)', - 'FormDataCollection': 'Runtime type. Use ValueTable', - 'FormDataTree': 'Runtime type. Use ValueTree', - 'FormDataTreeItem': 'Runtime type, not valid in XML', - 'FormDataCollectionItem': 'Runtime type, not valid in XML', - 'FormGroup': 'UI element type, not a data type', - 'FormField': 'UI element type, not a data type', - 'FormButton': 'UI element type, not a data type', - 'FormDecoration': 'UI element type, not a data type', - 'FormTable': 'UI element type, not a data type', -} - - -_FORM_TYPE_SYNONYMS = { - "строка": "string", "число": "decimal", "булево": "boolean", - "дата": "date", "датавремя": "dateTime", - "number": "decimal", "bool": "boolean", - "справочникссылка": "CatalogRef", "справочникобъект": "CatalogObject", - "документссылка": "DocumentRef", "документобъект": "DocumentObject", - "перечислениессылка": "EnumRef", - "плансчетовссылка": "ChartOfAccountsRef", - "планвидовхарактеристикссылка": "ChartOfCharacteristicTypesRef", - "планвидоврасчётассылка": "ChartOfCalculationTypesRef", - "планвидоврасчетассылка": "ChartOfCalculationTypesRef", - "планобменассылка": "ExchangePlanRef", - "бизнеспроцессссылка": "BusinessProcessRef", - "задачассылка": "TaskRef", - "определяемыйтип": "DefinedType", - "характеристика": "Characteristic", - "любаяссылка": "AnyRef", - "любаяссылкаиб": "AnyIBRef", - # Платформенные v8-типы (forgiving: англ. без префикса + рус.) → каноничный с префиксом v8: - "standardperiod": "v8:StandardPeriod", - "стандартныйпериод": "v8:StandardPeriod", - "standardbeginningdate": "v8:StandardBeginningDate", - "стандартнаядатаначала": "v8:StandardBeginningDate", - "uuid": "v8:UUID", - "уникальныйидентификатор": "v8:UUID", - "списокзначений": "ValueList", -} - - -def resolve_type_str(type_str): - if not type_str: - return type_str - # Lenient: strip leading cfg: prefix if user passed it (canonical form is without prefix) - if type_str.startswith('cfg:'): - type_str = type_str[4:] - m = re.match(r'^([^(]+)\((.+)\)$', type_str) - if m: - base, params = m.group(1).strip(), m.group(2) - r = _FORM_TYPE_SYNONYMS.get(base.lower()) - return f"{r}({params})" if r else type_str - if '.' in type_str: - i = type_str.index('.') - prefix, suffix = type_str[:i], type_str[i:] - r = _FORM_TYPE_SYNONYMS.get(prefix.lower()) - return f"{r}{suffix}" if r else type_str - r = _FORM_TYPE_SYNONYMS.get(type_str.lower()) - return r if r else type_str - - -def emit_single_type(lines, type_str, indent): - type_str = resolve_type_str(type_str) - # TypeId — тип, заданный глобальным стабильным GUID (, не ). Платформа так - # сериализует типы, чьё имя в этом контексте недоступно (определяемые/характеристики). GUID - # глобально стабилен → эмитим verbatim (как роль-по-GUID). Маркер декомпилятора: 'typeid:GUID'. - m = re.match(r'^typeid:([0-9a-fA-F-]{36})$', type_str) - if m: - lines.append(f'{indent}{m.group(1)}') - return - # boolean - if type_str == 'boolean': - lines.append(f'{indent}xs:boolean') - return - - # string or string(N) or string(N,fixed) (AllowedLength: Variable дефолт / Fixed) - m = re.match(r'^string(\((\d+)(\s*,\s*(fixed|variable))?\))?$', type_str, re.IGNORECASE) - if m: - length = m.group(2) if m.group(2) else '0' - al = 'Fixed' if (m.group(4) and m.group(4).lower() == 'fixed') else 'Variable' - lines.append(f'{indent}xs:string') - lines.append(f'{indent}') - lines.append(f'{indent}\t{length}') - lines.append(f'{indent}\t{al}') - lines.append(f'{indent}') - return - - # decimal(D,F) or decimal(D,F,nonneg) - m = re.match(r'^decimal\((\d+),(\d+)(,nonneg)?\)$', type_str) - if m: - digits = m.group(1) - fraction = m.group(2) - sign = 'Nonnegative' if m.group(3) else 'Any' - lines.append(f'{indent}xs:decimal') - lines.append(f'{indent}') - lines.append(f'{indent}\t{digits}') - lines.append(f'{indent}\t{fraction}') - lines.append(f'{indent}\t{sign}') - lines.append(f'{indent}') - return - - # date / dateTime / time - m = re.match(r'^(date|dateTime|time)$', type_str) - if m: - fractions_map = {'date': 'Date', 'dateTime': 'DateTime', 'time': 'Time'} - fractions = fractions_map[type_str] - lines.append(f'{indent}xs:dateTime') - lines.append(f'{indent}') - lines.append(f'{indent}\t{fractions}') - lines.append(f'{indent}') - return - - # V8 types - if type_str in V8_TYPES: - lines.append(f'{indent}{V8_TYPES[type_str]}') - return - - # UI types - if type_str in UI_TYPES: - lines.append(f'{indent}{UI_TYPES[type_str]}') - return - - # DCS types - if type_str.startswith('DataComposition'): - if type_str in DCS_MAP: - lines.append(f'{indent}{DCS_MAP[type_str]}') - return - - # Голые конфигурационные типы (cfg: без .Имя): дин-список, набор констант, общий объект отчёта. - # Корпус (acc+erp 8.3.24): DynamicList 5205, ConstantsSet 103, ReportObject 10. - if type_str in ('DynamicList', 'ConstantsSet', 'ReportObject'): - lines.append(f'{indent}cfg:{type_str}') - return - - # TypeSet (набор типов) → : определяемый тип / характеристика (именованные) - # + «любая ссылка вида» (голый ref-вид без .Имя). Развязка с обычным типом — по наличию точки. - if re.match(r'^(DefinedType|Characteristic)\.', type_str): - lines.append(f'{indent}cfg:{type_str}') - return - if re.match(r'^(AnyRef|AnyIBRef|CatalogRef|DocumentRef|EnumRef|ExchangePlanRef|TaskRef|BusinessProcessRef|ChartOfAccountsRef|ChartOfCharacteristicTypesRef|ChartOfCalculationTypesRef)$', type_str): - lines.append(f'{indent}cfg:{type_str}') - return - - # cfg: references - if CFG_REF_PATTERN.match(type_str): - lines.append(f'{indent}cfg:{type_str}') - return - - # Спец-типы платформы с собственным namespace (объявляется ЛОКАЛЬНО на ). - # Префикс d5p1 неоднозначен (5 разных URI), поэтому маппинг по полному значению типа. - # К таким типам привязаны спец-поля: mxl→SpreadSheetDocumentField, fd→FormattedDocumentField, - # d5p1:TextDocument→TextDocumentField, pdfdoc→PDF, pl→Planner, chart/geo/graphscheme/data-analysis. - special_type_ns = { - "mxl:SpreadsheetDocument": "http://v8.1c.ru/8.2/data/spreadsheet", - "fd:FormattedDocument": "http://v8.1c.ru/8.2/data/formatted-document", - "d5p1:TextDocument": "http://v8.1c.ru/8.1/data/txtedt", - "d5p1:Chart": "http://v8.1c.ru/8.2/data/chart", - "d5p1:GanttChart": "http://v8.1c.ru/8.2/data/chart", - "d5p1:Dendrogram": "http://v8.1c.ru/8.2/data/chart", - "d5p1:FlowchartContextType": "http://v8.1c.ru/8.2/data/graphscheme", - "d5p1:DataAnalysisTimeIntervalUnitType": "http://v8.1c.ru/8.2/data/data-analysis", - "d5p1:GeographicalSchema": "http://v8.1c.ru/8.2/data/geo", - "pdfdoc:PDFDocument": "http://v8.1c.ru/8.3/data/pdf", - "pl:Planner": "http://v8.1c.ru/8.3/data/planner", - } - if type_str in special_type_ns: - pref = type_str.split(':', 1)[0] - lines.append(f'{indent}{type_str}') - return - - # Fallback with validation - if type_str in KNOWN_INVALID_TYPES: - raise ValueError(f"Invalid form attribute type '{type_str}': {KNOWN_INVALID_TYPES[type_str]}") - # Платформенный тип с префиксом (v8:/v8ui:/xs:/dcs*:) — verbatim (напр. v8:UUID, v8:StandardPeriod). - if re.match(r'^(v8|v8ui|xs|ent|style|sys|web|win|dcs\w*):', type_str): - lines.append(f'{indent}{type_str}') - elif '.' in type_str: - lines.append(f'{indent}cfg:{type_str}') - else: - print(f"WARNING: Unrecognized bare type '{type_str}' — will be emitted without namespace prefix", file=sys.stderr) - lines.append(f'{indent}{type_str}') - - -def emit_type(lines, type_str, indent, tag="Type", tag_attrs=""): - # tag/tag_attrs — обёртка (по умолчанию ); для valueType ValueList вызывается с - # tag="Settings", tag_attrs=' xsi:type="v8:TypeDescription"'. - if not type_str: - lines.append(f'{indent}<{tag}{tag_attrs}/>') - return - - type_string = str(type_str) - parts = [p.strip() for p in re.split(r'[|+]', type_string)] - - lines.append(f'{indent}<{tag}{tag_attrs}>') - for part in parts: - emit_single_type(lines, part, f'{indent}\t') - lines.append(f'{indent}') - - -# --- Element emitters --- - -def emit_element(lines, el, indent, in_cmd_bar=False): - # Companion-панели (объект/массив-значение) → commandBar/contextMenu, до тип-синонимов. - normalize_panel_synonyms(el) - - # Silent synonyms: model often writes XML name or Russian (ПолеПереключателя/RadioButtonField → radio). - # commandBar/autoCommandBar/КоманднаяПанель → тип-элемент ТОЛЬКО при строковом значении (имя). - for src, dst in ELEMENT_TYPE_SYNONYMS.items(): - if src in el and dst not in el: - if src in STR_ONLY_TYPE_SYNONYMS and not isinstance(el[src], str): - continue - el[dst] = el.pop(src) - - # Синонимы ключей-свойств (русские имена 1С → канон. англ.). Case/space-insensitive. - # Канон побеждает: если задан и русский, и англ. ключ — англ. остаётся, русский отбрасываем. - for p_name in list(el.keys()): - norm = p_name.replace(' ', '').lower() - canon = PROP_SYNONYMS.get(norm) - if canon and p_name != canon: - val = el.pop(p_name) - if canon not in el: - el[canon] = val - - type_key = None - for key in TYPE_KEYS: - if el.get(key) is not None: - type_key = key - break - - if not type_key: - print("WARNING: Unknown element type, skipping", file=sys.stderr) - return - - # Validate known keys (внутренние маркеры на _ пропускаем). Оформление (цвета/шрифты/граница) - # проверяем против самих структур appearance — канонические ключи + forgiving-синонимы, чтобы - # allowlist не дрейфовал при добавлении новых. - for p_name in el.keys(): - if p_name.startswith('_'): - continue - if p_name not in KNOWN_KEYS and p_name not in APPEARANCE_SPEC and p_name not in APPEARANCE_SYNONYMS: - print(f"WARNING: Element '{el.get(type_key, '')}': unknown key '{p_name}' -- ignored. Check SKILL.md for valid keys.", file=sys.stderr) - - name = get_element_name(el, type_key) - _ensure_unique(name, _seen_element_names, 'element') - eid = new_id() - - emitters = { - 'group': emit_group, - 'columnGroup': emit_column_group, - 'buttonGroup': emit_button_group, - 'input': emit_input, - 'check': emit_check, - 'radio': emit_radio_button_field, - 'label': emit_label, - 'labelField': emit_label_field, - 'table': emit_table, - 'pages': emit_pages, - 'page': emit_page, - 'button': emit_button, - 'picture': emit_picture_decoration, - 'picField': emit_picture_field, - 'calendar': emit_calendar, - 'cmdBar': emit_command_bar, - 'popup': emit_popup, - 'searchString': lambda lines, el, name, eid, indent: emit_addition(lines, el, name, eid, 'searchString', indent), - 'viewStatus': lambda lines, el, name, eid, indent: emit_addition(lines, el, name, eid, 'viewStatus', indent), - 'searchControl': lambda lines, el, name, eid, indent: emit_addition(lines, el, name, eid, 'searchControl', indent), - 'spreadsheet': lambda lines, el, name, eid, indent: emit_simple_field(lines, el, name, eid, indent, 'SpreadSheetDocumentField', 'spreadsheet'), - 'html': lambda lines, el, name, eid, indent: emit_simple_field(lines, el, name, eid, indent, 'HTMLDocumentField', 'html'), - 'textDoc': lambda lines, el, name, eid, indent: emit_simple_field(lines, el, name, eid, indent, 'TextDocumentField', 'textDoc'), - 'formattedDoc': lambda lines, el, name, eid, indent: emit_simple_field(lines, el, name, eid, indent, 'FormattedDocumentField', 'formattedDoc'), - 'progressBar': lambda lines, el, name, eid, indent: emit_simple_field(lines, el, name, eid, indent, 'ProgressBarField', 'progressBar'), - 'trackBar': lambda lines, el, name, eid, indent: emit_simple_field(lines, el, name, eid, indent, 'TrackBarField', 'trackBar'), - 'chart': lambda lines, el, name, eid, indent: emit_simple_field(lines, el, name, eid, indent, 'ChartField', 'chart'), - 'graphicalSchema': lambda lines, el, name, eid, indent: emit_simple_field(lines, el, name, eid, indent, 'GraphicalSchemaField', 'graphicalSchema'), - 'planner': lambda lines, el, name, eid, indent: emit_simple_field(lines, el, name, eid, indent, 'PlannerField', 'planner'), - 'periodField': lambda lines, el, name, eid, indent: emit_simple_field(lines, el, name, eid, indent, 'PeriodField', 'periodField'), - 'dendrogram': lambda lines, el, name, eid, indent: emit_simple_field(lines, el, name, eid, indent, 'DendrogramField', 'dendrogram'), - 'ganttChart': emit_gantt_chart, - } - - emitter = emitters.get(type_key) - if emitter: - if type_key == 'button': - emitter(lines, el, name, eid, indent, in_cmd_bar=in_cmd_bar) - else: - emitter(lines, el, name, eid, indent) - - -def emit_group(lines, el, name, eid, indent): - lines.append(f'{indent}') - inner = f'{indent}\t' - - emit_title(lines, el, name, inner) - - # Group orientation - # Group orientation (направление). Legacy: group:'collapsible' = Vertical + behavior collapsible. - group_val = str(el.get('group', '')).lower() - orientation_map = { - 'horizontal': 'Horizontal', - 'vertical': 'Vertical', - 'alwayshorizontal': 'AlwaysHorizontal', - 'alwaysvertical': 'AlwaysVertical', - 'horizontalifpossible': 'HorizontalIfPossible', - 'collapsible': 'Vertical', - } - orientation = orientation_map.get(group_val) - if orientation: - lines.append(f'{inner}{orientation}') - - # Behavior: ключ behavior (usual/collapsible/popup) → ; отсутствие = Авто (не эмитим). - behavior_val = str(el['behavior']).lower() if el.get('behavior') else ('collapsible' if group_val == 'collapsible' else None) - bmap = {'usual': 'Usual', 'collapsible': 'Collapsible', 'popup': 'PopUp'} - if behavior_val and behavior_val in bmap: - lines.append(f'{inner}{bmap[behavior_val]}') - # Collapsed — у Collapsible и PopUp (не привязано к одному behavior) - if el.get('collapsed') is True: - lines.append(f'{inner}true') - - # Representation - if el.get('representation'): - repr_map = { - 'none': 'None', - 'normal': 'NormalSeparation', - 'weak': 'WeakSeparation', - 'strong': 'StrongSeparation', - } - repr_val = repr_map.get(str(el['representation']), str(el['representation'])) - lines.append(f'{inner}{repr_val}') - - # ShowTitle - if el.get('showTitle') is not None: - lines.append(f'{inner}{"true" if el["showTitle"] else "false"}') - # Заголовок свёрнутого представления (collapsible/popup) — мультиязычный текст - if el.get('collapsedTitle'): - emit_mltext(lines, inner, 'CollapsedRepresentationTitle', el['collapsedTitle']) - - # United - if el.get('united') is False: - lines.append(f'{inner}false') - - # Формат значения пути к данным заголовка (; парный к titleDataPath группы) - if el.get('format'): - emit_mltext(lines, inner, 'Format', el['format']) - if el.get('editFormat'): - emit_mltext(lines, inner, 'EditFormat', el['editFormat']) - - emit_common_flags(lines, el, inner) - emit_layout(lines, el, inner) - - # Оформление (цвета/шрифты/граница) — перед компаньоном - emit_appearance(lines, el, inner, 'field') - - # Companion: ExtendedTooltip - emit_companion(lines, 'ExtendedTooltip', f'{name}\u0420\u0430\u0441\u0448\u0438\u0440\u0435\u043d\u043d\u0430\u044f\u041f\u043e\u0434\u0441\u043a\u0430\u0437\u043a\u0430', inner, el.get('extendedTooltip')) - - # Children - if el.get('children') and len(el['children']) > 0: - lines.append(f'{inner}') - for child in el['children']: - emit_element(lines, child, f'{inner}\t') - lines.append(f'{inner}') - - lines.append(f'{indent}') - - -def emit_column_group(lines, el, name, eid, indent): - lines.append(f'{indent}') - inner = f'{indent}\t' - - emit_title(lines, el, name, inner) - - group_val = str(el.get('columnGroup', '')) - 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 not None: - lines.append(f'{inner}{"true" if el["showTitle"] else "false"}') - # showInHeader эмитится общим emit_common_element_props (через emit_layout) - - emit_common_flags(lines, el, inner) - emit_layout(lines, el, inner) - - # Картинка заголовка колонки-группы (после ShowInHeader/Layout, перед оформлением — порядок XSD) - emit_column_pics(lines, el, inner) - - # Оформление (цвета/шрифты/граница) — перед компаньоном - emit_appearance(lines, el, inner, 'field') - - emit_companion(lines, 'ExtendedTooltip', f'{name}РасширеннаяПодсказка', inner, el.get('extendedTooltip')) - - if el.get('children') and len(el['children']) > 0: - lines.append(f'{inner}') - for child in el['children']: - emit_element(lines, child, f'{inner}\t') - lines.append(f'{inner}') - - lines.append(f'{indent}') - - -def emit_input(lines, el, name, eid, indent): - lines.append(f'{indent}') - inner = f'{indent}\t' - - if el.get('path'): - lines.append(f'{inner}{el["path"]}') - - emit_title(lines, el, name, inner, auto=not el.get('path')) - emit_common_flags(lines, el, inner) - - if el.get('titleLocation'): - loc_map = {'none': 'None', 'left': 'Left', 'right': 'Right', 'top': 'Top', 'bottom': 'Bottom'} - loc = loc_map.get(str(el['titleLocation']), str(el['titleLocation'])) - lines.append(f'{inner}{loc}') - - if el.get('multiLine') is not None: - lines.append(f'{inner}{"true" if el["multiLine"] else "false"}') - if el.get('passwordMode') is not None: - lines.append(f'{inner}{"true" if el["passwordMode"] else "false"}') - # ChoiceButton — захват «как есть» (платформа эмитит явное значение; ref-поля выводят сама, - # декомпилятор фиксирует факт. значение). Нет ключа → не эмитим (не додумываем по событию). - if el.get('choiceButton') is not None: - lines.append(f'{inner}{"true" if el["choiceButton"] else "false"}') - # Кнопки поля ввода — захват «как есть» (платформа эмитит явное значение, в т.ч. false) - if el.get('clearButton') is not None: - lines.append(f'{inner}{"true" if el["clearButton"] else "false"}') - if el.get('spinButton') is not None: - lines.append(f'{inner}{"true" if el["spinButton"] else "false"}') - if el.get('dropListButton') is not None: - lines.append(f'{inner}{"true" if el["dropListButton"] else "false"}') - if el.get('choiceListButton') is not None: - lines.append(f'{inner}{"true" if el["choiceListButton"] else "false"}') - if el.get('markIncomplete') is not None: - lines.append(f'{inner}{"true" if el["markIncomplete"] else "false"}') - if el.get('editMode'): - lines.append(f'{inner}{el["editMode"]}') - emit_column_pics(lines, el, inner) - if el.get('textEdit') is False: - lines.append(f'{inner}false') - # InputField-специфичные скаляры (захват «как есть»: платформа эмитит явное не-дефолтное значение) - for key, tag in (('wrap', 'Wrap'), ('openButton', 'OpenButton'), ('listChoiceMode', 'ListChoiceMode'), - ('extendedEditMultipleValues', 'ExtendedEditMultipleValues'), ('chooseType', 'ChooseType'), - ('quickChoice', 'QuickChoice'), ('autoChoiceIncomplete', 'AutoChoiceIncomplete')): - if el.get(key) is not None: - lines.append(f'{inner}<{tag}>{"true" if el[key] else "false"}') - # Ограничение доступных типов (поле на составном типе): домен типов + явный набор. - # availableTypes — формат типа реквизита (§type); emit_type сам разбирает мультитип "a | b". - if el.get('typeDomainEnabled') is not None: - lines.append(f'{inner}{"true" if el["typeDomainEnabled"] else "false"}') - if el.get('availableTypes'): - emit_type(lines, el['availableTypes'], inner, tag='AvailableTypes') - # InputField-специфичные value-скаляры - for key, tag in (('choiceForm', 'ChoiceForm'), ('choiceHistoryOnInput', 'ChoiceHistoryOnInput'), - ('choiceFoldersAndItems', 'ChoiceFoldersAndItems'), ('footerDataPath', 'FooterDataPath')): - if el.get(key): - lines.append(f'{inner}<{tag}>{esc_xml(str(el[key]))}') - # MinValue/MaxValue — типизированное. JSON-число → xs:decimal, строка → xs:string (тип сохранён декомпилятором). - for key, tag in (('minValue', 'MinValue'), ('maxValue', 'MaxValue')): - if el.get(key) is not None: - mvt = 'xs:string' if isinstance(el[key], str) else 'xs:decimal' - lines.append(f'{inner}<{tag} xsi:type="{mvt}">{esc_xml(str(el[key]))}') - if el.get('choiceButtonRepresentation'): - lines.append(f'{inner}{el["choiceButtonRepresentation"]}') - emit_picture_ref(lines, el.get('choiceButtonPicture'), 'ChoiceButtonPicture', inner) - emit_layout(lines, el, inner, multi_line_default=(el.get('multiLine') is True)) - - if el.get('inputHint'): - emit_mltext(lines, inner, 'InputHint', el['inputHint']) - if el.get('warningOnEdit') is not None: - emit_mltext(lines, inner, 'WarningOnEdit', el['warningOnEdit']) - if el.get('footerText') is not None: - emit_mltext(lines, inner, 'FooterText', el['footerText']) - - # Формат / формат редактирования (LocalStringType — строка или {ru,en}) - if el.get('format'): - emit_mltext(lines, inner, 'Format', el['format']) - if el.get('editFormat'): - emit_mltext(lines, inner, 'EditFormat', el['editFormat']) - - emit_choice_list(lines, el, inner) - - # Связи по типу / связи параметров выбора / параметры выбора - emit_type_link(lines, el, inner) - emit_choice_parameter_links(lines, el, inner) - emit_choice_parameters(lines, el, inner) - - # Оформление (цвета/шрифты/граница) — перед компаньонами - emit_appearance(lines, el, inner, 'field') - - # Companions - emit_companion_panel(lines, 'ContextMenu', f'{name}\u041a\u043e\u043d\u0442\u0435\u043a\u0441\u0442\u043d\u043e\u0435\u041c\u0435\u043d\u044e', inner, el.get('contextMenu')) - emit_companion(lines, 'ExtendedTooltip', f'{name}\u0420\u0430\u0441\u0448\u0438\u0440\u0435\u043d\u043d\u0430\u044f\u041f\u043e\u0434\u0441\u043a\u0430\u0437\u043a\u0430', inner, el.get('extendedTooltip')) - - emit_events(lines, el, name, inner, 'input') - - lines.append(f'{indent}') - - -def emit_check(lines, el, name, eid, indent): - lines.append(f'{indent}') - inner = f'{indent}\t' - - if el.get('path'): - lines.append(f'{inner}{el["path"]}') - - emit_title(lines, el, name, inner, auto=not el.get('path')) - emit_common_flags(lines, el, inner) - - if el.get('editMode'): - lines.append(f'{inner}{el["editMode"]}') - emit_column_pics(lines, el, inner) - # CheckBoxType: нет ключа → умный дефолт Auto; "" → подавить; значение → маппинг - _cbt_map = {'auto': 'Auto', 'checkbox': 'CheckBox', 'switcher': 'Switcher', 'tumbler': 'Tumbler'} - if 'checkBoxType' in el: - if el.get('checkBoxType'): - lines.append(f'{inner}{_cbt_map.get(str(el["checkBoxType"]).lower(), el["checkBoxType"])}') - else: - lines.append(f'{inner}Auto') - - emit_title_location(lines, el, inner, 'Right') - - emit_layout(lines, el, inner) - - if el.get('warningOnEdit') is not None: - emit_mltext(lines, inner, 'WarningOnEdit', el['warningOnEdit']) - - # Формат / формат редактирования (LocalStringType — строка или {ru,en}) - if el.get('format'): - emit_mltext(lines, inner, 'Format', el['format']) - if el.get('editFormat'): - emit_mltext(lines, inner, 'EditFormat', el['editFormat']) - - # Оформление (цвета/шрифты/граница) — перед компаньонами - emit_appearance(lines, el, inner, 'field') - - # Companions - emit_companion_panel(lines, 'ContextMenu', f'{name}\u041a\u043e\u043d\u0442\u0435\u043a\u0441\u0442\u043d\u043e\u0435\u041c\u0435\u043d\u044e', inner, el.get('contextMenu')) - emit_companion(lines, 'ExtendedTooltip', f'{name}\u0420\u0430\u0441\u0448\u0438\u0440\u0435\u043d\u043d\u0430\u044f\u041f\u043e\u0434\u0441\u043a\u0430\u0437\u043a\u0430', inner, el.get('extendedTooltip')) - - emit_events(lines, el, name, inner, 'check') - - lines.append(f'{indent}') - - -def emit_radio_button_field(lines, el, name, eid, indent): - lines.append(f'{indent}') - inner = f'{indent}\t' - - if el.get('path'): - lines.append(f'{inner}{el["path"]}') - - emit_title(lines, el, name, inner, auto=not el.get('path')) - emit_common_flags(lines, el, inner) - - if el.get('editMode'): - lines.append(f'{inner}{el["editMode"]}') - emit_title_location(lines, el, inner, 'None') - - rbt = normalize_radio_button_type(el.get('radioButtonType')) - lines.append(f'{inner}{rbt}') - - if el.get('columnsCount') is not None: - lines.append(f'{inner}{el["columnsCount"]}') - - emit_choice_list(lines, el, inner) - - emit_layout(lines, el, inner) - - if el.get('warningOnEdit') is not None: - emit_mltext(lines, inner, 'WarningOnEdit', el['warningOnEdit']) - - # Оформление (цвета/шрифты/граница) — перед компаньонами - emit_appearance(lines, el, inner, 'field') - - emit_companion_panel(lines, 'ContextMenu', f'{name}КонтекстноеМеню', inner, el.get('contextMenu')) - emit_companion(lines, 'ExtendedTooltip', f'{name}РасширеннаяПодсказка', inner, el.get('extendedTooltip')) - - emit_events(lines, el, name, inner, 'radio') - - lines.append(f'{indent}') - - -# Заголовок декорации (Label/Picture): formatted-aware через единую ML-text форму -# (reuse resolve_ml_formatted, как у extendedTooltip). Sibling-ключ formatted — back-compat override. -def emit_decoration_title(lines, el, name, indent, auto=False): - has_key = 'title' in el - title_val = el['title'] if has_key else (title_from_name(name) if (auto and name) else None) - if title_val: - text, fmt = resolve_ml_formatted(title_val) - if 'formatted' in el: - fmt = bool(el['formatted']) - lines.append(f'{indent}<Title formatted="{"true" if fmt else "false"}">') - emit_ml_items(lines, f'{indent}\t', text) - lines.append(f'{indent}') - if el.get('tooltip'): - emit_mltext(lines, indent, 'ToolTip', el['tooltip']) - if el.get('tooltipRepresentation'): - lines.append(f'{indent}{el["tooltipRepresentation"]}') - - -def emit_label(lines, el, name, eid, indent): - lines.append(f'{indent}') - inner = f'{indent}\t' - - # Порядок как у платформы: own-content (флаги/hyperlink/layout/оформление) ПЕРЕД Title - # (корпус layout-first 16970 vs 44 — заодно убирает шум атрибуции харнесса на многострочном Title). - emit_common_flags(lines, el, inner) - if el.get('hyperlink') is True: - lines.append(f'{inner}true') - emit_layout(lines, el, inner) - emit_appearance(lines, el, inner, 'decoration') - - emit_decoration_title(lines, el, name, inner, auto=True) - - # Companions - emit_companion_panel(lines, 'ContextMenu', f'{name}\u041a\u043e\u043d\u0442\u0435\u043a\u0441\u0442\u043d\u043e\u0435\u041c\u0435\u043d\u044e', inner, el.get('contextMenu')) - emit_companion(lines, 'ExtendedTooltip', f'{name}\u0420\u0430\u0441\u0448\u0438\u0440\u0435\u043d\u043d\u0430\u044f\u041f\u043e\u0434\u0441\u043a\u0430\u0437\u043a\u0430', inner, el.get('extendedTooltip')) - - emit_events(lines, el, name, inner, 'label') - - lines.append(f'{indent}') - - -def emit_label_field(lines, el, name, eid, indent): - lines.append(f'{indent}') - inner = f'{indent}\t' - - if el.get('path'): - lines.append(f'{inner}{el["path"]}') - - emit_title(lines, el, name, inner, auto=not el.get('path')) - emit_common_flags(lines, el, inner) - - if el.get('titleLocation'): - lines.append(f'{inner}{map_title_loc(el["titleLocation"])}') - if el.get('editMode'): - lines.append(f'{inner}{el["editMode"]}') - # FooterDataPath — путь данных подвала колонки (общий cell-prop, как у input); после EditMode - if el.get('footerDataPath'): - lines.append(f'{inner}{esc_xml(str(el["footerDataPath"]))}') - # PasswordMode на LabelField — платформа эмитит явный false (редко); факт. значение - if el.get('passwordMode') is not None: - lines.append(f'{inner}{"true" if el["passwordMode"] else "false"}') - emit_column_pics(lines, el, inner) - # ВНИМАНИЕ: у LabelField платформенный тег (опечатка 1С), не . - if el.get('hyperlink') is True: - lines.append(f'{inner}true') - emit_layout(lines, el, inner) - - if el.get('warningOnEdit') is not None: - emit_mltext(lines, inner, 'WarningOnEdit', el['warningOnEdit']) - if el.get('footerText') is not None: - emit_mltext(lines, inner, 'FooterText', el['footerText']) - - # Формат / формат редактирования (LocalStringType — строка или {ru,en}) - if el.get('format'): - emit_mltext(lines, inner, 'Format', el['format']) - if el.get('editFormat'): - emit_mltext(lines, inner, 'EditFormat', el['editFormat']) - - # Оформление (цвета/шрифты/граница + header/footer) — перед компаньонами - emit_appearance(lines, el, inner, 'field') - - # Companions - emit_companion_panel(lines, 'ContextMenu', f'{name}\u041a\u043e\u043d\u0442\u0435\u043a\u0441\u0442\u043d\u043e\u0435\u041c\u0435\u043d\u044e', inner, el.get('contextMenu')) - emit_companion(lines, 'ExtendedTooltip', f'{name}\u0420\u0430\u0441\u0448\u0438\u0440\u0435\u043d\u043d\u0430\u044f\u041f\u043e\u0434\u0441\u043a\u0430\u0437\u043a\u0430', inner, el.get('extendedTooltip')) - - emit_events(lines, el, name, inner, 'labelField') - - lines.append(f'{indent}') - - -# Блок свойств таблицы, привязанной к динамическому списку (Group A defaults + B/C). -def emit_dynlist_table_block(lines, el, indent): - # (useAlternationRowColor — общее свойство таблицы, эмитится в emit_table) - # Group A (гарант. блок): дефолт + override - ar = 'true' if el.get('autoRefresh') is True else 'false' - lines.append(f'{indent}{ar}') - arp = el['autoRefreshPeriod'] if el.get('autoRefreshPeriod') is not None else 60 - lines.append(f'{indent}{arp}') - lines.append(f'{indent}') - lines.append(f'{indent}\tCustom') - lines.append(f'{indent}\t0001-01-01T00:00:00') - lines.append(f'{indent}\t0001-01-01T00:00:00') - lines.append(f'{indent}') - cfi = el.get('choiceFoldersAndItems') or 'Items' - lines.append(f'{indent}{cfi}') - rcr = 'true' if el.get('restoreCurrentRow') is True else 'false' - lines.append(f'{indent}{rcr}') - lines.append(f'{indent}') - sr = 'false' if el.get('showRoot') is False else 'true' - lines.append(f'{indent}{sr}') - arc = 'true' if el.get('allowRootChoice') is True else 'false' - lines.append(f'{indent}{arc}') - uodc = el.get('updateOnDataChange') or 'Auto' - lines.append(f'{indent}{uodc}') - if el.get('userSettingsGroup'): - lines.append(f'{indent}{el["userSettingsGroup"]}') - agcru = 'false' if el.get('allowGettingCurrentRowURL') is False else 'true' - lines.append(f'{indent}{agcru}') - - -def emit_table(lines, el, name, eid, indent): - _current_table_name['name'] = name # дефолт source для кастомных дополнений в commandBar - lines.append(f'{indent}') - inner = f'{indent}\t' - - if el.get('path'): - lines.append(f'{inner}{el["path"]}') - - emit_title(lines, el, name, inner, auto=not el.get('path')) - emit_common_flags(lines, el, inner) - - if el.get('representation'): - lines.append(f'{inner}{el["representation"]}') - if el.get('titleLocation'): - lines.append(f'{inner}{map_title_loc(el["titleLocation"])}') - # ChangeRowSet/Order — явное значение (в т.ч. false: платформа пишет его на ValueTable) - if 'changeRowSet' in el and el['changeRowSet'] is not None: - lines.append(f'{inner}{"true" if el["changeRowSet"] is True else "false"}') - if 'changeRowOrder' in el and el['changeRowOrder'] is not None: - lines.append(f'{inner}{"true" if el["changeRowOrder"] is True else "false"}') - if el.get('autoInsertNewRow') is True: - lines.append(f'{inner}true') - # RowFilter — nil-плейсхолдер (ключ присутствует → эмитим) - if 'rowFilter' in el: - lines.append(f'{inner}') - # Высота в строках () — отдельное свойство от (высота элемента, - # эмитится generic-ом emit_layout ниже). Таблица может нести оба (237 в корпусе). - if el.get('heightInTableRows'): - lines.append(f'{inner}{el["heightInTableRows"]}') - if el.get('header') is False: - lines.append(f'{inner}
false
') - if el.get('footer') is True: - lines.append(f'{inner}
true
') - - if el.get('commandBarLocation'): - lines.append(f'{inner}{el["commandBarLocation"]}') - if el.get('searchStringLocation'): - lines.append(f'{inner}{el["searchStringLocation"]}') - - if el.get('choiceMode') is True: - lines.append(f'{inner}true') - # Скаляры таблицы (захват «как есть»). Autofill — СВОЁ свойство таблицы (≠ AutoCommandBar autofill = tableAutofill). - if el.get('autofill') is not None: - lines.append(f'{inner}{"true" if el["autofill"] else "false"}') - if el.get('multipleChoice') is True: - lines.append(f'{inner}true') - if el.get('searchOnInput'): - lines.append(f'{inner}{el["searchOnInput"]}') - if el.get('markIncomplete') is not None: - lines.append(f'{inner}{"true" if el["markIncomplete"] else "false"}') - # Высота шапки/подвала в строках (pass-through; 1С толерантна к порядку детей Table) - if el.get('headerHeight') is not None: - lines.append(f'{inner}{el["headerHeight"]}') - if el.get('footerHeight') is not None: - lines.append(f'{inner}{el["footerHeight"]}') - if el.get('useAlternationRowColor') is True: - lines.append(f'{inner}true') - if el.get('selectionMode'): - lines.append(f'{inner}{el["selectionMode"]}') - if el.get('rowSelectionMode'): - lines.append(f'{inner}{el["rowSelectionMode"]}') - if el.get('verticalLines') is False: - lines.append(f'{inner}false') - if el.get('horizontalLines') is False: - lines.append(f'{inner}false') - if el.get('initialTreeView'): - lines.append(f'{inner}{el["initialTreeView"]}') - if el.get('enableDrag') is not None: - lines.append(f'{inner}{"true" if el["enableDrag"] else "false"}') - if el.get('rowPictureDataPath'): - lines.append(f'{inner}{el["rowPictureDataPath"]}') - # RowsPicture — та же конвенция, что ValuesPicture (дефолт LoadTransparent=false; abs/TransparentPixel) - emit_picture_ref(lines, el.get('rowsPicture'), 'RowsPicture', inner) - # Использование текущей строки таблицы (pass-through; в корпусе соседствует с блоком дин-списка) - if el.get('currentRowUse'): - lines.append(f'{inner}{el["currentRowUse"]}') - # Запрос обновления дин-списка (pass-through; в корпусе всегда PullFromTop) - if el.get('refreshRequest'): - lines.append(f'{inner}{el["refreshRequest"]}') - # Блок свойств дин-список-таблицы (помечена эвристикой) - if el.get('_dynList'): - emit_dynlist_table_block(lines, el, inner) - if el.get('viewStatusLocation'): - lines.append(f'{inner}{el["viewStatusLocation"]}') - if el.get('searchControlLocation'): - lines.append(f'{inner}{el["searchControlLocation"]}') - emit_layout(lines, el, inner) - - # CommandSet таблицы эмитится через emit_layout (общий механизм поля) - - # Оформление (цвета/граница таблицы) — перед компаньонами - emit_appearance(lines, el, inner, 'field') - - # Companions - emit_companion_panel(lines, 'ContextMenu', f'{name}\u041a\u043e\u043d\u0442\u0435\u043a\u0441\u0442\u043d\u043e\u0435\u041c\u0435\u043d\u044e', inner, el.get('contextMenu')) - # AutoCommandBar — with optional Autofill control - if el.get('commandBar') is not None: - emit_companion_panel(lines, 'AutoCommandBar', f'{name}\u041a\u043e\u043c\u0430\u043d\u0434\u043d\u0430\u044f\u041f\u0430\u043d\u0435\u043b\u044c', inner, el.get('commandBar')) - elif el.get('tableAutofill') is not None: - acb_id = new_id() - acb_name = f'{name}\u041a\u043e\u043c\u0430\u043d\u0434\u043d\u0430\u044f\u041f\u0430\u043d\u0435\u043b\u044c' - af_val = 'true' if el['tableAutofill'] else 'false' - lines.append(f'{inner}') - lines.append(f'{inner}\t{af_val}') - lines.append(f'{inner}') - else: - emit_companion(lines, 'AutoCommandBar', f'{name}\u041a\u043e\u043c\u0430\u043d\u0434\u043d\u0430\u044f\u041f\u0430\u043d\u0435\u043b\u044c', inner) - emit_companion(lines, 'ExtendedTooltip', f'{name}\u0420\u0430\u0441\u0448\u0438\u0440\u0435\u043d\u043d\u0430\u044f\u041f\u043e\u0434\u0441\u043a\u0430\u0437\u043a\u0430', inner, el.get('extendedTooltip')) - adds = el.get('additions') - emit_table_addition(lines, 'searchString', name, inner, get_addition_override(adds, 'searchString')) - emit_table_addition(lines, 'viewStatus', name, inner, get_addition_override(adds, 'viewStatus')) - emit_table_addition(lines, 'searchControl', name, inner, get_addition_override(adds, 'searchControl')) - - # Columns - if el.get('columns') and len(el['columns']) > 0: - lines.append(f'{inner}') - for col in el['columns']: - emit_element(lines, col, f'{inner}\t') - lines.append(f'{inner}') - - emit_events(lines, el, name, inner, 'table') - - lines.append(f'{indent}
') - - -def emit_pages(lines, el, name, eid, indent): - lines.append(f'{indent}') - inner = f'{indent}\t' - - emit_title(lines, el, name, inner) - - if el.get('pagesRepresentation'): - lines.append(f'{inner}{el["pagesRepresentation"]}') - - emit_common_flags(lines, el, inner) - emit_layout(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, el.get('extendedTooltip')) - - emit_events(lines, el, name, inner, 'pages') - - # Children (pages) - if el.get('children') and len(el['children']) > 0: - lines.append(f'{inner}') - for child in el['children']: - emit_element(lines, child, f'{inner}\t') - lines.append(f'{inner}') - - lines.append(f'{indent}') - - -def emit_page(lines, el, name, eid, indent): - lines.append(f'{indent}') - inner = f'{indent}\t' - - emit_title(lines, el, name, inner, auto=True) - emit_common_flags(lines, el, inner) - - # Картинка страницы (иконка вкладки): после Title/флагов, перед Group (порядок XSD). - # Конвенция как у ValuesPicture (дефолт LoadTransparent=false): скаляр-Ref/'abs:X' или объект. - emit_picture_ref(lines, el.get('picture'), 'Picture', inner) - - if el.get('group'): - orientation_map = { - 'horizontal': 'Horizontal', - 'vertical': 'Vertical', - 'alwaysHorizontal': 'AlwaysHorizontal', - 'alwaysVertical': 'AlwaysVertical', - 'horizontalIfPossible': 'HorizontalIfPossible', - } - orientation = orientation_map.get(str(el['group'])) - if orientation: - lines.append(f'{inner}{orientation}') - if el.get('showTitle') is not None: - lines.append(f'{inner}{"true" if el["showTitle"] else "false"}') - # Формат значения пути к данным заголовка (; парный к titleDataPath страницы) - if el.get('format'): - emit_mltext(lines, inner, 'Format', el['format']) - if el.get('editFormat'): - emit_mltext(lines, inner, 'EditFormat', el['editFormat']) - emit_layout(lines, el, inner) - - # \u041e\u0444\u043e\u0440\u043c\u043b\u0435\u043d\u0438\u0435 \u0441\u0442\u0440\u0430\u043d\u0438\u0446\u044b (BackColor / TitleTextColor / TitleFont) \u2014 \u043f\u043e\u0441\u043b\u0435 ShowTitle, \u043f\u0435\u0440\u0435\u0434 \u043a\u043e\u043c\u043f\u0430\u043d\u044c\u043e\u043d\u043e\u043c - emit_appearance(lines, el, inner, 'field') - - # Companion - emit_companion(lines, 'ExtendedTooltip', f'{name}\u0420\u0430\u0441\u0448\u0438\u0440\u0435\u043d\u043d\u0430\u044f\u041f\u043e\u0434\u0441\u043a\u0430\u0437\u043a\u0430', inner, el.get('extendedTooltip')) - - # Children - if el.get('children') and len(el['children']) > 0: - lines.append(f'{inner}') - for child in el['children']: - emit_element(lines, child, f'{inner}\t') - lines.append(f'{inner}') - - lines.append(f'{indent}') - - -def emit_button(lines, el, name, eid, indent, in_cmd_bar=False): - lines.append(f'{indent}') - - -def emit_picture_decoration(lines, el, name, eid, indent): - lines.append(f'{indent}') - inner = f'{indent}\t' - - emit_decoration_title(lines, el, name, inner) - # Текст при невыбранной картинке (NonselectedPictureText) — после Title (порядок корпуса) - if el.get('nonselectedPictureText') is not None: - emit_mltext(lines, inner, 'NonselectedPictureText', el['nonselectedPictureText']) - emit_common_flags(lines, el, inner) - - # Источник картинки — ТОЛЬКО src (ключ 'picture' = тип/имя элемента, не источник). - # Префикс "abs:" → встроенная картинка ; иначе именованная/стилевая . - if el.get('src'): - src_str = str(el['src']) - lt = 'true' if el.get('loadTransparent') is True else 'false' - lines.append(f'{inner}') - if src_str.startswith('abs:'): - lines.append(f'{inner}\t{esc_xml(src_str[4:])}') - else: - lines.append(f'{inner}\t{esc_xml(src_str)}') - lines.append(f'{inner}\t{lt}') - tpx = el.get('transparentPixel') - if tpx: - lines.append(f'{inner}\t') - lines.append(f'{inner}') - - if el.get('hyperlink') is True: - lines.append(f'{inner}true') - emit_layout(lines, el, inner) - - # Оформление (цвета/шрифт/граница) — профиль декорации (1С толерантна к порядку appearance) - emit_appearance(lines, el, inner, 'decoration') - - # Companions - emit_companion_panel(lines, 'ContextMenu', f'{name}\u041a\u043e\u043d\u0442\u0435\u043a\u0441\u0442\u043d\u043e\u0435\u041c\u0435\u043d\u044e', inner, el.get('contextMenu')) - emit_companion(lines, 'ExtendedTooltip', f'{name}\u0420\u0430\u0441\u0448\u0438\u0440\u0435\u043d\u043d\u0430\u044f\u041f\u043e\u0434\u0441\u043a\u0430\u0437\u043a\u0430', inner, el.get('extendedTooltip')) - - emit_events(lines, el, name, inner, 'picture') - - lines.append(f'{indent}') - - -def emit_picture_field(lines, el, name, eid, indent): - lines.append(f'{indent}') - inner = f'{indent}\t' - - if el.get('path'): - lines.append(f'{inner}{el["path"]}') - - emit_title(lines, el, name, inner) - emit_common_flags(lines, el, inner) - - if el.get('editMode'): - lines.append(f'{inner}{el["editMode"]}') - emit_column_pics(lines, el, inner) - if el.get('titleLocation'): - lines.append(f'{inner}{map_title_loc(el["titleLocation"])}') - if el.get('hyperlink') is True: - lines.append(f'{inner}true') - - emit_layout(lines, el, inner) - - # ValuesPicture — picture (collection) used to render the field's value. - # Required for a Boolean-bound PictureField to actually show an icon. - # Скаляр (Ref) или объект {src, loadTransparent}; LoadTransparent эмитится всегда. - emit_picture_ref(lines, el.get('valuesPicture'), 'ValuesPicture', inner) - if el.get('nonselectedPictureText') is not None: - emit_mltext(lines, inner, 'NonselectedPictureText', el['nonselectedPictureText']) - - # Оформление (цвета/шрифты/граница) — перед компаньонами - emit_appearance(lines, el, inner, 'field') - - # Companions - emit_companion_panel(lines, 'ContextMenu', f'{name}\u041a\u043e\u043d\u0442\u0435\u043a\u0441\u0442\u043d\u043e\u0435\u041c\u0435\u043d\u044e', inner, el.get('contextMenu')) - emit_companion(lines, 'ExtendedTooltip', f'{name}\u0420\u0430\u0441\u0448\u0438\u0440\u0435\u043d\u043d\u0430\u044f\u041f\u043e\u0434\u0441\u043a\u0430\u0437\u043a\u0430', inner, el.get('extendedTooltip')) - - emit_events(lines, el, name, inner, 'picField') - - lines.append(f'{indent}') - - -def emit_simple_field(lines, el, name, eid, indent, xml_tag, type_key): - # Спец-поля "документ/датчик" (SpreadSheet/HTML/Text/Formatted/ProgressBar/TrackBar): - # единый скелет поля. Типоспец. enum/bool скаляры — через generic (emit_layout); - # числовые скаляры датчиков (min/max/шаги) — без xsi:type; enableDrag — фактическое значение. - lines.append(f'{indent}<{xml_tag} name="{name}" id="{eid}"{di_attr(el)}>') - inner = f'{indent}\t' - - if el.get('path'): - lines.append(f'{inner}{el["path"]}') - emit_title(lines, el, name, inner, auto=not el.get('path')) - emit_common_flags(lines, el, inner) - if el.get('titleLocation'): - lines.append(f'{inner}{map_title_loc(el["titleLocation"])}') - if el.get('editMode'): - lines.append(f'{inner}{el["editMode"]}') - - emit_layout(lines, el, inner) - - # EnableDrag — фактическое значение (SpreadSheet; платформа эмитит явный false). enableStartDrag — через emit_layout. - if el.get('enableDrag') is not None: - lines.append(f'{inner}{"true" if el["enableDrag"] else "false"}') - - # Датчики (ProgressBar/TrackBar) — числовые скаляры (без xsi:type) - for key, tag in (('minValue', 'MinValue'), ('maxValue', 'MaxValue'), ('largeStep', 'LargeStep'), ('markingStep', 'MarkingStep'), ('step', 'Step')): - if el.get(key) is not None: - lines.append(f'{inner}<{tag}>{el[key]}') - - # Оформление (цвета/шрифты/граница) — перед компаньонами - emit_appearance(lines, el, inner, 'field') - - # Companions - emit_companion_panel(lines, 'ContextMenu', f'{name}КонтекстноеМеню', inner, el.get('contextMenu')) - emit_companion(lines, 'ExtendedTooltip', f'{name}РасширеннаяПодсказка', inner, el.get('extendedTooltip')) - - emit_events(lines, el, name, inner, type_key) - - lines.append(f'{indent}') - - -def emit_gantt_chart(lines, el, name, eid, indent): - # GanttChartField — скелет поля + вложенная (полноценная таблица, через emit_element). - lines.append(f'{indent}') - inner = f'{indent}\t' - if el.get('path'): - lines.append(f'{inner}{el["path"]}') - emit_title(lines, el, name, inner, auto=not el.get('path')) - emit_common_flags(lines, el, inner) - if el.get('titleLocation'): - lines.append(f'{inner}{map_title_loc(el["titleLocation"])}') - emit_layout(lines, el, inner) - emit_appearance(lines, el, inner, 'field') - emit_companion_panel(lines, 'ContextMenu', f'{name}КонтекстноеМеню', inner, el.get('contextMenu')) - emit_companion(lines, 'ExtendedTooltip', f'{name}РасширеннаяПодсказка', inner, el.get('extendedTooltip')) - # Вложенная таблица диаграммы Ганта (стандартный Table — переиспользуем emit_element) - if el.get('ganttTable'): - emit_element(lines, el['ganttTable'], inner) - emit_events(lines, el, name, inner, 'ganttChart') - lines.append(f'{indent}') - - -def emit_calendar(lines, el, name, eid, indent): - lines.append(f'{indent}') - inner = f'{indent}\t' - - if el.get('path'): - lines.append(f'{inner}{el["path"]}') - - emit_title(lines, el, name, inner, auto=not el.get('path')) - emit_common_flags(lines, el, inner) - - if el.get('titleLocation'): - loc_map = {'none': 'None', 'left': 'Left', 'right': 'Right', 'top': 'Top', 'bottom': 'Bottom', 'auto': 'Auto'} - loc = loc_map.get(str(el['titleLocation']), str(el['titleLocation'])) - lines.append(f'{inner}{loc}') - - emit_layout(lines, el, inner) - - # Календарно-специфичные свойства (порядок схемы: после layout, до companions) - if el.get('selectionMode'): - lines.append(f'{inner}{el["selectionMode"]}') - if el.get('showCurrentDate') is not None: - lines.append(f'{inner}{"true" if el["showCurrentDate"] else "false"}') - if el.get('widthInMonths') is not None: - lines.append(f'{inner}{el["widthInMonths"]}') - if el.get('heightInMonths') is not None: - lines.append(f'{inner}{el["heightInMonths"]}') - if el.get('showMonthsPanel') is not None: - lines.append(f'{inner}{"true" if el["showMonthsPanel"] else "false"}') - - # Оформление (цвета/шрифты/граница) — перед компаньонами - emit_appearance(lines, el, inner, 'field') - - # Companions - emit_companion_panel(lines, 'ContextMenu', f'{name}\u041a\u043e\u043d\u0442\u0435\u043a\u0441\u0442\u043d\u043e\u0435\u041c\u0435\u043d\u044e', inner, el.get('contextMenu')) - emit_companion(lines, 'ExtendedTooltip', f'{name}\u0420\u0430\u0441\u0448\u0438\u0440\u0435\u043d\u043d\u0430\u044f\u041f\u043e\u0434\u0441\u043a\u0430\u0437\u043a\u0430', inner, el.get('extendedTooltip')) - - emit_events(lines, el, name, inner, 'calendar') - - lines.append(f'{indent}') - - -def emit_command_bar(lines, el, name, eid, indent): - lines.append(f'{indent}') - inner = f'{indent}\t' - - emit_title(lines, el, name, inner) - - if el.get('commandSource'): - lines.append(f'{inner}{el["commandSource"]}') - - if el.get('autofill') is True: - lines.append(f'{inner}true') - - _hl = get_hlocation(el) - if _hl: - lines.append(f'{inner}{_hl}') - - emit_common_flags(lines, el, inner) - emit_layout(lines, el, inner) - emit_companion(lines, 'ExtendedTooltip', f'{name}РасширеннаяПодсказка', inner, el.get('extendedTooltip')) - - # Children - if el.get('children') and len(el['children']) > 0: - lines.append(f'{inner}') - for child in el['children']: - emit_element(lines, child, f'{inner}\t', in_cmd_bar=True) - lines.append(f'{inner}') - - lines.append(f'{indent}') - - -def emit_popup(lines, el, name, eid, indent): - lines.append(f'{indent}') - inner = f'{indent}\t' - - emit_title(lines, el, name, inner, auto=True) - emit_common_flags(lines, el, inner) - - emit_command_picture(lines, el.get('picture'), el.get('loadTransparent'), inner) - - if el.get('representation'): - lines.append(f'{inner}{el["representation"]}') - emit_layout(lines, el, inner) - - # Оформление попапа (TitleTextColor / TitleFont) — перед компаньоном - emit_appearance(lines, el, inner, 'field') - - emit_companion(lines, 'ExtendedTooltip', f'{name}РасширеннаяПодсказка', inner, el.get('extendedTooltip')) - - # Children - if el.get('children') and len(el['children']) > 0: - lines.append(f'{inner}') - for child in el['children']: - emit_element(lines, child, f'{inner}\t', in_cmd_bar=True) - lines.append(f'{inner}') - - lines.append(f'{indent}') - - -def emit_button_group(lines, el, name, eid, indent): - lines.append(f'{indent}') - inner = f'{indent}\t' - - emit_title(lines, el, name, inner) - - if el.get('commandSource'): - lines.append(f'{inner}{el["commandSource"]}') - - if el.get('representation'): - lines.append(f'{inner}{el["representation"]}') - - emit_common_flags(lines, el, inner) - emit_layout(lines, el, inner) - - # Companion: ExtendedTooltip - emit_companion(lines, 'ExtendedTooltip', f'{name}РасширеннаяПодсказка', inner, el.get('extendedTooltip')) - - # Children (кнопки в контексте командной панели) - if el.get('children') and len(el['children']) > 0: - lines.append(f'{inner}') - for child in el['children']: - emit_element(lines, child, f'{inner}\t', in_cmd_bar=True) - lines.append(f'{inner}') - - lines.append(f'{indent}') - - -# --- Attribute emitter --- - -def emit_functional_options(lines, fo, indent): - # FunctionalOption.X…> — у Attribute/Command/Column. - # Forgiving: "X"/"FunctionalOption.X" → FunctionalOption.X; GUID (расширение) — как есть. - if not fo: - return - lines.append(f'{indent}') - for opt in fo: - v = str(opt) - if re.match(r'^[0-9a-fA-F]{8}-[0-9a-fA-F-]{27,}$', v): - pass - elif v.startswith('FunctionalOption.'): - pass - else: - v = f'FunctionalOption.{v}' - lines.append(f'{indent}\t{v}') - lines.append(f'{indent}') - - -def emit_attr_column(lines, col, indent): - # Колонка реквизита (ValueTable/Tree или AdditionalColumns): name/Title/Type/FunctionalOptions. - col_id = new_id() - lines.append(f'{indent}') - if col.get('title'): - emit_mltext(lines, f'{indent}\t', 'Title', col['title']) - emit_type(lines, str(col.get('type', '')), f'{indent}\t') - emit_functional_options(lines, col.get('functionalOptions'), f'{indent}\t') - # Ролевой доступ колонки (View/Edit) — xr-флаг, как у самого реквизита - if col.get('view') is not None: - emit_xr_flag(lines, 'View', col['view'], f'{indent}\t') - if col.get('edit') is not None: - emit_xr_flag(lines, 'Edit', col['edit'], f'{indent}\t') - lines.append(f'{indent}') - - -# --- Schema-параметры динамического списка (DataCompositionSchemaParameter) --- -# Зеркало form-compile.ps1 (Emit-DLParameters). Та же сущность, что параметры СКД, но в -# форме: обёртка + дети dcssch:. DSL переиспользует грамматику параметров СКД. -# Контекстные дефолты: useRestriction эмитим ВСЕГДА, дефолт true (в СКД false); title — авто -# из имени; пустое value — всегда xsi:nil (даже при известном типе). Канон. порядок детей -# (по корпусу): name, title, valueType, value, useRestriction, expression, availableValue*, -# valueListAllowed, availableAsField, inputParameters, denyIncompleteValues, use. - -def emit_dl_mltext(lines, indent, tag, text): - # ML-текст с xsi:type="v8:LocalStringType" (в dcssch:* обязателен; emit_mltext его не ставит). - lines.append(f'{indent}<{tag} xsi:type="v8:LocalStringType">') - emit_ml_items(lines, f'{indent}\t', text) - lines.append(f'{indent}') - - -def split_dl_valuelist_csv(s): - result = [] - if s is None: - return result - items = [] - buf = [] - in_quote = None - for ch in s: - if in_quote: - buf.append(ch) - if ch == in_quote: - in_quote = None - elif ch in ("'", '"'): - in_quote = ch - buf.append(ch) - elif ch == ',': - items.append(''.join(buf)); buf = [] - else: - buf.append(ch) - if buf: - items.append(''.join(buf)) - for raw in items: - t = raw.strip() - if len(t) >= 2 and ((t[0] == "'" and t[-1] == "'") or (t[0] == '"' and t[-1] == '"')): - t = t[1:-1] - if t != '': - result.append(t) - return result - - -def parse_dl_param_shorthand(s): - result = {'name': '', 'type': '', 'value': None, 'title': None} - if '@valueList' in s: - result['valueListAllowed'] = True - s = re.sub(r'\s*@valueList', '', s) - if '@hidden' in s: - result['hidden'] = True - s = re.sub(r'\s*@hidden', '', s) - m = re.search(r'\[([^\]]*)\]', s) - if m: - result['title'] = m.group(1).strip() - s = re.sub(r'\s*\[[^\]]*\]\s*', ' ', s).strip() - # Тип может быть СОСТАВНЫМ (A | B | C — с пробелами); значение — после '=' (тип '=' не содержит). - m = re.match(r'^([^:]+):\s*([^=]+?)(\s*=\s*(.*))?$', s) - if m: - result['name'] = m.group(1).strip() - type_raw = m.group(2).strip() - if re.search(r'[|+]', type_raw): - result['type'] = ' | '.join(resolve_type_str(p.strip()) for p in re.split(r'\s*[|+]\s*', type_raw)) - else: - result['type'] = resolve_type_str(type_raw) - if m.group(4): - rhs = m.group(4).strip() - items = split_dl_valuelist_csv(rhs) - if len(items) >= 2: - result['value'] = items - result['valueListAllowed'] = True - elif len(items) == 1: - result['value'] = items[0] - else: - result['value'] = rhs - else: - result['name'] = s.strip() - return result - - -def is_dl_empty_value(v): - if v is None: - return True - sv = str(v).strip() - return sv == '' or sv == '_' or sv.lower() == 'null' - - -def emit_dl_value(lines, type_str, val, indent, value_list_allowed=False): - if is_dl_empty_value(val): - # Дин-список: пустое значение платформа ВСЕГДА пишет как xsi:nil (даже при известном типе). - if value_list_allowed: - return - lines.append(f'{indent}') - return - if isinstance(val, bool): - val_str = 'true' if val else 'false' - else: - val_str = str(val) - t = type_str or '' - if re.match(r'^(date|dateTime|time)', t): - lines.append(f'{indent}{esc_xml(val_str)}') - elif t == 'boolean': - lines.append(f'{indent}{esc_xml(val_str)}') - elif t == 'v8:Type': - ns_attr = _value_type_ns_attr('v8:Type', val_str) - lines.append(f'{indent}{esc_xml(val_str)}') - elif re.match(r'^decimal', t): - lines.append(f'{indent}{esc_xml(val_str)}') - elif re.match(r'^string', t): - lines.append(f'{indent}{esc_xml(val_str)}') - elif re.match(r'^(CatalogRef|DocumentRef|EnumRef|ChartOfAccountsRef|ChartOfCharacteristicTypesRef|ChartOfCalculationTypesRef|BusinessProcessRef|TaskRef|ExchangePlanRef)\.', t): - lines.append(f'{indent}{esc_xml(val_str)}') - else: - if re.match(r'^\d{4}-\d{2}-\d{2}T', val_str): - lines.append(f'{indent}{esc_xml(val_str)}') - elif val_str in ('true', 'false'): - lines.append(f'{indent}{esc_xml(val_str)}') - elif re.match(r'^(ПланСчетов|Справочник|Перечисление|Документ|ПланВидовХарактеристик|ПланВидовРасчета|БизнесПроцесс|Задача|РегистрСведений|ПланОбмена)\.', val_str) or re.match(r'^(ChartOfAccounts|Catalog|Enum|Document|ChartOfCharacteristicTypes|ChartOfCalculationTypes|BusinessProcess|Task|InformationRegister|ExchangePlan)\.', val_str): - lines.append(f'{indent}{esc_xml(val_str)}') - else: - lines.append(f'{indent}{esc_xml(val_str)}') - - -def emit_dl_value_type(lines, type_str, indent): - if not type_str: - return - lines.append(f'{indent}') - for part in re.split(r'\s*[|+]\s*', str(type_str)): - emit_single_type(lines, part.strip(), f'{indent}\t') - lines.append(f'{indent}') - - -def emit_dl_available_value(lines, av, type_str, indent): - lines.append(f'{indent}') - av_val = av.get('value') if isinstance(av, dict) else None - emit_dl_value(lines, type_str, av_val, f'{indent}\t', False) - pres = (av.get('presentation') or av.get('title')) if isinstance(av, dict) else None - if pres: - emit_dl_mltext(lines, f'{indent}\t', 'dcssch:presentation', pres) - lines.append(f'{indent}') - - -def emit_dl_input_parameters(lines, ip, indent): - if ip is None: - return - items = ip if isinstance(ip, list) else [ip] - if len(items) == 0: - return - lines.append(f'{indent}') - for item in items: - lines.append(f'{indent}\t') - if 'use' in item and item.get('use') is not None and not item.get('use'): - lines.append(f'{indent}\t\tfalse') - lines.append(f'{indent}\t\t{esc_xml(str(item.get("parameter", "")))}') - if 'choiceParameters' in item: - cp_items = item.get('choiceParameters') or [] - if len(cp_items) == 0: - lines.append(f'{indent}\t\t') - else: - lines.append(f'{indent}\t\t') - for cp in cp_items: - lines.append(f'{indent}\t\t\t') - lines.append(f'{indent}\t\t\t\t{esc_xml(str(cp.get("name", "")))}') - for v in (cp.get('values') or []): - if isinstance(v, bool): - lines.append(f'{indent}\t\t\t\t{"true" if v else "false"}') - elif isinstance(v, (int, float)): - lines.append(f'{indent}\t\t\t\t{v}') - else: - lines.append(f'{indent}\t\t\t\t{esc_xml(str(v))}') - lines.append(f'{indent}\t\t\t') - lines.append(f'{indent}\t\t') - elif 'choiceParameterLinks' in item: - cpl_items = item.get('choiceParameterLinks') or [] - if len(cpl_items) == 0: - lines.append(f'{indent}\t\t') - else: - lines.append(f'{indent}\t\t') - for cpl in cpl_items: - lines.append(f'{indent}\t\t\t') - lines.append(f'{indent}\t\t\t\t{esc_xml(str(cpl.get("name", "")))}') - lines.append(f'{indent}\t\t\t\t{esc_xml(str(cpl.get("value", "")))}') - mode = str(cpl.get('mode') or 'Auto') - lines.append(f'{indent}\t\t\t\t{mode}') - lines.append(f'{indent}\t\t\t') - lines.append(f'{indent}\t\t') - elif 'value' in item: - val = item.get('value') - if isinstance(val, bool): - lines.append(f'{indent}\t\t{"true" if val else "false"}') - elif isinstance(val, (int, float)): - lines.append(f'{indent}\t\t{val}') - elif isinstance(val, dict): - emit_dl_mltext(lines, f'{indent}\t\t', 'dcscor:value', val) - else: - lines.append(f'{indent}\t\t{esc_xml(str(val))}') - lines.append(f'{indent}\t') - lines.append(f'{indent}') - - -# ── dataParameters (значения параметров запроса в настройках компоновки) — порт из skd ── -def _test_empty_value(v): - if v is None: - return True - s = str(v).strip() - return s == '' or s == '_' or s.lower() == 'null' - - -def emit_empty_value(lines, type_str, indent, tag_prefix='', value_list_allowed=False): - if value_list_allowed: - return - t = type_str or '' - t_bare = t[3:] if t.startswith('xs:') else t - pf = tag_prefix - if t == '': - lines.append(f'{indent}<{pf}value xsi:nil="true"/>') - elif t == 'StandardPeriod': - lines.append(f'{indent}<{pf}value xsi:type="v8:StandardPeriod">') - lines.append(f'{indent}\tCustom') - lines.append(f'{indent}\t0001-01-01T00:00:00') - lines.append(f'{indent}\t0001-01-01T00:00:00') - lines.append(f'{indent}') - elif re.match(r'^string', t_bare): - lines.append(f'{indent}<{pf}value xsi:type="xs:string"/>') - elif re.match(r'^(date|time)', t_bare): - lines.append(f'{indent}<{pf}value xsi:type="xs:dateTime">0001-01-01T00:00:00') - elif re.match(r'^decimal', t_bare): - lines.append(f'{indent}<{pf}value xsi:type="xs:decimal">0') - elif t_bare == 'boolean': - lines.append(f'{indent}<{pf}value xsi:type="xs:boolean">false') - else: - lines.append(f'{indent}<{pf}value xsi:nil="true"/>') - - -_DP_PERIOD_VARIANTS = {"Custom","Today","ThisWeek","ThisTenDays","ThisMonth","ThisQuarter","ThisHalfYear","ThisYear","FromBeginningOfThisWeek","FromBeginningOfThisTenDays","FromBeginningOfThisMonth","FromBeginningOfThisQuarter","FromBeginningOfThisHalfYear","FromBeginningOfThisYear","LastWeek","LastTenDays","LastMonth","LastQuarter","LastHalfYear","LastYear","NextDay","NextWeek","NextTenDays","NextMonth","NextQuarter","NextHalfYear","NextYear","TillEndOfThisWeek","TillEndOfThisTenDays","TillEndOfThisMonth","TillEndOfThisQuarter","TillEndOfThisHalfYear","TillEndOfThisYear"} - - -def parse_data_param_shorthand(s): - result = {'parameter': '', 'value': None, 'use': True, 'userSettingID': None, 'viewMode': None} - if '@user' in s: - result['userSettingID'] = 'auto'; s = re.sub(r'\s*@user', '', s) - if '@off' in s: - result['use'] = False; s = re.sub(r'\s*@off', '', s) - if '@quickAccess' in s: - result['viewMode'] = 'QuickAccess'; s = re.sub(r'\s*@quickAccess', '', s) - if '@normal' in s: - result['viewMode'] = 'Normal'; s = re.sub(r'\s*@normal', '', s) - s = s.strip() - m = re.match(r'^([^=]+)=\s*(.+)$', s) - if m: - result['parameter'] = m.group(1).strip() - val_str = m.group(2).strip() - if val_str in _DP_PERIOD_VARIANTS: - result['value'] = {'variant': val_str} - elif re.match(r'^\d{4}-\d{2}-\d{2}T', val_str): - result['value'] = val_str - elif val_str in ('true', 'false'): - result['value'] = (val_str == 'true') - else: - result['value'] = val_str - else: - result['parameter'] = s - return result - - -def emit_data_parameters(lines, items, indent, block_view_mode=None): - if not items or len(items) == 0: - return - lines.append(f'{indent}') - for dp in items: - if isinstance(dp, str): - parsed = parse_data_param_shorthand(dp) - dp = {'parameter': parsed['parameter']} - if parsed['value'] is not None: - dp['value'] = parsed['value'] - if parsed['use'] is False: - dp['use'] = False - if parsed['userSettingID']: - dp['userSettingID'] = parsed['userSettingID'] - if parsed['viewMode']: - dp['viewMode'] = parsed['viewMode'] - lines.append(f'{indent}\t') - if dp.get('use') is False: - lines.append(f'{indent}\t\tfalse') - lines.append(f'{indent}\t\t{esc_xml(str(dp.get("parameter", "")))}') - vtype = str(dp.get('valueType') or '') - val = dp.get('value') - if dp.get('nilValue') is True: - lines.append(f'{indent}\t\t') - elif _test_empty_value(val) and vtype: - emit_empty_value(lines, vtype, f'{indent}\t\t', tag_prefix='dcscor:', value_list_allowed=False) - elif _test_empty_value(val): - pass # нет значения → не эмитим value-узел (form дин-список: use=false плейсхолдер) - elif val is not None: - if isinstance(val, dict) and val.get('variant'): - variant = str(val.get('variant')) - has_date = 'date' in val - has_sd = 'startDate' in val - is_sbd = has_date or (not has_sd and variant.startswith('BeginningOf')) - if is_sbd: - lines.append(f'{indent}\t\t') - lines.append(f'{indent}\t\t\t{esc_xml(variant)}') - if variant == 'Custom': - d = str(val.get('date') or '0001-01-01T00:00:00') - lines.append(f'{indent}\t\t\t{esc_xml(d)}') - lines.append(f'{indent}\t\t') - else: - lines.append(f'{indent}\t\t') - lines.append(f'{indent}\t\t\t{esc_xml(variant)}') - if variant == 'Custom': - sd = str(val.get('startDate') or '0001-01-01T00:00:00') - ed = str(val.get('endDate') or '0001-01-01T00:00:00') - lines.append(f'{indent}\t\t\t{esc_xml(sd)}') - lines.append(f'{indent}\t\t\t{esc_xml(ed)}') - lines.append(f'{indent}\t\t') - elif re.match(r'^[a-zA-Z]+:', vtype): - v_str = str(val).lower() if isinstance(val, bool) else str(val) - lines.append(f'{indent}\t\t{esc_xml(v_str)}') - elif vtype == 'boolean' or isinstance(val, bool): - lines.append(f'{indent}\t\t{esc_xml(str(val).lower())}') - elif re.match(r'^date', vtype) or re.match(r'^\d{4}-\d{2}-\d{2}T', str(val)): - lines.append(f'{indent}\t\t{esc_xml(str(val))}') - elif re.match(r'^decimal', vtype): - lines.append(f'{indent}\t\t{esc_xml(str(val))}') - elif re.match(r'^string', vtype): - lines.append(f'{indent}\t\t{esc_xml(str(val))}') - elif re.match(r'^(ПланСчетов|Справочник|Перечисление|Документ|ПланВидовХарактеристик|ПланВидовРасчета|БизнесПроцесс|Задача|РегистрСведений|ПланОбмена)\.', str(val)) or re.match(r'^(ChartOfAccounts|Catalog|Enum|Document|ChartOfCharacteristicTypes|ChartOfCalculationTypes|BusinessProcess|Task|InformationRegister|ExchangePlan)\.', str(val)): - lines.append(f'{indent}\t\t{esc_xml(str(val))}') - else: - lines.append(f'{indent}\t\t{esc_xml(str(val))}') - if dp.get('viewMode'): - lines.append(f'{indent}\t\t{esc_xml(str(dp["viewMode"]))}') - if dp.get('userSettingID'): - uid = new_uuid() if str(dp['userSettingID']) == 'auto' else str(dp['userSettingID']) - lines.append(f'{indent}\t\t{esc_xml(uid)}') - if dp.get('userSettingPresentation'): - emit_us_presentation(lines, f'{indent}\t\t', 'dcsset:userSettingPresentation', dp['userSettingPresentation']) - lines.append(f'{indent}\t') - if block_view_mode is not None: - lines.append(f'{indent}\t{esc_xml(str(block_view_mode))}') - lines.append(f'{indent}') - - -def emit_dl_parameter(lines, p, parsed, indent): - is_obj = not isinstance(p, str) - lines.append(f'{indent}') - ci = f'{indent}\t' - lines.append(f'{ci}{esc_xml(parsed["name"])}') - # Title: явный override (shorthand [..] / объект title/presentation) или авто из имени. - title = None - if parsed.get('title'): - title = parsed['title'] - elif is_obj and p.get('title'): - title = p['title'] - elif is_obj and p.get('presentation'): - title = p['presentation'] - if title is None or (isinstance(title, str) and title == ''): - title = title_from_name(parsed['name']) - emit_dl_mltext(lines, ci, 'dcssch:title', title) - # valueType - if parsed.get('type'): - emit_dl_value_type(lines, parsed['type'], ci) - # value (дефолт nil; при valueListAllowed пустое — опускаем) - vla = bool(parsed.get('valueListAllowed')) - pv = parsed.get('value') - if isinstance(pv, list): - for v in pv: - emit_dl_value(lines, parsed.get('type', ''), v, ci, False) - elif vla and is_dl_empty_value(pv) and parsed.get('value_explicit'): - # valueListAllowed + явный пустой (value:null от декомпилятора) → платформа пишет nil - lines.append(f'{ci}') - else: - emit_dl_value(lines, parsed.get('type', ''), pv, ci, vla) - # useRestriction — ВСЕГДА; дефолт true; false только при явном useRestriction:false. - ur = True - if is_obj and 'useRestriction' in p: - ur = bool(p['useRestriction']) - lines.append(f'{ci}{"true" if ur else "false"}') - # expression - expr = str(p['expression']) if (is_obj and p.get('expression')) else None - if expr: - lines.append(f'{ci}{esc_xml(expr)}') - # availableValues - if is_obj and p.get('availableValues'): - for av in p['availableValues']: - emit_dl_available_value(lines, av, parsed.get('type', ''), ci) - # valueListAllowed - if vla: - lines.append(f'{ci}true') - # availableAsField=false (hidden или явный) - aaf = None - if parsed.get('hidden') is True: - aaf = False - if is_obj and 'availableAsField' in p: - aaf = bool(p['availableAsField']) - if aaf is False: - lines.append(f'{ci}false') - # inputParameters - if is_obj and p.get('inputParameters'): - emit_dl_input_parameters(lines, p['inputParameters'], ci) - # denyIncompleteValues - if is_obj and p.get('denyIncompleteValues') is True: - lines.append(f'{ci}true') - # use - if is_obj and p.get('use'): - lines.append(f'{ci}{esc_xml(str(p["use"]))}') - lines.append(f'{indent}') - - -def emit_dl_parameters(lines, params, indent): - if not params: - return - for p in params: - if isinstance(p, str): - parsed = parse_dl_param_shorthand(p) - else: - resolved_type = '' - if p.get('type'): - if isinstance(p['type'], list): - resolved_type = ' | '.join(resolve_type_str(str(x)) for x in p['type']) - else: - resolved_type = resolve_type_str(str(p['type'])) - elif p.get('valueType'): - resolved_type = resolve_type_str(str(p['valueType'])) - parsed = {'name': str(p.get('name', '')), 'type': resolved_type, - 'value': p.get('value') if 'value' in p else None, - 'value_explicit': ('value' in p), 'title': None} - if p.get('valueListAllowed') is True: - parsed['valueListAllowed'] = True - if p.get('hidden') is True: - parsed['hidden'] = True - emit_dl_parameter(lines, p, parsed, indent) - - -def emit_attributes(lines, attrs, indent, conditional_appearance=None): - has_ca = bool(conditional_appearance) and len(conditional_appearance) > 0 - # Платформа ВСЕГДА эмитит (100% корпуса; 162 формы — пустой ). - if (not attrs or len(attrs) == 0) and not has_ca: - lines.append(f'{indent}') - return - if not attrs or len(attrs) == 0: - # Нет реквизитов, но есть условное оформление (последний child ) - lines.append(f'{indent}') - emit_conditional_appearance(lines, conditional_appearance, f'{indent}\t', wrap_tag='ConditionalAppearance') - lines.append(f'{indent}') - return - - lines.append(f'{indent}') - seen_attrs = set() - for attr in attrs: - attr_id = new_id() - attr_name = str(attr['name']) - _ensure_unique(attr_name, seen_attrs, 'attribute') - - lines.append(f'{indent}\t') - inner = f'{indent}\t\t' - - # Title атрибута (зеркало emit_title): нет ключа → авто-вывод из имени (кроме main); - # title "" → подавить; непустой → эмитить как есть. - if 'title' in attr: - if attr.get('title'): - emit_mltext(lines, inner, 'Title', attr['title']) - elif attr.get('main') is not True: - emit_mltext(lines, inner, 'Title', title_from_name(attr_name)) - - # Type - if attr.get('type'): - emit_type(lines, str(attr['type']), inner) - else: - lines.append(f'{inner}') - # valueType: ОписаниеТипов значений ValueList → - # (та же грамматика типа, включая составной "A | B"). Forgiving-синонимы. - # Три состояния: нет ключа → нет Settings; "" → пустой ; тип → с типом. - vt_spec = None - has_vt = False - for k in ('valueType', 'typeDescription', 'описаниеТипов', 'типЗначений'): - if k in attr: - vt_spec = attr[k] - has_vt = True - break - if has_vt: - emit_type(lines, '' if vt_spec is None else str(vt_spec), inner, tag="Settings", tag_attrs=' xsi:type="v8:TypeDescription"') - # Planner design-time (встроенный конфиг планировщика). - if attr.get('planner') is not None: - emit_planner_settings(lines, attr['planner'], inner) - # Chart/GanttChart design-time (тип выводится из типа реквизита). - if attr.get('chart') is not None: - ctype = 'd4p1:GanttChart' if 'GanttChart' in str(attr.get('type', '')) else 'd4p1:Chart' - emit_chart_settings(lines, attr['chart'], inner, ctype) - - if attr.get('main') is True: - lines.append(f'{inner}true') - # Доступ по ролям: просмотр/редактирование (порядок схемы: View → Edit, после MainAttribute) - if attr.get('view') is not None: - emit_xr_flag(lines, 'View', attr.get('view'), inner) - if attr.get('edit') is not None: - emit_xr_flag(lines, 'Edit', attr.get('edit'), inner) - main_saved = False - if attr.get('main') is True and attr.get('type'): - t = str(attr['type']) - main_saved = bool(re.match(r'^(CatalogObject|DocumentObject|ChartOfAccountsObject|ChartOfCalculationTypesObject|ChartOfCharacteristicTypesObject|ExchangePlanObject|BusinessProcessObject|TaskObject)\.', t)) or ('RecordManager.' in t) - # Явный ключ savedData побеждает (в т.ч. False → суппресс авто-вывода main_saved); нет ключа → авто. - emit_saved = (attr['savedData'] is True) if 'savedData' in attr else main_saved - if emit_saved: - lines.append(f'{inner}true') - # Save: сохранение значения реквизита в пользовательских настройках. true → имя; - # строка/массив → под-поля с авто-префиксом "имя." (путь с точкой / UUID / =имя — как есть). - # Нет ключа или false → не эмитим. - if 'save' in attr and attr['save'] is not None: - save_fields = [] - sv = attr['save'] - if isinstance(sv, bool): - if sv: - save_fields.append(attr_name) - else: - for e in (sv if isinstance(sv, (list, tuple)) else [sv]): - fld = str(e) - if not fld: - continue - if fld != attr_name and '.' not in fld and not re.match(r'^\d+/\d+', fld): - fld = f'{attr_name}.{fld}' - if fld not in save_fields: - save_fields.append(fld) - if save_fields: - lines.append(f'{inner}') - for f in save_fields: - lines.append(f'{inner}\t{esc_xml(f)}') - lines.append(f'{inner}') - # Проверка заполнения → (реальный тег; в схеме нет). - # bool true → ShowError; строка → verbatim. Синоним fillChecking. - fc_raw = attr['fillCheck'] if 'fillCheck' in attr else attr.get('fillChecking') - if fc_raw: - fcv = 'ShowError' if isinstance(fc_raw, bool) else str(fc_raw) - lines.append(f'{inner}{fcv}') - - # UseAlways: поля, всегда читаемые. Две формы DSL сливаются: - # attr.useAlways[] (короткие имена) + columns с useAlways:true → ИмяРеквизита.Поле. - ua_fields = [] - for e in (attr.get('useAlways') or []): - fld = str(e) - # Префикс "ИмяРеквизита." добавляем к коротким именам. Поля дин-списка с маркером "~" - # (query-поля, ~13% корпуса) — префикс ставится ПОСЛЕ "~": ~Остановлен → ~Список.Остановлен. - # Полная форма (~Список.Остановлен / Список.Остановлен) — verbatim (forgiving ввод). - if fld.startswith('~'): - bare = fld[1:] - if not re.match(r'^' + re.escape(attr_name) + r'\.', bare): - bare = f'{attr_name}.{bare}' - fld = f'~{bare}' - elif not re.match(r'^' + re.escape(attr_name) + r'\.', fld): - fld = f'{attr_name}.{fld}' - if fld not in ua_fields: - ua_fields.append(fld) - for col in (attr.get('columns') or []): - if col.get('useAlways') is True: - fld = f'{attr_name}.{col["name"]}' - if fld not in ua_fields: - ua_fields.append(fld) - if ua_fields: - lines.append(f'{inner}') - for f in ua_fields: - lines.append(f'{inner}\t{f}') - lines.append(f'{inner}') - - emit_functional_options(lines, attr.get('functionalOptions'), inner) - - # Columns: прямые + (доп. колонки табличных частей объекта). - # Прямые сначала, затем AdditionalColumns-группы. Для дин-списка (settings) прямые НЕ эмитим. - has_direct_cols = bool(attr.get('columns')) and len(attr['columns']) > 0 and not attr.get('settings') - has_add_cols = bool(attr.get('additionalColumns')) and len(attr['additionalColumns']) > 0 - if has_direct_cols or has_add_cols: - lines.append(f'{inner}') - if has_direct_cols: - seen_cols = set() # колонки уникальны в пределах своего реквизита - for col in attr['columns']: - _ensure_unique(str(col['name']), seen_cols, f"column of '{attr_name}'") - emit_attr_column(lines, col, f'{inner}\t') - if has_add_cols: - for ac in attr['additionalColumns']: - ac_cols = ac.get('columns') or [] - if not ac_cols: - # Пустая группа доп.колонок (table-ref без колонок) → self-closing (как платформа) - lines.append(f'{inner}\t') - continue - lines.append(f'{inner}\t') - seen_ac_cols = set() # уникальность в пределах группы AdditionalColumns - for col in ac_cols: - _ensure_unique(str(col['name']), seen_ac_cols, f"column of '{attr_name}'") - emit_attr_column(lines, col, f'{inner}\t\t') - lines.append(f'{inner}\t') - lines.append(f'{inner}') - - # Settings (динамический список) - if attr.get('settings'): - s = attr['settings'] - lines.append(f'{inner}') - si = f'{inner}\t' - # Порядок платформы: AutoFillAvailableFields, ManualQuery, DynamicDataRead, QueryText, Field*, MainTable, ListSettings - # AutoFillAvailableFields — дефолт true; эмитим только при заданном ключе (отклонение). - if s.get('autoFillAvailableFields') is not None: - lines.append(f'{si}{"true" if s["autoFillAvailableFields"] else "false"}') - # Порядок платформы: ManualQuery, DynamicDataRead, QueryText, Field*, MainTable, ListSettings - has_query = bool(s.get('query') and str(s['query']).strip()) - mq = 'true' if (has_query or s.get('manualQuery')) else 'false' - lines.append(f'{si}{mq}') - # DynamicDataRead: дефолт true; false только при явном отключении - ddr = 'false' if s.get('dynamicDataRead') is False else 'true' - lines.append(f'{si}{ddr}') - if has_query: - qtext = resolve_query_value(str(s['query']), QUERY_BASE_DIR) - lines.append(f'{si}{esc_xml(qtext)}') - # Явные поля набора (редко): override title/dataPath - if s.get('fields'): - for fld in s['fields']: - # Тип поля набора: DataSetFieldField (дефолт) vs DataSetFieldNestedDataSet - # (поле-вложенный набор = реквизит табличной части; маркер nested). - ftype = 'DataSetFieldNestedDataSet' if fld.get('nested') else 'DataSetFieldField' - lines.append(f'{si}') - dp = fld.get('dataPath') or fld.get('field') - lines.append(f'{si}\t{esc_xml(str(dp))}') - lines.append(f'{si}\t{esc_xml(str(fld.get("field", "")))}') - if fld.get('title'): - lines.append(f'{si}\t') - emit_ml_items(lines, f'{si}\t\t', fld['title']) - lines.append(f'{si}\t') - # Ограничения использования поля — после title, перед presentationExpression - emit_restrict_block(lines, 'useRestriction', fld.get('useRestriction'), f'{si}\t') - emit_restrict_block(lines, 'attributeUseRestriction', fld.get('attributeUseRestriction'), f'{si}\t') - # presentationExpression поля — перед valueType (порядок исходника) - if fld.get('presentationExpression'): - lines.append(f'{si}\t{esc_xml(str(fld["presentationExpression"]))}') - # valueType поля набора (тип значения; вычисляемые/кастомные поля) - if fld.get('valueType'): - emit_dl_value_type(lines, fld['valueType'], f'{si}\t') - # appearance поля (формат/оформление) — после valueType (порядок исходника) - if fld.get('appearance'): - lines.append(f'{si}\t') - for ak, av in fld['appearance'].items(): - emit_appearance_value(lines, ak, av, f'{si}\t\t') - lines.append(f'{si}\t') - # inputParameters поля (связь по параметрам выбора) — в конце - if fld.get('inputParameters'): - emit_dl_input_parameters(lines, fld['inputParameters'], f'{si}\t') - lines.append(f'{si}') - # Вычисляемые поля DataSet () — после Field*, до Parameter*. - emit_calc_fields(lines, s.get('calculatedFields'), si) - # Schema-параметры дин-списка (DataCompositionSchemaParameter) — после Field*, до MainTable. - emit_dl_parameters(lines, s.get('parameters'), si) - # Ключ набора (query-based список без MainTable): KeyType (RowNumber/FieldValue/RowKey) - # + KeyField* — после Parameter*, до MainTable. Захват/эмит факт. значений. - if s.get('keyType'): - lines.append(f'{si}{esc_xml(str(s["keyType"]))}') - if s.get('keyFields'): - for kf in s['keyFields']: - lines.append(f'{si}{esc_xml(str(kf))}') - if s.get('mainTable'): - lines.append(f'{si}{normalize_meta_type_ref(str(s["mainTable"]))}') - # AutoSaveUserSettings — после MainTable (дефолт true; эмитим только при заданном ключе = отклонении). - if s.get('autoSaveUserSettings') is not None: - lines.append(f'{si}{"true" if s["autoSaveUserSettings"] else "false"}') - # ListSettings: filter/order/conditionalAppearance (skd-грамматика) + каноничные блок-GUID. - # Нет items → контейнеры всё равно эмитятся (blockMeta) = каноничный пустой скелет платформы. - lsi = f'{si}\t' - lines.append(f'{si}') - ls_open_idx = len(lines) - 1 # для self-closing, если внутри ничего не эмитнётся - ls_shape = s.get('listSettings') - if ls_shape is not None: - # Частичная/минимальная форма скелета — эмитим ТОЛЬКО указанные части с их блок-метой. - for tag, meta in ls_shape.items(): - meta = str(meta) - bvm = 'Normal' if 'v' in meta else None - if tag == 'filter': - bus = CANON_FILTER_ID if 'u' in meta else None - emit_filter(lines, s.get('filter'), lsi, block_view_mode=bvm, block_user_setting_id=bus) - elif tag == 'order': - bus = CANON_ORDER_ID if 'u' in meta else None - emit_order(lines, s.get('order'), lsi, block_view_mode=bvm, block_user_setting_id=bus) - elif tag == 'conditionalAppearance': - bus = CANON_CA_ID if 'u' in meta else None - emit_conditional_appearance(lines, s.get('conditionalAppearance'), lsi, block_view_mode=bvm, block_user_setting_id=bus) - elif tag == 'itemsViewMode': - lines.append(f'{lsi}Normal') - elif tag == 'itemsUserSettingID': - lines.append(f'{lsi}{CANON_ITEMS_ID}') - elif tag == 'structure': - emit_list_grouping(lines, get_list_grouping_value(s), lsi) - else: - # Полный каноничный скелет (умолчание, ~93% форм) — без изменений. - emit_filter(lines, s.get('filter'), lsi, block_view_mode='Normal', block_user_setting_id=CANON_FILTER_ID) - # dataParameters — после filter, до order (XSD-порядок ListSettings) - if 'dataParameters' in s: - emit_data_parameters(lines, s.get('dataParameters'), lsi) - emit_order(lines, s.get('order'), lsi, block_view_mode='Normal', block_user_setting_id=CANON_ORDER_ID) - emit_conditional_appearance(lines, s.get('conditionalAppearance'), lsi, block_view_mode='Normal', block_user_setting_id=CANON_CA_ID) - # Группировка строк списка (авторинг без round-trip дескриптора) — после CA, до itemsViewMode - emit_list_grouping(lines, get_list_grouping_value(s), lsi) - lines.append(f'{lsi}Normal') - lines.append(f'{lsi}{CANON_ITEMS_ID}') - if len(lines) - 1 == ls_open_idx: - # Пустой дескриптор listSettings:{} (оригинал = ) → зеркалим self-closing. - lines[ls_open_idx] = f'{si}' - else: - lines.append(f'{si}') - lines.append(f'{inner}') - - lines.append(f'{indent}\t') - # Условное оформление формы — последний child (та же DCS-грамматика, что settings CA) - emit_conditional_appearance(lines, conditional_appearance, f'{indent}\t', wrap_tag='ConditionalAppearance') - lines.append(f'{indent}') - - -# --- Parameter emitter --- - -def emit_parameters(lines, params, indent): - if not params or len(params) == 0: - return - - lines.append(f'{indent}') - seen_params = set() - for param in params: - _ensure_unique(str(param['name']), seen_params, 'parameter') - lines.append(f'{indent}\t') - inner = f'{indent}\t\t' - - emit_type(lines, str(param.get('type', '')), inner) - - if param.get('key') is True: - lines.append(f'{inner}true') - - lines.append(f'{indent}\t') - lines.append(f'{indent}') - - -# --- Command emitter --- - -def emit_commands(lines, cmds, indent): - if not cmds or len(cmds) == 0: - return - - lines.append(f'{indent}') - seen_cmds = set() - for cmd in cmds: - cmd_id = new_id() - _ensure_unique(str(cmd['name']), seen_cmds, 'command') - lines.append(f'{indent}\t') - inner = f'{indent}\t\t' - - # Заголовок команды (зеркало emit_title): ключ есть+непустой → эмитим; ключ есть+"" → суппресс - # (в оригинале нет — не додумывать); ключ отсутствует → авто-вывод из имени. - if 'title' in cmd: - if cmd['title']: - emit_mltext(lines, inner, 'Title', cmd['title']) - else: - cmd_title = title_from_name(str(cmd['name'])) - if cmd_title: - emit_mltext(lines, inner, 'Title', cmd_title) - - if cmd.get('tooltip'): - emit_mltext(lines, inner, 'ToolTip', cmd['tooltip']) - - # Доступность команды по ролям (после ToolTip, до Action) - if cmd.get('use') is not None: - emit_xr_flag(lines, 'Use', cmd.get('use'), inner) - - if cmd.get('action'): - lines.append(f'{inner}<Action>{cmd["action"]}</Action>') - - if cmd.get('modifiesSavedData') is True: - lines.append(f'{inner}<ModifiesSavedData>true</ModifiesSavedData>') - - emit_functional_options(lines, cmd.get('functionalOptions'), inner) - - if cmd.get('currentRowUse'): - lines.append(f'{inner}<CurrentRowUse>{cmd["currentRowUse"]}</CurrentRowUse>') - - # Используемая таблица — имя элемента-таблицы (xsi:type обязателен). - # Forgiving-ключи: table / associatedTableElementId (XML-тег) / ИспользуемаяТаблица (рус., регистр-незав.) - _cmd_norm = {k.replace(' ', '').lower(): v for k, v in cmd.items()} - cmd_table = (_cmd_norm.get('table') or _cmd_norm.get('associatedtableelementid') - or _cmd_norm.get('используемаятаблица')) - if cmd_table: - lines.append(f'{inner}<AssociatedTableElementId xsi:type="xs:string">{esc_xml(str(cmd_table))}</AssociatedTableElementId>') - - if cmd.get('shortcut'): - lines.append(f'{inner}<Shortcut>{cmd["shortcut"]}</Shortcut>') - - emit_command_picture(lines, cmd.get('picture'), cmd.get('loadTransparent'), inner) - - if cmd.get('representation'): - lines.append(f'{inner}<Representation>{cmd["representation"]}</Representation>') - - lines.append(f'{indent}\t</Command>') - lines.append(f'{indent}</Commands>') - - -# Командный интерфейс формы (<CommandInterface>): панели CommandBar + NavigationPanel. -# Элемент: строка (голый command, Type=Auto) или dict. Порядок тегов: -# Command, Type(деф. Auto), Attribute, CommandGroup, Index, DefaultVisible, Visible(xr-flag). -def _resolve_command_group_key(key, panel_tag): - """Ключ-группа древовидной формы → CommandGroup (зависит от панели); иначе verbatim.""" - k = re.sub(r'\s', '', str(key)).lower() - if panel_tag == 'NavigationPanel': - m = {'important': 'FormNavigationPanelImportant', 'важное': 'FormNavigationPanelImportant', - 'goto': 'FormNavigationPanelGoTo', 'перейти': 'FormNavigationPanelGoTo', - 'seealso': 'FormNavigationPanelSeeAlso', 'смтакже': 'FormNavigationPanelSeeAlso'} - else: - m = {'important': 'FormCommandBarImportant', 'важное': 'FormCommandBarImportant', - 'createbasedon': 'FormCommandBarCreateBasedOn', 'создатьнаосновании': 'FormCommandBarCreateBasedOn'} - return m.get(k, key) - - -def emit_command_interface(lines, ci, indent): - if not ci: - return - inner = f'{indent}\t' - panels = [ - ('CommandBar', ('commandBar', 'команднаяПанель', 'КоманднаяПанель')), - ('NavigationPanel', ('navigationPanel', 'панельНавигации', 'ПанельНавигации')), - ] - present = [] - for tag, syns in panels: - items = None - for syn in syns: - if isinstance(ci, dict) and syn in ci: - items = ci[syn] - break - if items is not None: - present.append((tag, items)) - if not present: - return - lines.append(f'{indent}<CommandInterface>') - for tag, items in present: - lines.append(f'{inner}<{tag}>') - # Нормализация: плоский список пар (элемент, group-из-дерева). dict → древовидная форма. - flat = [] - if isinstance(items, dict): - for gkey, gitems in items.items(): - grp_tree = _resolve_command_group_key(gkey, tag) - for it in gitems: - flat.append((it, grp_tree)) - else: - for it in items: - flat.append((it, None)) - for item, tree_group in flat: - if isinstance(item, str): - cmd, typ, attr, grp, idx, dv, vis = item, 'Auto', None, None, None, None, None - else: - cmd = get_el_prop(item, ('command', 'команда')) - typ = get_el_prop(item, ('type', 'тип')) or 'Auto' - attr = get_el_prop(item, ('attribute', 'реквизит')) - grp = get_el_prop(item, ('group', 'группа', 'группаКоманд')) - idx = get_el_prop(item, ('index', 'индекс')) - dv = get_el_prop(item, ('defaultVisible', 'видимость', 'видимостьПоУмолчанию')) - vis = get_el_prop(item, ('visible', 'видимостьПоРолям', 'настройкаВидимости')) - # group из дерева побеждает (если задан и непустой); явный group элемента — фолбэк - if tree_group: - grp = tree_group - lines.append(f'{inner}\t<Item>') - lines.append(f'{inner}\t\t<Command>{esc_xml(str(cmd))}</Command>') - lines.append(f'{inner}\t\t<Type>{typ}</Type>') - if attr: - lines.append(f'{inner}\t\t<Attribute>{esc_xml(str(attr))}</Attribute>') - if grp: - lines.append(f'{inner}\t\t<CommandGroup>{esc_xml(str(grp))}</CommandGroup>') - if idx is not None: - lines.append(f'{inner}\t\t<Index>{idx}</Index>') - if dv is not None: - lines.append(f'{inner}\t\t<DefaultVisible>{"true" if dv else "false"}</DefaultVisible>') - if vis is not None: - emit_xr_flag(lines, 'Visible', vis, f'{inner}\t\t') - lines.append(f'{inner}\t</Item>') - lines.append(f'{inner}</{tag}>') - lines.append(f'{indent}</CommandInterface>') - - -# --- Properties emitter --- - -PROP_MAP = { - "autoTitle": "AutoTitle", - "windowOpeningMode": "WindowOpeningMode", - "commandBarLocation": "CommandBarLocation", - "saveDataInSettings": "SaveDataInSettings", - "autoSaveDataInSettings": "AutoSaveDataInSettings", - "autoTime": "AutoTime", - "usePostingMode": "UsePostingMode", - "repostOnWrite": "RepostOnWrite", - "autoURL": "AutoURL", - "autoFillCheck": "AutoFillCheck", - "customizable": "Customizable", - "enterKeyBehavior": "EnterKeyBehavior", - "verticalScroll": "VerticalScroll", - "scalingMode": "ScalingMode", - "useForFoldersAndItems": "UseForFoldersAndItems", - "reportResult": "ReportResult", - "detailsData": "DetailsData", - "reportFormType": "ReportFormType", - "autoShowState": "AutoShowState", - "width": "Width", - "height": "Height", - "group": "Group", -} - - -def emit_properties(lines, props, indent): - if not props: - return - - for p_name, p_value in props.items(): - xml_name = PROP_MAP.get(p_name) - if not xml_name: - # Auto PascalCase - xml_name = p_name[0].upper() + p_name[1:] - - # Пустая строка = суппресс-маркер (напр. autoTitle:"" — не эмитить и не додумывать) - if isinstance(p_value, str) and p_value == '': - continue - # Convert boolean to lowercase - if isinstance(p_value, bool): - val = 'true' if p_value else 'false' - else: - val = str(p_value) - lines.append(f'{indent}<{xml_name}>{val}</{xml_name}>') - - - -def detect_format_version(d): - while d: - cfg_path = os.path.join(d, "Configuration.xml") - if os.path.isfile(cfg_path): - with open(cfg_path, "r", encoding="utf-8-sig") as f: - head = f.read(2000) - m = re.search(r'<MetaDataObject[^>]+version="(\d+\.\d+)"', head) - if m: - return m.group(1) - parent = os.path.dirname(d) - if parent == d: - break - d = parent - return "2.17" - - -def _normalize_elements(defn): - """Convert dict-style elements from --from-object generators to list-style expected by compiler. - Generator format: elements = {"ИмяЭлемента": {"element": "input", "path": "..."}, ...} - Compiler format: elements = [{"input": "ИмяЭлемента", "path": "..."}, ...] - Also handles nested 'elements' in groups and 'columns' in tables recursively. - """ - def convert_elements(els): - if isinstance(els, list): - # Already list format — but may have nested dicts inside groups - result = [] - for el in els: - if isinstance(el, dict): - el = dict(el) # copy - if 'elements' in el and isinstance(el['elements'], dict): - el['elements'] = convert_elements(el['elements']) - if 'columns' in el and isinstance(el['columns'], dict): - el['columns'] = convert_columns(el['columns']) - result.append(el) - return result - if isinstance(els, dict): - result = [] - for name, props in els.items(): - if not isinstance(props, dict): - continue - new_el = {} - el_type = props.get('element', 'input') - # Map element type to the key name used in JSON DSL - type_map = { - 'input': 'input', 'check': 'check', 'labelField': 'labelField', - 'table': 'table', 'group': 'group', 'pages': 'pages', - 'page': 'page', 'label': 'label', 'button': 'button', - 'checkBox': 'check', 'radioButton': 'radioButton', - 'pictureField': 'pictureField', - } - mapped_type = type_map.get(el_type, el_type) - new_el[mapped_type] = name - for k, v in props.items(): - if k == 'element': - continue - if k == 'elements' and isinstance(v, dict): - new_el['elements'] = convert_elements(v) - elif k == 'columns' and isinstance(v, dict): - new_el['columns'] = convert_columns(v) - elif k == 'groupType': - # groupType → group property in DSL - new_el['group'] = v - elif k == 'showTitle': - new_el['showTitle'] = v - elif k == 'representation': - new_el['representation'] = v - elif k == 'autoCommandBar': - new_el['autoCommandBar'] = v - elif k == 'commandBarLocation': - new_el['commandBarLocation'] = v - else: - new_el[k] = v - result.append(new_el) - return result - return els - - def convert_columns(cols): - if isinstance(cols, list): - return cols - if isinstance(cols, dict): - result = [] - for name, props in cols.items(): - if not isinstance(props, dict): - continue - new_col = {} - el_type = props.get('element', 'input') - type_map = { - 'input': 'input', 'check': 'check', 'labelField': 'labelField', - 'checkBox': 'check', - } - mapped_type = type_map.get(el_type, el_type) - new_col[mapped_type] = name - for k, v in props.items(): - if k == 'element': - continue - new_col[k] = v - result.append(new_col) - return result - return cols - - if 'elements' in defn: - defn['elements'] = convert_elements(defn['elements']) - return defn - - -def main(): - sys.stdout.reconfigure(encoding="utf-8") - sys.stderr.reconfigure(encoding="utf-8") - global _next_id - - parser = argparse.ArgumentParser(description='Compile 1C managed form from JSON or object metadata', allow_abbrev=False) - parser.add_argument('-JsonPath', type=str, default=None) - parser.add_argument('-OutputPath', type=str, required=True) - parser.add_argument('-FromObject', action='store_true', default=False) - parser.add_argument('-ObjectPath', type=str, default=None) - parser.add_argument('-Purpose', type=str, default=None) - parser.add_argument('-Preset', type=str, default='erp-standard') - parser.add_argument('-EmitDsl', type=str, default=None) - args = parser.parse_args() - - # Form name -> purpose mapping - _FORM_NAME_TO_PURPOSE = { - '\u0424\u043e\u0440\u043c\u0430\u0414\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430': 'Item', # ФормаДокумента - '\u0424\u043e\u0440\u043c\u0430\u042d\u043b\u0435\u043c\u0435\u043d\u0442\u0430': 'Item', # ФормаЭлемента - '\u0424\u043e\u0440\u043c\u0430\u0421\u043f\u0438\u0441\u043a\u0430': 'List', # ФормаСписка - '\u0424\u043e\u0440\u043c\u0430\u0412\u044b\u0431\u043e\u0440\u0430': 'Choice', # ФормаВыбора - '\u0424\u043e\u0440\u043c\u0430\u0413\u0440\u0443\u043f\u043f\u044b': 'Folder', # ФормаГруппы - '\u0424\u043e\u0440\u043c\u0430\u0417\u0430\u043f\u0438\u0441\u0438': 'Record', # ФормаЗаписи - '\u0424\u043e\u0440\u043c\u0430\u0421\u0447\u0435\u0442\u0430': 'Item', # ФормаСчета - '\u0424\u043e\u0440\u043c\u0430\u0423\u0437\u043b\u0430': 'Item', # ФормаУзла - } - - # Mutual exclusion validation - if args.FromObject and args.JsonPath: - print("Cannot use both -JsonPath and -FromObject. Choose one mode.", file=sys.stderr) - sys.exit(1) - if not args.FromObject and not args.JsonPath: - print("Either -JsonPath or -FromObject is required.", file=sys.stderr) - sys.exit(1) - - # Normalize OutputPath in from-object mode: append /Ext/Form.xml if missing - if args.FromObject: - out_norm = args.OutputPath.rstrip('/\\') - if not re.search(r'[/\\]Ext[/\\]Form\.xml$', out_norm): - if re.search(r'[/\\]Ext$', out_norm): - args.OutputPath = out_norm + '/Form.xml' - else: - args.OutputPath = out_norm + '/Ext/Form.xml' - print(f"[resolved] OutputPath -> {args.OutputPath}") - - # --- Detect XML format version --- - out_path_resolved = args.OutputPath if os.path.isabs(args.OutputPath) else os.path.join(os.getcwd(), args.OutputPath) - format_version = detect_format_version(os.path.dirname(out_path_resolved)) - - # --- 0. From-object mode --- - if args.FromObject: - # Resolve object path and purpose from OutputPath convention: - # .../TypePlural/ObjectName/Forms/FormName/Ext/Form.xml - out_abs = out_path_resolved - parts = re.split(r'[/\\]', out_abs) - forms_idx = -1 - for i in range(len(parts) - 1, -1, -1): - if parts[i] == 'Forms': - forms_idx = i - break - - resolved_object_path = None - resolved_purpose = None - - if forms_idx >= 2: - form_name = parts[forms_idx + 1] - object_name = parts[forms_idx - 1] - type_plural_and_above = os.sep.join(parts[:forms_idx - 1]) - - if form_name in _FORM_NAME_TO_PURPOSE: - resolved_purpose = _FORM_NAME_TO_PURPOSE[form_name] - - candidate = os.path.join(type_plural_and_above, f'{object_name}.xml') - if os.path.exists(candidate): - resolved_object_path = candidate - - # Apply: explicit -ObjectPath / -Purpose override resolved - from_obj_path = None - if args.ObjectPath: - from_obj_path = args.ObjectPath if os.path.isabs(args.ObjectPath) else os.path.join(os.getcwd(), args.ObjectPath) - if not from_obj_path.endswith('.xml'): - from_obj_path += '.xml' - elif resolved_object_path: - from_obj_path = resolved_object_path - print(f"[resolved] ObjectPath -> {from_obj_path}") - else: - print("Cannot derive object path from OutputPath. Use -ObjectPath explicitly.", file=sys.stderr) - sys.exit(1) - - if not os.path.exists(from_obj_path): - print(f"Object file not found: {from_obj_path}", file=sys.stderr) - sys.exit(1) - - purpose = args.Purpose or resolved_purpose or 'Item' - if resolved_purpose and not args.Purpose: - print(f"[resolved] Purpose -> {purpose}") - - meta = parse_object_meta(from_obj_path) - print(f"[from-object] Type={meta['Type']}, Name={meta['Name']}, Attrs={len(meta['Attributes'])}, TS={len(meta['TabularSections'])}") - - preset_data = load_preset(args.Preset, os.path.dirname(os.path.abspath(__file__)), out_path_resolved) - - supported = { - 'Document': ['Item', 'List', 'Choice'], - 'Catalog': ['Item', 'Folder', 'List', 'Choice'], - 'InformationRegister': ['Record', 'List'], - 'AccumulationRegister': ['List'], - 'ChartOfCharacteristicTypes': ['Item', 'Folder', 'List', 'Choice'], - 'ExchangePlan': ['Item', 'List', 'Choice'], - 'ChartOfAccounts': ['Item', 'Folder', 'List', 'Choice'], - } - if meta['Type'] not in supported: - print(f"Object type '{meta['Type']}' not supported. Supported: Document, Catalog, InformationRegister, AccumulationRegister, ChartOfCharacteristicTypes, ExchangePlan, ChartOfAccounts.", file=sys.stderr) - sys.exit(1) - if purpose not in supported[meta['Type']]: - print(f"Purpose '{purpose}' not valid for {meta['Type']}. Valid: {', '.join(supported[meta['Type']])}", file=sys.stderr) - sys.exit(1) - - dsl_dispatch = { - 'Document': generate_document_dsl, - 'Catalog': generate_catalog_dsl, - 'InformationRegister': generate_information_register_dsl, - 'AccumulationRegister': generate_accumulation_register_dsl, - 'ChartOfCharacteristicTypes': generate_chart_of_characteristic_types_dsl, - 'ExchangePlan': generate_exchange_plan_dsl, - 'ChartOfAccounts': generate_chart_of_accounts_dsl, - } - dsl = dsl_dispatch[meta['Type']](meta, preset_data, purpose) - - if args.EmitDsl: - dsl_path = args.EmitDsl if os.path.isabs(args.EmitDsl) else os.path.join(os.getcwd(), args.EmitDsl) - os.makedirs(os.path.dirname(dsl_path) or '.', exist_ok=True) - with open(dsl_path, 'w', encoding='utf-8') as f: - json.dump(dsl, f, ensure_ascii=False, indent=2) - print(f"[from-object] DSL saved: {dsl_path}") - - defn = json.loads(json.dumps(dsl)) # normalize OrderedDict to regular dict - # Convert dict-style elements (from generators) to list-style (expected by compiler) - defn = _normalize_elements(defn) - else: - # --- 1. Load and validate JSON --- - json_path = args.JsonPath - if not os.path.exists(json_path): - print(f"File not found: {json_path}", file=sys.stderr) - sys.exit(1) - - with open(json_path, 'r', encoding='utf-8-sig') as f: - defn = json.load(f) - global QUERY_BASE_DIR - QUERY_BASE_DIR = os.path.dirname(os.path.abspath(json_path)) - - # --- 1b. Pre-pass: synonyms, main attribute inference, heuristics, autoCmdBar extraction --- - def _normalize_synonyms(el): - if not isinstance(el, dict): - return - # Companion-панели (объект/массив-значение) → commandBar/contextMenu - normalize_panel_synonyms(el) - # Тип-синонимы: commandBar/autoCommandBar → элемент-тип ТОЛЬКО при строковом значении - synonyms = {'commandBar': 'cmdBar', 'autoCommandBar': 'autoCmdBar', 'extTooltip': 'extendedTooltip'} - for src, dst in synonyms.items(): - if src in el and dst not in el: - if src in STR_ONLY_TYPE_SYNONYMS and not isinstance(el[src], str): - continue - el[dst] = el.pop(src) - # Рекурсия в детей панелей (commandBar/contextMenu) - for pk in ('commandBar', 'contextMenu'): - pv = el.get(pk) - kids = pv if isinstance(pv, list) else (pv.get('children') if isinstance(pv, dict) else None) - if isinstance(kids, list): - for child in kids: - _normalize_synonyms(child) - if isinstance(el.get('children'), list): - for child in el['children']: - _normalize_synonyms(child) - if isinstance(el.get('columns'), list): - for child in el['columns']: - _normalize_synonyms(child) - - def _has_cmd_bar_recursive(el): - if not isinstance(el, dict): - return False - if el.get('cmdBar') is not None: - return True - if isinstance(el.get('children'), list): - for child in el['children']: - if _has_cmd_bar_recursive(child): - return True - if isinstance(el.get('columns'), list): - for child in el['columns']: - if _has_cmd_bar_recursive(child): - return True - return False - - def _apply_dlist_table_heuristic(el, list_name, has_main_table): - if not isinstance(el, dict): - return - if el.get('table') is not None and str(el.get('path', '')) == list_name: - # Маркер дин-список-таблицы → emit_table эмитит блок свойств - el['_dynList'] = True - if 'tableAutofill' not in el: - el['tableAutofill'] = False - if 'commandBarLocation' not in el: - el['commandBarLocation'] = 'None' - # RowPictureDataPath: умный дефолт <Список>.DefaultPicture, если ключ ОТСУТСТВУЕТ. - # Декомпилятор опускает при rpdp == smart-default; реальное отсутствие → ""-маркер (не - # перезатирается). Гейт has_main_table снят: дин-список без mainTable тоже несёт RowPictureDataPath. - if 'rowPictureDataPath' not in el: - el['rowPictureDataPath'] = f'{list_name}.DefaultPicture' - if isinstance(el.get('children'), list): - for child in el['children']: - _apply_dlist_table_heuristic(child, list_name, has_main_table) - - def _is_object_like_type(t): - if not t: - return False - if t == 'DynamicList' or t == 'ConstantsSet': - return True - object_suffixes = ( - 'CatalogObject', 'DocumentObject', 'DataProcessorObject', 'ReportObject', - 'ExternalDataProcessorObject', 'ExternalReportObject', 'BusinessProcessObject', - 'TaskObject', 'ChartOfAccountsObject', 'ChartOfCharacteristicTypesObject', - 'ChartOfCalculationTypesObject', 'ExchangePlanObject', - ) - record_set_prefixes = ( - 'InformationRegisterRecordSet', 'AccumulationRegisterRecordSet', - 'AccountingRegisterRecordSet', 'CalculationRegisterRecordSet', - 'InformationRegisterRecordManager', - ) - for s in object_suffixes: - if t.startswith(s + '.'): - return True - for s in record_set_prefixes: - if t.startswith(s + '.'): - return True - return False - - # 1b.1: Normalize synonyms recursively - if isinstance(defn.get('elements'), list): - for el in defn['elements']: - _normalize_synonyms(el) - - # 1b.2: Extract autoCmdBar element from defn['elements'] - main_acb_def = None - if isinstance(defn.get('elements'), list): - auto_bars = [el for el in defn['elements'] if isinstance(el, dict) and el.get('autoCmdBar') is not None] - if len(auto_bars) > 1: - print(f"form-compile: more than one autoCmdBar in def.elements (found {len(auto_bars)}); only one allowed.", file=sys.stderr) - sys.exit(1) - if len(auto_bars) == 1: - main_acb_def = auto_bars[0] - defn['elements'] = [el for el in defn['elements'] if el is not main_acb_def] - - # 1b.3: Infer main attribute - if isinstance(defn.get('attributes'), list): - has_explicit_main = any(a.get('main') is True for a in defn['attributes'] if isinstance(a, dict)) - if not has_explicit_main: - candidates = [] - for a in defn['attributes']: - if not isinstance(a, dict): - continue - if 'main' in a and a.get('main') is False: - continue - if _is_object_like_type(str(a.get('type', ''))): - candidates.append(a) - if len(candidates) == 1: - candidates[0]['main'] = True - print(f"[INFO] Inferred main attribute: {candidates[0].get('name')} ({candidates[0].get('type')})") - elif len(candidates) > 1: - names = ', '.join(c.get('name', '') for c in candidates) - print(f"[WARN] Multiple main-attribute candidates: {names}; specify \"main\": true explicitly") - - # 1b.4: DynamicList → table heuristic (для ВСЕХ DynamicList-реквизитов, не только main) - if isinstance(defn.get('attributes'), list) and isinstance(defn.get('elements'), list): - for attr in defn['attributes']: - if not isinstance(attr, dict) or str(attr.get('type', '')) != 'DynamicList': - continue - settings = attr.get('settings') or {} - has_mt = bool(isinstance(settings, dict) and settings.get('mainTable')) - for el in defn['elements']: - _apply_dlist_table_heuristic(el, attr.get('name', ''), has_mt) - - # 1b.5: Compute main AutoCommandBar Autofill (B3) - def _compute_main_acb_autofill(): - if main_acb_def is not None: - if 'autofill' in main_acb_def: - return bool(main_acb_def.get('autofill')) - return True - if isinstance(defn.get('elements'), list): - for el in defn['elements']: - if _has_cmd_bar_recursive(el): - return False - return True - - # --- 2. Main compilation --- - _next_id = 0 - _seen_element_names.clear() # пул имён элементов (на случай повторного вызова в одном процессе) - lines = [] - - lines.append('<?xml version="1.0" encoding="UTF-8"?>') - lines.append(f'<Form xmlns="http://v8.1c.ru/8.3/xcf/logform" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:dcscor="http://v8.1c.ru/8.1/data-composition-system/core" xmlns:dcssch="http://v8.1c.ru/8.1/data-composition-system/schema" xmlns:dcsset="http://v8.1c.ru/8.1/data-composition-system/settings" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="{format_version}">') - - # Title - form_title = defn.get('title') - if not form_title and defn.get('properties') and defn['properties'].get('title'): - form_title = defn['properties']['title'] - if form_title: - emit_mltext(lines, '\t', 'Title', form_title) - - # Properties (skip 'title' — handled above) - # When form-level Title is set, default autoTitle=false (≈95% of ERP forms do this; - # otherwise platform appends synonym → "Title: Synonym" double-titles). - props_src = defn.get('properties') or {} - props_clone = OrderedDict() - if form_title and 'autoTitle' not in props_src: - props_clone['autoTitle'] = False - for k, v in props_src.items(): - if k != 'title': - props_clone[k] = v - emit_properties(lines, props_clone, '\t') - - # CommandSet (excluded commands) - if defn.get('excludedCommands') and len(defn['excludedCommands']) > 0: - lines.append('\t<CommandSet>') - for cmd in defn['excludedCommands']: - lines.append(f'\t\t<ExcludedCommand>{cmd}</ExcludedCommand>') - lines.append('\t</CommandSet>') - - # MobileDeviceCommandBarContent — форменный список имён командных панелей/кнопок - # (Presentation пустой, CheckState=0, тип xs:string — константы; варьируется только имя-Value). - # 12 форм корпуса несут один пустой item (Value="") — список присутствует, но не пуст по len. - if defn.get('mobileCommandBarContent') is not None and len(defn['mobileCommandBarContent']) > 0: - lines.append('\t<MobileDeviceCommandBarContent>') - for nm in defn['mobileCommandBarContent']: - lines.append('\t\t<xr:Item>') - lines.append('\t\t\t<xr:Presentation/>') - lines.append('\t\t\t<xr:CheckState>0</xr:CheckState>') - # пустое значение → самозакрывающийся тег (зеркало платформы) - if not str(nm): - lines.append('\t\t\t<xr:Value xsi:type="xs:string"/>') - else: - lines.append(f'\t\t\t<xr:Value xsi:type="xs:string">{esc_xml(str(nm))}</xr:Value>') - lines.append('\t\t</xr:Item>') - lines.append('\t</MobileDeviceCommandBarContent>') - - # AutoCommandBar (always present, id=-1) - acb_autofill = _compute_main_acb_autofill() - acb_name = '\u0424\u043e\u0440\u043c\u0430\u041a\u043e\u043c\u0430\u043d\u0434\u043d\u0430\u044f\u041f\u0430\u043d\u0435\u043b\u044c' - acb_halign = None - if main_acb_def is not None: - v = main_acb_def.get('autoCmdBar') - if v: - acb_name = str(v) - if main_acb_def.get('name'): - acb_name = str(main_acb_def['name']) - if main_acb_def.get('horizontalAlign'): - acb_halign = str(main_acb_def['horizontalAlign']) - has_acb_children = bool(main_acb_def and isinstance(main_acb_def.get('children'), list) and len(main_acb_def['children']) > 0) - has_inner = bool(acb_halign) or (not acb_autofill) or has_acb_children - if has_inner: - lines.append(f'\t<AutoCommandBar name="{acb_name}" id="-1">') - if acb_halign: - lines.append(f'\t\t<HorizontalAlign>{acb_halign}</HorizontalAlign>') - if not acb_autofill: - lines.append('\t\t<Autofill>false</Autofill>') - if has_acb_children: - lines.append('\t\t<ChildItems>') - for child in main_acb_def['children']: - emit_element(lines, child, '\t\t\t', in_cmd_bar=True) - lines.append('\t\t</ChildItems>') - lines.append('\t</AutoCommandBar>') - else: - lines.append(f'\t<AutoCommandBar name="{acb_name}" id="-1"/>') - - # Events - if defn.get('events'): - for evt_name in defn['events']: - if evt_name not in KNOWN_FORM_EVENTS: - print(f"[WARN] Unknown form event '{evt_name}'. Known: {', '.join(KNOWN_FORM_EVENTS)}") - lines.append('\t<Events>') - for evt_name, evt_handler in defn['events'].items(): - lines.append(f'\t\t<Event name="{evt_name}">{evt_handler}</Event>') - lines.append('\t</Events>') - - # ChildItems (elements) - if defn.get('elements') and len(defn['elements']) > 0: - lines.append('\t<ChildItems>') - for el in defn['elements']: - emit_element(lines, el, '\t\t') - lines.append('\t</ChildItems>') - - # Attributes - emit_attributes(lines, defn.get('attributes'), '\t', conditional_appearance=defn.get('conditionalAppearance')) - - # Parameters - emit_parameters(lines, defn.get('parameters'), '\t') - - # Commands - emit_commands(lines, defn.get('commands'), '\t') - - # CommandInterface (командный интерфейс формы — последний дочерний Form) - emit_command_interface(lines, defn.get('commandInterface'), '\t') - - # Close - lines.append('</Form>') - - # --- 3. Write output --- - out_path = args.OutputPath - if not os.path.isabs(out_path): - out_path = os.path.join(os.getcwd(), out_path) - out_dir = os.path.dirname(out_path) - if out_dir and not os.path.exists(out_dir): - os.makedirs(out_dir, exist_ok=True) - - content = '\n'.join(lines) + '\n' - write_utf8_bom(out_path, content) - - # --- 4. Auto-register form in parent object XML --- - # Infer parent from OutputPath: .../TypePlural/ObjectName/Forms/FormName/Ext/Form.xml - form_xml_dir = os.path.dirname(out_path) # Ext - form_name_dir = os.path.dirname(form_xml_dir) # FormName - forms_dir = os.path.dirname(form_name_dir) # Forms - object_dir = os.path.dirname(forms_dir) # ObjectName - type_plural_dir = os.path.dirname(object_dir) # TypePlural - - form_name = os.path.basename(form_name_dir) - object_name = os.path.basename(object_dir) - forms_leaf = os.path.basename(forms_dir) - - if forms_leaf == 'Forms': - object_xml_path = os.path.join(type_plural_dir, f'{object_name}.xml') - if os.path.exists(object_xml_path): - with open(object_xml_path, 'r', encoding='utf-8-sig') as f: - raw_text = f.read() - - # Check if already registered - if f'<Form>{form_name}</Form>' not in raw_text: - # Insert before </ChildObjects> - if '</ChildObjects>' in raw_text: - insert_line = f'\t\t\t<Form>{form_name}</Form>\n' - raw_text = raw_text.replace('</ChildObjects>', insert_line + '\t\t</ChildObjects>', 1) - elif '<ChildObjects/>' in raw_text: - replacement = f'<ChildObjects>\n\t\t\t<Form>{form_name}</Form>\n\t\t</ChildObjects>' - raw_text = raw_text.replace('<ChildObjects/>', replacement, 1) - - write_utf8_bom(object_xml_path, raw_text) - print(f" Registered: <Form>{form_name}</Form> in {object_name}.xml") - - # --- 5. Summary --- - el_count = _next_id - print(f"[OK] Compiled: {args.OutputPath}") - print(f" Elements+IDs: {el_count}") - if defn.get('attributes'): - print(f" Attributes: {len(defn['attributes'])}") - if defn.get('commands'): - print(f" Commands: {len(defn['commands'])}") - if defn.get('parameters'): - print(f" Parameters: {len(defn['parameters'])}") - - -if __name__ == '__main__': - main() +#!/usr/bin/env python3 +# form-compile v1.152 — 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 + + # (ChoiceButton у ref-полей платформа выводит сама; компилятор эмитит true по StartChoice-эвристике. + # Явный choiceButton из декомпиляции эмитится verbatim. Дефолт-«true» здесь НЕ ставим, чтобы + # from-object вывод совпадал с сертифицированным и не плодил ChoiceButton на каждом ref-поле.) + + # Extra props + if extra_props: + for k in extra_props: + el[k] = extra_props[k] + + return el + + +# --- Catalog DSL generators --- + +def generate_catalog_dsl(meta, preset_data, purpose): + purpose_key = f"catalog.{purpose.lower()}" + p = preset_data.get(purpose_key, {}) + fd = p.get('fieldDefaults', {}) + + dispatch = { + 'Folder': lambda: generate_catalog_folder_dsl(meta, p), + 'List': lambda: generate_catalog_list_dsl(meta, p), + 'Choice': lambda: generate_catalog_choice_dsl(meta, p, preset_data), + 'Item': lambda: generate_catalog_item_dsl(meta, p, fd), + } + return dispatch[purpose]() + + +def generate_catalog_folder_dsl(meta, p): + elements = [] + # Code (if CodeLength > 0) + if meta.get('CodeLength', 0) > 0: + elements.append(OrderedDict([('input', '\u041a\u043e\u0434'), ('path', '\u041e\u0431\u044a\u0435\u043a\u0442.Code')])) + # Description + elements.append(OrderedDict([('input', '\u041d\u0430\u0438\u043c\u0435\u043d\u043e\u0432\u0430\u043d\u0438\u0435'), ('path', '\u041e\u0431\u044a\u0435\u043a\u0442.Description')])) + # Parent + parent_title = p.get('parent', {}).get('title') + parent_el = OrderedDict([('input', '\u0420\u043e\u0434\u0438\u0442\u0435\u043b\u044c'), ('path', '\u041e\u0431\u044a\u0435\u043a\u0442.Parent')]) + if parent_title: + parent_el['title'] = parent_title + elements.append(parent_el) + + props = OrderedDict([('windowOpeningMode', 'LockOwnerWindow')]) + if p.get('properties'): + for k in p['properties']: + props[k] = p['properties'][k] + + form_props = OrderedDict([('useForFoldersAndItems', 'Folders')]) + for k in props: + form_props[k] = props[k] + + return OrderedDict([ + ('title', meta['Synonym']), + ('properties', form_props), + ('elements', elements), + ('attributes', [ + OrderedDict([('name', '\u041e\u0431\u044a\u0435\u043a\u0442'), ('type', f"CatalogObject.{meta['Name']}"), ('main', True)]) + ]), + ]) + + +def generate_catalog_list_dsl(meta, p): + columns = [] + # Description always first + columns.append(OrderedDict([('labelField', '\u041d\u0430\u0438\u043c\u0435\u043d\u043e\u0432\u0430\u043d\u0438\u0435'), ('path', '\u0421\u043f\u0438\u0441\u043e\u043a.Description')])) + # Code if present + if meta.get('CodeLength', 0) > 0: + columns.append(OrderedDict([('labelField', '\u041a\u043e\u0434'), ('path', '\u0421\u043f\u0438\u0441\u043e\u043a.Code')])) + # Custom attributes + for attr in meta['Attributes']: + if not is_displayable_type(attr['Type']): + continue + columns.append(OrderedDict([('labelField', attr['Name']), ('path', f"\u0421\u043f\u0438\u0441\u043e\u043a.{attr['Name']}")])) + # Hidden ref + if p.get('hiddenRef', True) is not False: + columns.append(OrderedDict([('labelField', '\u0421\u0441\u044b\u043b\u043a\u0430'), ('path', '\u0421\u043f\u0438\u0441\u043e\u043a.Ref'), ('userVisible', False)])) + + table_el = OrderedDict([ + ('table', '\u0421\u043f\u0438\u0441\u043e\u043a'), ('path', '\u0421\u043f\u0438\u0441\u043e\u043a'), + ('rowPictureDataPath', '\u0421\u043f\u0438\u0441\u043e\u043a.DefaultPicture'), + ('commandBarLocation', 'None'), + ('tableAutofill', False), + ('columns', columns), + ]) + # Hierarchical properties + if meta.get('Hierarchical'): + table_el['initialTreeView'] = 'ExpandTopLevel' + table_el['enableStartDrag'] = True + table_el['enableDrag'] = True + + form_props = OrderedDict() + if p.get('properties'): + for k in p['properties']: + form_props[k] = p['properties'][k] + + return OrderedDict([ + ('title', meta['Synonym']), + ('properties', form_props), + ('elements', [table_el]), + ('attributes', [ + OrderedDict([ + ('name', '\u0421\u043f\u0438\u0441\u043e\u043a'), ('type', 'DynamicList'), ('main', True), + ('settings', OrderedDict([('mainTable', f"Catalog.{meta['Name']}"), ('dynamicDataRead', True)])), + ]) + ]), + ]) + + +def generate_catalog_choice_dsl(meta, p, preset_data): + # Start from list + list_key = 'catalog.list' + lp = preset_data.get(list_key, {}) + dsl = generate_catalog_list_dsl(meta, lp) + + # Add choice-specific properties + dsl['properties']['windowOpeningMode'] = 'LockOwnerWindow' + if p.get('properties'): + for k in p['properties']: + dsl['properties'][k] = p['properties'][k] + + # Set ChoiceMode on table + dsl['elements'][0]['choiceMode'] = True + + return dsl + + +def generate_catalog_item_dsl(meta, p, fd): + header_children = [] + + # Owner (if subordinate) + if meta.get('Owners') and len(meta['Owners']) > 0: + owner_el = OrderedDict([('input', '\u0412\u043b\u0430\u0434\u0435\u043b\u0435\u0446'), ('path', '\u041e\u0431\u044a\u0435\u043a\u0442.Owner'), ('readOnly', True)]) + header_children.append(owner_el) + + # Code + Description + cd_layout = (p.get('codeDescription') or {}).get('layout', 'horizontal') + cd_order = (p.get('codeDescription') or {}).get('order', 'descriptionFirst') + has_code = meta.get('CodeLength', 0) > 0 + + if cd_layout == 'horizontal' and has_code: + cd_children = [] + desc_el = OrderedDict([('input', '\u041d\u0430\u0438\u043c\u0435\u043d\u043e\u0432\u0430\u043d\u0438\u0435'), ('path', '\u041e\u0431\u044a\u0435\u043a\u0442.Description')]) + code_el = OrderedDict([('input', '\u041a\u043e\u0434'), ('path', '\u041e\u0431\u044a\u0435\u043a\u0442.Code')]) + if cd_order == 'descriptionFirst': + cd_children = [desc_el, code_el] + else: + cd_children = [code_el, desc_el] + header_children.append(OrderedDict([ + ('group', 'horizontal'), ('name', '\u0413\u0440\u0443\u043f\u043f\u0430\u041a\u043e\u0434\u041d\u0430\u0438\u043c\u0435\u043d\u043e\u0432\u0430\u043d\u0438\u0435'), ('showTitle', False), + ('representation', 'none'), ('children', cd_children), + ])) + else: + # Vertical or no code + header_children.append(OrderedDict([('input', '\u041d\u0430\u0438\u043c\u0435\u043d\u043e\u0432\u0430\u043d\u0438\u0435'), ('path', '\u041e\u0431\u044a\u0435\u043a\u0442.Description')])) + if has_code: + header_children.append(OrderedDict([('input', '\u041a\u043e\u0434'), ('path', '\u041e\u0431\u044a\u0435\u043a\u0442.Code')])) + + # Parent (for hierarchical catalogs) + parent_pos = (p.get('parent') or {}).get('position', 'afterCodeDescription') + parent_title = (p.get('parent') or {}).get('title') + if meta.get('Hierarchical'): + parent_el = OrderedDict([('input', '\u0420\u043e\u0434\u0438\u0442\u0435\u043b\u044c'), ('path', '\u041e\u0431\u044a\u0435\u043a\u0442.Parent')]) + if parent_title: + parent_el['title'] = parent_title + if parent_pos == 'beforeCodeDescription': + insert_idx = 1 if (meta.get('Owners') and len(meta['Owners']) > 0) else 0 + header_children.insert(insert_idx, parent_el) + else: + # afterCodeDescription (default) + header_children.append(parent_el) + + # Custom attributes -> header + footer_field_names = (p.get('footer') or {}).get('fields', []) + + for attr in meta['Attributes']: + if attr['Name'] in footer_field_names: + continue + if not is_displayable_type(attr['Type']): + continue + header_children.append(new_field_element(attr['Name'], f"\u041e\u0431\u044a\u0435\u043a\u0442.{attr['Name']}", attr['Type'], fd)) + + # Build root elements + root_elements = [] + + # ГруппаШапка + root_elements.append(OrderedDict([ + ('group', 'vertical'), ('name', '\u0413\u0440\u0443\u043f\u043f\u0430\u0428\u0430\u043f\u043a\u0430'), ('showTitle', False), + ('representation', 'none'), ('children', header_children), + ])) + + # Tabular sections + ts_exclude = ['\u0414\u043e\u043f\u043e\u043b\u043d\u0438\u0442\u0435\u043b\u044c\u043d\u044b\u0435\u0420\u0435\u043a\u0432\u0438\u0437\u0438\u0442\u044b', '\u041f\u0440\u0435\u0434\u0441\u0442\u0430\u0432\u043b\u0435\u043d\u0438\u044f'] + if (p.get('tabularSections') or {}).get('exclude'): + ts_exclude = p['tabularSections']['exclude'] + ts_line_number = (p.get('tabularSections') or {}).get('lineNumber', True) + + visible_ts = [ts for ts in meta['TabularSections'] if ts['Name'] not in ts_exclude] + + for ts in visible_ts: + ts_cols = [] + if ts_line_number: + ts_cols.append(OrderedDict([('labelField', f"{ts['Name']}\u041d\u043e\u043c\u0435\u0440\u0421\u0442\u0440\u043e\u043a\u0438"), ('path', f"\u041e\u0431\u044a\u0435\u043a\u0442.{ts['Name']}.LineNumber")])) + for col in ts['Columns']: + ts_cols.append(new_field_element(f"{ts['Name']}{col['Name']}", f"\u041e\u0431\u044a\u0435\u043a\u0442.{ts['Name']}.{col['Name']}", col['Type'], fd)) + root_elements.append(OrderedDict([('table', ts['Name']), ('path', f"\u041e\u0431\u044a\u0435\u043a\u0442.{ts['Name']}"), ('columns', ts_cols)])) + + # Footer fields + for fn in footer_field_names: + f_attr = next((a for a in meta['Attributes'] if a['Name'] == fn), None) + if f_attr: + root_elements.append(new_field_element(f_attr['Name'], f"\u041e\u0431\u044a\u0435\u043a\u0442.{f_attr['Name']}", f_attr['Type'], fd)) + + # BSP group + bsp_group = (p.get('additional') or {}).get('bspGroup', True) + if bsp_group: + root_elements.append(OrderedDict([('group', 'vertical'), ('name', '\u0413\u0440\u0443\u043f\u043f\u0430\u0414\u043e\u043f\u043e\u043b\u043d\u0438\u0442\u0435\u043b\u044c\u043d\u044b\u0435\u0420\u0435\u043a\u0432\u0438\u0437\u0438\u0442\u044b')])) + + # Properties + form_props = OrderedDict() + if p.get('properties'): + for k in p['properties']: + form_props[k] = p['properties'][k] + # UseForFoldersAndItems + if meta.get('Hierarchical') and meta.get('HierarchyType') == 'HierarchyFoldersAndItems': + form_props['useForFoldersAndItems'] = 'Items' + + return OrderedDict([ + ('title', meta['Synonym']), + ('properties', form_props), + ('elements', root_elements), + ('attributes', [ + OrderedDict([('name', '\u041e\u0431\u044a\u0435\u043a\u0442'), ('type', f"CatalogObject.{meta['Name']}"), ('main', True)]) + ]), + ]) + + +# --- Document DSL generators --- + +def generate_document_dsl(meta, preset_data, purpose): + purpose_key = f"document.{purpose.lower()}" + p = preset_data.get(purpose_key, {}) + fd = p.get('fieldDefaults', {}) + + dispatch = { + 'List': lambda: generate_document_list_dsl(meta, p), + 'Choice': lambda: generate_document_choice_dsl(meta, p, preset_data), + 'Item': lambda: generate_document_item_dsl(meta, p, fd), + } + return dispatch[purpose]() + + +def generate_document_list_dsl(meta, p): + columns = [] + # Standard columns: Number + Date + columns.append(OrderedDict([('labelField', 'Номер'), ('path', 'Список.Number')])) + columns.append(OrderedDict([('labelField', 'Дата'), ('path', 'Список.Date')])) + # All custom attributes as labelField + for attr in meta['Attributes']: + if not is_displayable_type(attr['Type']): + continue + columns.append(OrderedDict([('labelField', attr['Name']), ('path', f"\u0421\u043f\u0438\u0441\u043e\u043a.{attr['Name']}")])) + # Hidden ref + if p.get('hiddenRef', True): + columns.append(OrderedDict([('labelField', '\u0421\u0441\u044b\u043b\u043a\u0430'), ('path', '\u0421\u043f\u0438\u0441\u043e\u043a.Ref'), ('userVisible', False)])) + + table_el = OrderedDict([ + ('table', '\u0421\u043f\u0438\u0441\u043e\u043a'), ('path', '\u0421\u043f\u0438\u0441\u043e\u043a'), + ('rowPictureDataPath', '\u0421\u043f\u0438\u0441\u043e\u043a.DefaultPicture'), + ('commandBarLocation', 'None'), + ('tableAutofill', False), + ('columns', columns), + ]) + + form_props = OrderedDict() + if p.get('properties'): + for k in p['properties']: + form_props[k] = p['properties'][k] + + return OrderedDict([ + ('title', meta['Synonym']), + ('properties', form_props), + ('elements', [table_el]), + ('attributes', [ + OrderedDict([ + ('name', '\u0421\u043f\u0438\u0441\u043e\u043a'), ('type', 'DynamicList'), ('main', True), + ('settings', OrderedDict([('mainTable', f"Document.{meta['Name']}"), ('dynamicDataRead', True)])), + ]) + ]), + ]) + + +def generate_document_choice_dsl(meta, p, preset_data): + list_key = 'document.list' + lp = preset_data.get(list_key, {}) + dsl = generate_document_list_dsl(meta, lp) + + dsl['properties']['windowOpeningMode'] = 'LockOwnerWindow' + if p.get('properties'): + for k in p['properties']: + dsl['properties'][k] = p['properties'][k] + + return dsl + + +def generate_document_item_dsl(meta, p, fd): + header_pos = (p.get('header') or {}).get('position', 'insidePage') + header_layout = (p.get('header') or {}).get('layout', '2col') + header_distribute = (p.get('header') or {}).get('distribute', 'even') + date_title = (p.get('header') or {}).get('dateTitle', '\u043e\u0442') + + footer_fields = (p.get('footer') or {}).get('fields', []) + footer_pos = (p.get('footer') or {}).get('position', 'insidePage') + + add_pos = (p.get('additional') or {}).get('position', 'page') + add_layout = (p.get('additional') or {}).get('layout', '2col') + add_bsp_group = (p.get('additional') or {}).get('bspGroup', True) + add_left = (p.get('additional') or {}).get('left', []) + add_right = (p.get('additional') or {}).get('right', []) + + header_right = (p.get('header') or {}).get('right', []) + + ts_exclude = ['\u0414\u043e\u043f\u043e\u043b\u043d\u0438\u0442\u0435\u043b\u044c\u043d\u044b\u0435\u0420\u0435\u043a\u0432\u0438\u0437\u0438\u0442\u044b'] + if (p.get('tabularSections') or {}).get('exclude'): + ts_exclude = p['tabularSections']['exclude'] + ts_line_number = (p.get('tabularSections') or {}).get('lineNumber', True) + + # Classify attributes + claimed = {} + for fn in footer_fields: + claimed[fn] = 'footer' + for fn in header_right: + claimed[fn] = 'header.right' + for fn in add_left: + claimed[fn] = 'additional.left' + for fn in add_right: + claimed[fn] = 'additional.right' + + unclaimed = [attr for attr in meta['Attributes'] if attr['Name'] not in claimed and is_displayable_type(attr['Type'])] + + # Distribute unclaimed + left_attrs = [] + right_extra_attrs = [] + if header_distribute == 'left': + left_attrs = unclaimed + elif header_distribute == 'right': + right_extra_attrs = unclaimed + else: # "even" + import math + half = math.ceil(len(unclaimed) / 2) if unclaimed else 0 + left_attrs = unclaimed[:half] + right_extra_attrs = unclaimed[half:] + + # Build ГруппаНомерДата + num_date_children = [ + OrderedDict([('input', '\u041d\u043e\u043c\u0435\u0440'), ('path', '\u041e\u0431\u044a\u0435\u043a\u0442.Number'), ('autoMaxWidth', False), ('width', 9)]), + OrderedDict([('input', '\u0414\u0430\u0442\u0430'), ('path', '\u041e\u0431\u044a\u0435\u043a\u0442.Date'), ('title', date_title)]), + ] + num_date_group = OrderedDict([ + ('group', 'horizontal'), ('name', '\u0413\u0440\u0443\u043f\u043f\u0430\u041d\u043e\u043c\u0435\u0440\u0414\u0430\u0442\u0430'), ('showTitle', False), ('children', num_date_children), + ]) + + # Build left column + left_children = [num_date_group] + for attr in left_attrs: + left_children.append(new_field_element(attr['Name'], f"\u041e\u0431\u044a\u0435\u043a\u0442.{attr['Name']}", attr['Type'], fd)) + + # Build right column + right_children = [] + for rn in header_right: + r_attr = next((a for a in meta['Attributes'] if a['Name'] == rn), None) + if r_attr: + right_children.append(new_field_element(r_attr['Name'], f"\u041e\u0431\u044a\u0435\u043a\u0442.{r_attr['Name']}", r_attr['Type'], fd)) + for attr in right_extra_attrs: + right_children.append(new_field_element(attr['Name'], f"\u041e\u0431\u044a\u0435\u043a\u0442.{attr['Name']}", attr['Type'], fd)) + + # Header group + if header_layout == '2col' and len(right_children) > 0: + header_group = OrderedDict([ + ('group', 'horizontal'), ('name', '\u0413\u0440\u0443\u043f\u043f\u0430\u0428\u0430\u043f\u043a\u0430'), ('showTitle', False), ('representation', 'none'), + ('children', [ + OrderedDict([('group', 'vertical'), ('name', '\u0413\u0440\u0443\u043f\u043f\u0430\u0428\u0430\u043f\u043a\u0430\u041b\u0435\u0432\u043e'), ('showTitle', False), ('children', left_children)]), + OrderedDict([('group', 'vertical'), ('name', '\u0413\u0440\u0443\u043f\u043f\u0430\u0428\u0430\u043f\u043a\u0430\u041f\u0440\u0430\u0432\u043e'), ('showTitle', False), ('children', right_children)]), + ]), + ]) + else: + # 1col or no right items + all_header_fields = left_children + right_children + header_group = OrderedDict([ + ('group', 'horizontal'), ('name', '\u0413\u0440\u0443\u043f\u043f\u0430\u0428\u0430\u043f\u043a\u0430'), ('showTitle', False), ('representation', 'none'), + ('children', [ + OrderedDict([('group', 'vertical'), ('name', '\u0413\u0440\u0443\u043f\u043f\u0430\u0428\u0430\u043f\u043a\u0430\u041b\u0435\u0432\u043e'), ('showTitle', False), ('children', all_header_fields)]), + ]), + ]) + + # Footer elements + footer_elements = [] + for fn in footer_fields: + f_attr = next((a for a in meta['Attributes'] if a['Name'] == fn), None) + if f_attr: + footer_elements.append(new_field_element(f_attr['Name'], f"\u041e\u0431\u044a\u0435\u043a\u0442.{f_attr['Name']}", f_attr['Type'], fd)) + + # Visible tabular sections + visible_ts = [ts for ts in meta['TabularSections'] if ts['Name'] not in ts_exclude] + + # Additional page content + additional_page = None + if add_pos == 'page': + add_left_els = [] + add_right_els = [] + for aln in add_left: + al_attr = next((a for a in meta['Attributes'] if a['Name'] == aln), None) + if al_attr: + add_left_els.append(new_field_element(al_attr['Name'], f"\u041e\u0431\u044a\u0435\u043a\u0442.{al_attr['Name']}", al_attr['Type'], fd)) + for arn in add_right: + ar_attr = next((a for a in meta['Attributes'] if a['Name'] == arn), None) + if ar_attr: + add_right_els.append(new_field_element(ar_attr['Name'], f"\u041e\u0431\u044a\u0435\u043a\u0442.{ar_attr['Name']}", ar_attr['Type'], fd)) + add_page_children = [] + if add_layout == '2col': + add_page_children.append(OrderedDict([ + ('group', 'horizontal'), ('name', '\u0413\u0440\u0443\u043f\u043f\u0430\u041f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b'), ('showTitle', False), + ('children', [ + OrderedDict([('group', 'vertical'), ('name', '\u0413\u0440\u0443\u043f\u043f\u0430\u041f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b\u041b\u0435\u0432\u043e'), ('showTitle', False), ('children', add_left_els)]), + OrderedDict([('group', 'vertical'), ('name', '\u0413\u0440\u0443\u043f\u043f\u0430\u041f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b\u041f\u0440\u0430\u0432\u043e'), ('showTitle', False), ('children', add_right_els)]), + ]), + ])) + else: + add_page_children.extend(add_left_els + add_right_els) + if add_bsp_group: + add_page_children.append(OrderedDict([('group', 'vertical'), ('name', '\u0413\u0440\u0443\u043f\u043f\u0430\u0414\u043e\u043f\u043e\u043b\u043d\u0438\u0442\u0435\u043b\u044c\u043d\u044b\u0435\u0420\u0435\u043a\u0432\u0438\u0437\u0438\u0442\u044b')])) + additional_page = OrderedDict([('page', '\u0413\u0440\u0443\u043f\u043f\u0430\u0414\u043e\u043f\u043e\u043b\u043d\u0438\u0442\u0435\u043b\u044c\u043d\u043e'), ('title', '\u0414\u043e\u043f\u043e\u043b\u043d\u0438\u0442\u0435\u043b\u044c\u043d\u043e'), ('children', add_page_children)]) + + # Build TS page elements + ts_pages = [] + for ts in visible_ts: + ts_cols = [] + if ts_line_number: + ts_cols.append(OrderedDict([('labelField', f"{ts['Name']}\u041d\u043e\u043c\u0435\u0440\u0421\u0442\u0440\u043e\u043a\u0438"), ('path', f"\u041e\u0431\u044a\u0435\u043a\u0442.{ts['Name']}.LineNumber")])) + for col in ts['Columns']: + ts_cols.append(new_field_element(f"{ts['Name']}{col['Name']}", f"\u041e\u0431\u044a\u0435\u043a\u0442.{ts['Name']}.{col['Name']}", col['Type'], fd)) + ts_pages.append(OrderedDict([ + ('page', f"\u0413\u0440\u0443\u043f\u043f\u0430{ts['Name']}"), ('title', ts['Synonym']), + ('children', [ + OrderedDict([('table', ts['Name']), ('path', f"\u041e\u0431\u044a\u0435\u043a\u0442.{ts['Name']}"), ('columns', ts_cols)]) + ]), + ])) + + # Assemble root elements + root_elements = [] + + if len(visible_ts) == 0: + # Simple form - no Pages + root_elements.append(header_group) + if footer_elements: + root_elements.extend(footer_elements) + if add_bsp_group and add_pos != 'none': + root_elements.append(OrderedDict([('group', 'vertical'), ('name', '\u0413\u0440\u0443\u043f\u043f\u0430\u0414\u043e\u043f\u043e\u043b\u043d\u0438\u0442\u0435\u043b\u044c\u043d\u044b\u0435\u0420\u0435\u043a\u0432\u0438\u0437\u0438\u0442\u044b')])) + else: + # Pages form + if header_pos == 'abovePages': + root_elements.append(header_group) + pages_children = list(ts_pages) + if additional_page: + pages_children.append(additional_page) + root_elements.append(OrderedDict([('pages', '\u0413\u0440\u0443\u043f\u043f\u0430\u0421\u0442\u0440\u0430\u043d\u0438\u0446\u044b'), ('children', pages_children)])) + else: + # insidePage (default) + osnovnoe_children = [header_group] + if footer_pos == 'insidePage' and footer_elements: + osnovnoe_children.extend(footer_elements) + pages_children = [] + pages_children.append(OrderedDict([('page', '\u0413\u0440\u0443\u043f\u043f\u0430\u041e\u0441\u043d\u043e\u0432\u043d\u043e\u0435'), ('title', '\u041e\u0441\u043d\u043e\u0432\u043d\u043e\u0435'), ('children', osnovnoe_children)])) + pages_children.extend(ts_pages) + if additional_page: + pages_children.append(additional_page) + root_elements.append(OrderedDict([('pages', '\u0413\u0440\u0443\u043f\u043f\u0430\u0421\u0442\u0440\u0430\u043d\u0438\u0446\u044b'), ('children', pages_children)])) + + # Footer below pages + if footer_pos == 'belowPages' and footer_elements: + root_elements.extend(footer_elements) + + # Properties + form_props = OrderedDict([('autoTitle', False)]) + if p.get('properties'): + for k in p['properties']: + form_props[k] = p['properties'][k] + + return OrderedDict([ + ('title', meta['Synonym']), + ('properties', form_props), + ('elements', root_elements), + ('attributes', [ + OrderedDict([('name', '\u041e\u0431\u044a\u0435\u043a\u0442'), ('type', f"DocumentObject.{meta['Name']}"), ('main', True)]) + ]), + ]) + + +# --- InformationRegister DSL generators --- + +def generate_information_register_dsl(meta, preset_data, purpose): + p_key = f"informationRegister.{purpose.lower()}" + p = preset_data.get(p_key, {}) + fd = p.get('fieldDefaults') or {'ref': {'choiceButton': True}, 'boolean': {'element': 'check'}} + dispatch = { + 'Record': lambda: generate_information_register_record_dsl(meta, p, fd), + 'List': lambda: generate_information_register_list_dsl(meta, p), + } + return dispatch[purpose]() + + +def generate_information_register_record_dsl(meta, p, fd): + elements = OrderedDict() + is_periodic = meta.get('Periodicity') and meta['Periodicity'] != 'Nonperiodical' + + # Period first (if periodic) + if is_periodic: + elements['\u041f\u0435\u0440\u0438\u043e\u0434'] = {'element': 'input', 'path': '\u0417\u0430\u043f\u0438\u0441\u044c.Period'} + # Dimensions + for dim in meta.get('Dimensions', []): + if not is_displayable_type(dim['Type']): + continue + elements[dim['Name']] = new_field_element(dim['Name'], f"\u0417\u0430\u043f\u0438\u0441\u044c.{dim['Name']}", dim['Type'], fd) + # Resources + for res in meta.get('Resources', []): + if not is_displayable_type(res['Type']): + continue + elements[res['Name']] = new_field_element(res['Name'], f"\u0417\u0430\u043f\u0438\u0441\u044c.{res['Name']}", res['Type'], fd) + # Attributes + for attr in meta['Attributes']: + if not is_displayable_type(attr['Type']): + continue + elements[attr['Name']] = new_field_element(attr['Name'], f"\u0417\u0430\u043f\u0438\u0441\u044c.{attr['Name']}", attr['Type'], fd) + + props = OrderedDict([('windowOpeningMode', 'LockOwnerWindow')]) + if p.get('properties'): + for k in p['properties']: + props[k] = p['properties'][k] + + return OrderedDict([ + ('title', meta['Synonym']), + ('properties', props), + ('elements', elements), + ('attributes', [ + {'name': '\u0417\u0430\u043f\u0438\u0441\u044c', 'type': f"InformationRegisterRecordManager.{meta['Name']}", 'main': True, 'savedData': True} + ]), + ]) + + +def generate_information_register_list_dsl(meta, p): + is_periodic = meta.get('Periodicity') and meta['Periodicity'] != 'Nonperiodical' + is_recorder_subordinate = meta.get('WriteMode') == 'RecorderSubordinate' + + columns_list = [] + # Period + if is_periodic: + columns_list.append(OrderedDict([('labelField', '\u041f\u0435\u0440\u0438\u043e\u0434'), ('path', '\u0421\u043f\u0438\u0441\u043e\u043a.Period')])) + # Recorder/LineNumber for subordinate registers + if is_recorder_subordinate: + columns_list.append(OrderedDict([('labelField', '\u0420\u0435\u0433\u0438\u0441\u0442\u0440\u0430\u0442\u043e\u0440'), ('path', '\u0421\u043f\u0438\u0441\u043e\u043a.Recorder')])) + columns_list.append(OrderedDict([('labelField', '\u041d\u043e\u043c\u0435\u0440\u0421\u0442\u0440\u043e\u043a\u0438'), ('path', '\u0421\u043f\u0438\u0441\u043e\u043a.LineNumber')])) + # Dimensions + for dim in meta.get('Dimensions', []): + if not is_displayable_type(dim['Type']): + continue + columns_list.append(OrderedDict([('labelField', dim['Name']), ('path', f"\u0421\u043f\u0438\u0441\u043e\u043a.{dim['Name']}")])) + # Resources + for res in meta.get('Resources', []): + if not is_displayable_type(res['Type']): + continue + el_key = 'check' if re.match(r'^xs:boolean$|^Boolean$', res['Type']) else 'labelField' + columns_list.append(OrderedDict([(el_key, res['Name']), ('path', f"\u0421\u043f\u0438\u0441\u043e\u043a.{res['Name']}")])) + # Attributes + for attr in meta['Attributes']: + if not is_displayable_type(attr['Type']): + continue + el_key = 'check' if re.match(r'^xs:boolean$|^Boolean$', attr['Type']) else 'labelField' + columns_list.append(OrderedDict([(el_key, attr['Name']), ('path', f"\u0421\u043f\u0438\u0441\u043e\u043a.{attr['Name']}")])) + + table_el = OrderedDict([ + ('table', '\u0421\u043f\u0438\u0441\u043e\u043a'), + ('path', '\u0421\u043f\u0438\u0441\u043e\u043a'), + ('rowPictureDataPath', '\u0421\u043f\u0438\u0441\u043e\u043a.DefaultPicture'), + ('commandBarLocation', 'None'), + ('tableAutofill', False), + ('columns', columns_list), + ]) + + props = OrderedDict() + if p.get('properties'): + for k in p['properties']: + props[k] = p['properties'][k] + + return OrderedDict([ + ('title', meta['Synonym']), + ('properties', props), + ('elements', [table_el]), + ('attributes', [ + {'name': '\u0421\u043f\u0438\u0441\u043e\u043a', 'type': 'DynamicList', 'main': True, 'settings': {'mainTable': f"InformationRegister.{meta['Name']}", 'dynamicDataRead': True}} + ]), + ]) + + +# --- AccumulationRegister DSL generators --- + +def generate_accumulation_register_dsl(meta, preset_data, purpose): + p_key = f"accumulationRegister.{purpose.lower()}" + p = preset_data.get(p_key, {}) + dispatch = { + 'List': lambda: generate_accumulation_register_list_dsl(meta, p), + } + return dispatch[purpose]() + + +def generate_accumulation_register_list_dsl(meta, p): + columns_list = [] + # AccumulationRegisters always have Period, Recorder, LineNumber + columns_list.append(OrderedDict([('labelField', '\u041f\u0435\u0440\u0438\u043e\u0434'), ('path', '\u0421\u043f\u0438\u0441\u043e\u043a.Period')])) + columns_list.append(OrderedDict([('labelField', '\u0420\u0435\u0433\u0438\u0441\u0442\u0440\u0430\u0442\u043e\u0440'), ('path', '\u0421\u043f\u0438\u0441\u043e\u043a.Recorder')])) + columns_list.append(OrderedDict([('labelField', '\u041d\u043e\u043c\u0435\u0440\u0421\u0442\u0440\u043e\u043a\u0438'), ('path', '\u0421\u043f\u0438\u0441\u043e\u043a.LineNumber')])) + # Dimensions + for dim in meta.get('Dimensions', []): + if not is_displayable_type(dim['Type']): + continue + columns_list.append(OrderedDict([('labelField', dim['Name']), ('path', f"\u0421\u043f\u0438\u0441\u043e\u043a.{dim['Name']}")])) + # Resources + for res in meta.get('Resources', []): + if not is_displayable_type(res['Type']): + continue + el_key = 'check' if re.match(r'^xs:boolean$|^Boolean$', res['Type']) else 'labelField' + columns_list.append(OrderedDict([(el_key, res['Name']), ('path', f"\u0421\u043f\u0438\u0441\u043e\u043a.{res['Name']}")])) + # Attributes + for attr in meta['Attributes']: + if not is_displayable_type(attr['Type']): + continue + el_key = 'check' if re.match(r'^xs:boolean$|^Boolean$', attr['Type']) else 'labelField' + columns_list.append(OrderedDict([(el_key, attr['Name']), ('path', f"\u0421\u043f\u0438\u0441\u043e\u043a.{attr['Name']}")])) + + table_el = OrderedDict([ + ('table', '\u0421\u043f\u0438\u0441\u043e\u043a'), + ('path', '\u0421\u043f\u0438\u0441\u043e\u043a'), + ('rowPictureDataPath', '\u0421\u043f\u0438\u0441\u043e\u043a.DefaultPicture'), + ('commandBarLocation', 'None'), + ('tableAutofill', False), + ('columns', columns_list), + ]) + + props = OrderedDict() + if p.get('properties'): + for k in p['properties']: + props[k] = p['properties'][k] + + return OrderedDict([ + ('title', meta['Synonym']), + ('properties', props), + ('elements', [table_el]), + ('attributes', [ + {'name': '\u0421\u043f\u0438\u0441\u043e\u043a', 'type': 'DynamicList', 'main': True, 'settings': {'mainTable': f"AccumulationRegister.{meta['Name']}", 'dynamicDataRead': True}} + ]), + ]) + + +# --- ChartOfCharacteristicTypes (delegates to Catalog) --- + +def generate_chart_of_characteristic_types_dsl(meta, preset_data, purpose): + # Delegate to Catalog generators -- meta already has CodeLength, DescriptionLength, etc. + dsl = generate_catalog_dsl(meta, preset_data, purpose) + + # Post-patch: replace Catalog types with ChartOfCharacteristicTypes types + cat_obj_type = f"CatalogObject.{meta['Name']}" + ccoct_obj_type = f"ChartOfCharacteristicTypesObject.{meta['Name']}" + cat_list_type = f"Catalog.{meta['Name']}" + ccoct_list_type = f"ChartOfCharacteristicTypes.{meta['Name']}" + + for a in dsl['attributes']: + if a.get('type') == cat_obj_type: + a['type'] = ccoct_obj_type + if a.get('type') == 'DynamicList' and a.get('settings') and a['settings'].get('mainTable') == cat_list_type: + a['settings']['mainTable'] = ccoct_list_type + + # For Item forms: inject ValueType field after Description/ГруппаКодНаименование + if purpose == 'Item' and dsl.get('elements'): + vt_el = OrderedDict([('input', '\u0422\u0438\u043f\u0417\u043d\u0430\u0447\u0435\u043d\u0438\u044f'), ('path', '\u041e\u0431\u044a\u0435\u043a\u0442.ValueType')]) + els = dsl['elements'] + if isinstance(els, list): + inserted = False + new_els = [] + for el in els: + new_els.append(el) + if not inserted and isinstance(el, dict): + name = el.get('input') or el.get('group') or '' + if name in ('\u041d\u0430\u0438\u043c\u0435\u043d\u043e\u0432\u0430\u043d\u0438\u0435', '\u0413\u0440\u0443\u043f\u043f\u0430\u041a\u043e\u0434\u041d\u0430\u0438\u043c\u0435\u043d\u043e\u0432\u0430\u043d\u0438\u0435'): + new_els.append(vt_el) + inserted = True + if not inserted: + new_els.append(vt_el) + dsl['elements'] = new_els + + return dsl + + +# --- ExchangePlan (delegates to Catalog) --- + +def generate_exchange_plan_dsl(meta, preset_data, purpose): + # ExchangePlans are not hierarchical and have no Folder form + dsl = generate_catalog_dsl(meta, preset_data, purpose) + + # Post-patch: replace Catalog types with ExchangePlan types + cat_obj_type = f"CatalogObject.{meta['Name']}" + ep_obj_type = f"ExchangePlanObject.{meta['Name']}" + cat_list_type = f"Catalog.{meta['Name']}" + ep_list_type = f"ExchangePlan.{meta['Name']}" + + for a in dsl['attributes']: + if a.get('type') == cat_obj_type: + a['type'] = ep_obj_type + if a.get('type') == 'DynamicList' and a.get('settings') and a['settings'].get('mainTable') == cat_list_type: + a['settings']['mainTable'] = ep_list_type + + # For Item forms: inject SentNo, ReceivedNo after Code/Description + if purpose == 'Item' and dsl.get('elements'): + sent_el = OrderedDict([('input', '\u041d\u043e\u043c\u0435\u0440\u041e\u0442\u043f\u0440\u0430\u0432\u043b\u0435\u043d\u043d\u043e\u0433\u043e'), ('path', '\u041e\u0431\u044a\u0435\u043a\u0442.SentNo'), ('readOnly', True)]) + recv_el = OrderedDict([('input', '\u041d\u043e\u043c\u0435\u0440\u041f\u0440\u0438\u043d\u044f\u0442\u043e\u0433\u043e'), ('path', '\u041e\u0431\u044a\u0435\u043a\u0442.ReceivedNo'), ('readOnly', True)]) + els = dsl['elements'] + if isinstance(els, list): + inserted = False + new_els = [] + for el in els: + new_els.append(el) + if not inserted and isinstance(el, dict): + name = el.get('input') or el.get('group') or '' + if name in ('\u041d\u0430\u0438\u043c\u0435\u043d\u043e\u0432\u0430\u043d\u0438\u0435', '\u0413\u0440\u0443\u043f\u043f\u0430\u041a\u043e\u0434\u041d\u0430\u0438\u043c\u0435\u043d\u043e\u0432\u0430\u043d\u0438\u0435'): + new_els.append(sent_el) + new_els.append(recv_el) + inserted = True + if not inserted: + new_els.append(sent_el) + new_els.append(recv_el) + dsl['elements'] = new_els + + return dsl + + +# --- ChartOfAccounts DSL generators --- + +def generate_chart_of_accounts_dsl(meta, preset_data, purpose): + p_key = f"chartOfAccounts.{purpose.lower()}" + p = preset_data.get(p_key, {}) + fd = p.get('fieldDefaults') or {'ref': {'choiceButton': True}, 'boolean': {'element': 'check'}} + dispatch = { + 'Item': lambda: generate_chart_of_accounts_item_dsl(meta, p, fd, preset_data), + 'Folder': lambda: generate_chart_of_accounts_folder_dsl(meta, p), + 'List': lambda: generate_chart_of_accounts_list_dsl(meta, preset_data), + 'Choice': lambda: generate_chart_of_accounts_choice_dsl(meta, preset_data), + } + return dispatch[purpose]() + + +def generate_chart_of_accounts_item_dsl(meta, p, fd, preset_data): + elements = [] + + # Header: Code + Parent + header_left_children = [] + if meta.get('CodeLength', 0) > 0: + header_left_children.append(OrderedDict([('input', '\u041a\u043e\u0434'), ('path', '\u041e\u0431\u044a\u0435\u043a\u0442.Code')])) + header_right_children = [] + if meta.get('Hierarchical'): + parent_title = (p.get('parent') or {}).get('title', '\u041f\u043e\u0434\u0447\u0438\u043d\u0435\u043d \u0441\u0447\u0435\u0442\u0443') + header_right_children.append(OrderedDict([('input', '\u0420\u043e\u0434\u0438\u0442\u0435\u043b\u044c'), ('path', '\u041e\u0431\u044a\u0435\u043a\u0442.Parent'), ('title', parent_title)])) + + if len(header_right_children) > 0: + elements.append(OrderedDict([ + ('group', 'horizontal'), ('name', '\u0413\u0440\u0443\u043f\u043f\u0430\u0428\u0430\u043f\u043a\u0430'), ('showTitle', False), ('representation', 'none'), + ('children', [ + OrderedDict([('group', 'vertical'), ('name', '\u0413\u0440\u0443\u043f\u043f\u0430\u0428\u0430\u043f\u043a\u0430\u041b\u0435\u0432\u043e'), ('showTitle', False), ('children', header_left_children)]), + OrderedDict([('group', 'vertical'), ('name', '\u0413\u0440\u0443\u043f\u043f\u0430\u0428\u0430\u043f\u043a\u0430\u041f\u0440\u0430\u0432\u043e'), ('showTitle', False), ('children', header_right_children)]), + ]), + ])) + elif len(header_left_children) > 0: + elements.extend(header_left_children) + + # Description + if meta.get('DescriptionLength', 0) > 0: + elements.append(OrderedDict([('input', '\u041d\u0430\u0438\u043c\u0435\u043d\u043e\u0432\u0430\u043d\u0438\u0435'), ('path', '\u041e\u0431\u044a\u0435\u043a\u0442.Description')])) + + # OffBalance + elements.append(OrderedDict([('check', '\u0417\u0430\u0431\u0430\u043b\u0430\u043d\u0441\u043e\u0432\u044b\u0439'), ('path', '\u041e\u0431\u044a\u0435\u043a\u0442.OffBalance')])) + + # AccountingFlags as checkboxes + if meta.get('AccountingFlags') and len(meta['AccountingFlags']) > 0: + flag_children = [] + for flag in meta['AccountingFlags']: + flag_children.append(OrderedDict([('check', flag['Name']), ('path', f"\u041e\u0431\u044a\u0435\u043a\u0442.{flag['Name']}")])) + elements.append(OrderedDict([ + ('group', 'vertical'), ('name', '\u0413\u0440\u0443\u043f\u043f\u0430\u041f\u0440\u0438\u0437\u043d\u0430\u043a\u0438\u0423\u0447\u0435\u0442\u0430'), ('title', '\u041f\u0440\u0438\u0437\u043d\u0430\u043a\u0438 \u0443\u0447\u0435\u0442\u0430'), + ('children', flag_children), + ])) + + # ExtDimensionTypes table + if meta.get('MaxExtDimensionCount', 0) > 0: + # Column names are prefixed with the table name (like the generic TS path and stock 1C), + # else a subconto flag column collides with a same-named account accounting-flag checkbox. + ed_table = '\u0412\u0438\u0434\u044b\u0421\u0443\u0431\u043a\u043e\u043d\u0442\u043e' + ed_cols = [] + ed_cols.append(OrderedDict([('input', f"{ed_table}\u0412\u0438\u0434\u0421\u0443\u0431\u043a\u043e\u043d\u0442\u043e"), ('path', '\u041e\u0431\u044a\u0435\u043a\u0442.ExtDimensionTypes.ExtDimensionType')])) + ed_cols.append(OrderedDict([('check', f"{ed_table}\u0422\u043e\u043b\u044c\u043a\u043e\u041e\u0431\u043e\u0440\u043e\u0442\u044b"), ('path', '\u041e\u0431\u044a\u0435\u043a\u0442.ExtDimensionTypes.TurnoversOnly')])) + if meta.get('ExtDimensionAccountingFlags'): + for ed_flag in meta['ExtDimensionAccountingFlags']: + ed_cols.append(OrderedDict([('check', f"{ed_table}{ed_flag['Name']}"), ('path', f"\u041e\u0431\u044a\u0435\u043a\u0442.ExtDimensionTypes.{ed_flag['Name']}")])) + elements.append(OrderedDict([ + ('table', ed_table), + ('path', '\u041e\u0431\u044a\u0435\u043a\u0442.ExtDimensionTypes'), + ('columns', ed_cols), + ])) + + # Custom attributes + for attr in meta['Attributes']: + if not is_displayable_type(attr['Type']): + continue + elements.append(new_field_element(attr['Name'], f"\u041e\u0431\u044a\u0435\u043a\u0442.{attr['Name']}", attr['Type'], fd)) + + # Tabular sections + ts_exclude = ['\u0414\u043e\u043f\u043e\u043b\u043d\u0438\u0442\u0435\u043b\u044c\u043d\u044b\u0435\u0420\u0435\u043a\u0432\u0438\u0437\u0438\u0442\u044b', '\u041f\u0440\u0435\u0434\u0441\u0442\u0430\u0432\u043b\u0435\u043d\u0438\u044f'] + for ts in meta['TabularSections']: + if ts['Name'] in ts_exclude: + continue + ts_cols = [] + for col in ts['Columns']: + if not is_displayable_type(col['Type']): + continue + ts_cols.append(new_field_element(f"{ts['Name']}{col['Name']}", f"\u041e\u0431\u044a\u0435\u043a\u0442.{ts['Name']}.{col['Name']}", col['Type'], fd)) + elements.append(OrderedDict([('table', ts['Name']), ('path', f"\u041e\u0431\u044a\u0435\u043a\u0442.{ts['Name']}"), ('columns', ts_cols)])) + + props = OrderedDict() + if p.get('properties'): + for k in p['properties']: + props[k] = p['properties'][k] + + return OrderedDict([ + ('title', meta['Synonym']), + ('properties', props), + ('elements', elements), + ('attributes', [ + {'name': '\u041e\u0431\u044a\u0435\u043a\u0442', 'type': f"ChartOfAccountsObject.{meta['Name']}", 'main': True, 'savedData': True} + ]), + ]) + + +def generate_chart_of_accounts_folder_dsl(meta, p): + elements = [] + if meta.get('CodeLength', 0) > 0: + elements.append(OrderedDict([('input', '\u041a\u043e\u0434'), ('path', '\u041e\u0431\u044a\u0435\u043a\u0442.Code')])) + if meta.get('DescriptionLength', 0) > 0: + elements.append(OrderedDict([('input', '\u041d\u0430\u0438\u043c\u0435\u043d\u043e\u0432\u0430\u043d\u0438\u0435'), ('path', '\u041e\u0431\u044a\u0435\u043a\u0442.Description')])) + if meta.get('Hierarchical'): + parent_title = (p.get('parent') or {}).get('title', '\u041f\u043e\u0434\u0447\u0438\u043d\u0435\u043d \u0441\u0447\u0435\u0442\u0443') + elements.append(OrderedDict([('input', '\u0420\u043e\u0434\u0438\u0442\u0435\u043b\u044c'), ('path', '\u041e\u0431\u044a\u0435\u043a\u0442.Parent'), ('title', parent_title)])) + + props = OrderedDict([('windowOpeningMode', 'LockOwnerWindow')]) + if p.get('properties'): + for k in p['properties']: + props[k] = p['properties'][k] + + return OrderedDict([ + ('title', meta['Synonym']), + ('useForFoldersAndItems', 'Folders'), + ('properties', props), + ('elements', elements), + ('attributes', [ + {'name': '\u041e\u0431\u044a\u0435\u043a\u0442', 'type': f"ChartOfAccountsObject.{meta['Name']}", 'main': True, 'savedData': True} + ]), + ]) + + +def generate_chart_of_accounts_list_dsl(meta, preset_data): + # Delegate to Catalog List and patch types + dsl = generate_catalog_dsl(meta, preset_data, 'List') + for a in dsl['attributes']: + if a.get('type') == 'DynamicList' and a.get('settings') and a['settings'].get('mainTable') == f"Catalog.{meta['Name']}": + a['settings']['mainTable'] = f"ChartOfAccounts.{meta['Name']}" + return dsl + + +def generate_chart_of_accounts_choice_dsl(meta, preset_data): + dsl = generate_catalog_dsl(meta, preset_data, 'Choice') + for a in dsl['attributes']: + if a.get('type') == 'DynamicList' and a.get('settings') and a['settings'].get('mainTable') == f"Catalog.{meta['Name']}": + a['settings']['mainTable'] = f"ChartOfAccounts.{meta['Name']}" + return dsl + + +# ═══════════════════════════════════════════════════════════════════════════ +# END OF FROM-OBJECT MODE FUNCTIONS +# ═══════════════════════════════════════════════════════════════════════════ + + +def esc_xml(s): + # Экранирование ТЕКСТА элемента (<v8:content>, <Value>): только & < > . + # Кавычки/апострофы в тексте 1С не экранирует (пишет литерально) — " ломал бы раундтрип. + return s.replace('&', '&').replace('<', '<').replace('>', '>') + + +def di_attr(el): + # DisplayImportance — атрибут открывающего тега элемента (адаптивная важность). "" если нет. + if isinstance(el, dict) and el.get('displayImportance'): + return f' DisplayImportance="{esc_xml(str(el["displayImportance"]))}"' + return '' + + +# Базовая директория для @file-ссылок в query динсписка (устанавливается в main) +QUERY_BASE_DIR = None + + +def resolve_query_value(val, base_dir): + if not val.startswith('@'): + return val + file_path = val[1:] + if os.path.isabs(file_path): + candidates = [file_path] + else: + candidates = [os.path.join(base_dir or os.getcwd(), file_path), os.path.join(os.getcwd(), file_path)] + for c in candidates: + if os.path.exists(c): + with open(c, 'r', encoding='utf-8-sig') as f: + return f.read().rstrip() + print(f"Query file not found: {file_path} (searched: {', '.join(candidates)})", file=sys.stderr) + sys.exit(1) + + +def emit_ml_items(lines, indent, val): + # строка → один ru-элемент; объект {lang: text} → по элементу на язык + if isinstance(val, dict): + for k, v in val.items(): + lines.append(f"{indent}<v8:item>") + lines.append(f"{indent}\t<v8:lang>{k}</v8:lang>") + lines.append(f"{indent}\t<v8:content>{esc_xml(str(v))}</v8:content>") + lines.append(f"{indent}</v8:item>") + else: + lines.append(f"{indent}<v8:item>") + lines.append(f"{indent}\t<v8:lang>ru</v8:lang>") + lines.append(f"{indent}\t<v8:content>{esc_xml(str(val))}</v8:content>") + lines.append(f"{indent}</v8:item>") + + +def emit_mltext(lines, indent, tag, text, xsi_type=None): + attr = f' xsi:type="{xsi_type}"' if xsi_type else '' + if not text: + lines.append(f"{indent}<{tag}{attr}/>") + return + lines.append(f"{indent}<{tag}{attr}>") + emit_ml_items(lines, f"{indent}\t", text) + lines.append(f"{indent}</{tag}>") + + +def emit_us_presentation(lines, indent, tag, val): + # <dcsset:userSettingPresentation>: плоская строка → xsi:type="xs:string"; мультиязычный → v8:LocalStringType + if val is None: + return + if isinstance(val, str): + lines.append(f'{indent}<{tag} xsi:type="xs:string">{esc_xml(val)}</{tag}>') + else: + emit_mltext(lines, indent, tag, val, xsi_type='v8:LocalStringType') + + +# Каноничные GUID пустых контейнеров ListSettings (умолчание платформы, ~90% форм). +CANON_FILTER_ID = 'dfcece9d-5077-440b-b6b3-45a5cb4538eb' +CANON_ORDER_ID = '88619765-ccb3-46c6-ac52-38e9c992ebd4' +CANON_CA_ID = 'b75fecce-942b-4aed-abc9-e6a02e460fb3' +CANON_ITEMS_ID = '911b6018-f537-43e8-a417-da56b22f9aec' + + +def new_uuid(): + return str(uuid.uuid4()) + + +# ───────────────────────────────────────────────────────────────────────────── +# Настройки компоновщика ListSettings: filter/order/conditionalAppearance. +# Грамматика DSL и эмиссия dcsset скопированы из skd-compile (навыки автономны). +# ───────────────────────────────────────────────────────────────────────────── +COMPARISON_TYPES = { + '=': 'Equal', '<>': 'NotEqual', + '>': 'Greater', '>=': 'GreaterOrEqual', + '<': 'Less', '<=': 'LessOrEqual', + 'in': 'InList', 'notIn': 'NotInList', + 'inHierarchy': 'InHierarchy', 'inListByHierarchy': 'InListByHierarchy', + 'contains': 'Contains', 'notContains': 'NotContains', + 'beginsWith': 'BeginsWith', 'notBeginsWith': 'NotBeginsWith', + 'like': 'Like', 'notLike': 'NotLike', + 'подобно': 'Like', 'неподобно': 'NotLike', # рус. синоним + 'filled': 'Filled', 'notFilled': 'NotFilled', +} +# Регистронезависимый лукап (зеркало PS-хэша): Like/LIKE/ПОДОБНО → канон +_COMPARISON_TYPES_CI = {k.lower(): v for k, v in COMPARISON_TYPES.items()} + +_REF_TYPE_RE = re.compile( + r'^(Перечисление|Справочник|ПланСчетов|Документ|ПланВидовХарактеристик|ПланВидовРасчета|' + r'БизнесПроцесс|Задача|РегистрСведений|ПланОбмена|Catalog|Enum|Document|ChartOfAccounts|' + r'ChartOfCharacteristicTypes|ChartOfCalculationTypes|BusinessProcess|Task|' + r'InformationRegister|ExchangePlan)\.') + + +def parse_filter_shorthand(s): + result = {'field': '', 'op': 'Equal', 'value': None, 'use': True, + 'userSettingID': None, 'viewMode': None, 'presentation': None} + if re.search(r'@user', s): + result['userSettingID'] = 'auto' + s = re.sub(r'\s*@user', '', s) + if re.search(r'@off', s): + result['use'] = False + s = re.sub(r'\s*@off', '', s) + if re.search(r'@quickAccess', s): + result['viewMode'] = 'QuickAccess' + s = re.sub(r'\s*@quickAccess', '', s) + if re.search(r'@normal', s): + result['viewMode'] = 'Normal' + s = re.sub(r'\s*@normal', '', s) + if re.search(r'@inaccessible', s): + result['viewMode'] = 'Inaccessible' + s = re.sub(r'\s*@inaccessible', '', s) + s = s.strip() + op_patterns = ['<>', '>=', '<=', '=', '>', '<', + r'notIn\b', r'in\b', r'inHierarchy\b', r'inListByHierarchy\b', + r'notContains\b', r'contains\b', r'notBeginsWith\b', r'beginsWith\b', + r'notLike\b', r'like\b', r'неподобно\b', r'подобно\b', + r'notFilled\b', r'filled\b'] + op_joined = '|'.join(op_patterns) + m = re.match(r'^(.+?)\s+(' + op_joined + r')\s*(.*)?$', s, re.IGNORECASE) + if m: + result['field'] = m.group(1).strip() + result['op'] = m.group(2).strip() + val_part = m.group(3).strip() if m.group(3) else '' + if val_part and val_part != '_': + if val_part == 'true' or val_part == 'false': + result['value'] = (val_part == 'true') + result['valueType'] = 'xs:boolean' + elif re.match(r'^\d{4}-\d{2}-\d{2}T', val_part): + # дата без valueType → emit_filter_item выведет StandardBeginningDate Custom (дефолт даты в фильтре) + result['value'] = val_part + elif re.match(r'^\d+(\.\d+)?$', val_part): + result['value'] = val_part + result['valueType'] = 'xs:decimal' + elif re.match(r'^(Перечисление|Справочник|ПланСчетов|Документ|ПланВидовХарактеристик|ПланВидовРасчета)\.', val_part): + result['value'] = val_part + result['valueType'] = 'dcscor:DesignTimeValue' + else: + result['value'] = val_part + result['valueType'] = 'xs:string' + else: + result['field'] = s + return result + + +def _value_type_for(v, explicit=None): + if explicit: + return explicit + if isinstance(v, bool): + return 'xs:boolean' + if isinstance(v, (int, float)): + return 'xs:decimal' + vs = str(v) + if re.match(r'^\d{4}-\d{2}-\d{2}T', vs): + return 'xs:dateTime' + if re.match(r'^-?\d+(\.\d+)?$', vs): + return 'xs:decimal' + if _REF_TYPE_RE.match(vs): + return 'dcscor:DesignTimeValue' + return 'xs:string' + + +# Значение типа v8:Type (напр. тип «Неопределено» = <prefix>:Undefined) ссылается на тип +# платформы из namespace http://v8.1c.ru/8.2/data/types — платформа объявляет его ЛОКАЛЬНО +# на теге значения (префикс авто: d6p1/d8p1/dN…). Без объявления QName битый. +def _value_type_ns_attr(value_type, value): + if value_type == 'v8:Type': + m = re.match(r'^([A-Za-z]\w*):', str(value)) + if m and m.group(1) not in ('xs', 'cfg', 'v8', 'v8ui', 'ent', 'dcscor', 'dcsset', 'dcssch'): + return f' xmlns:{m.group(1)}="http://v8.1c.ru/8.2/data/types"' + return '' + + +def emit_filter_item(lines, item, indent): + if item.get('group'): + g = str(item['group']) + group_type = {'And': 'AndGroup', 'Or': 'OrGroup', 'Not': 'NotGroup'}.get(g, g + 'Group') + lines.append(f'{indent}<dcsset:item xsi:type="dcsset:FilterItemGroup">') + lines.append(f'{indent}\t<dcsset:groupType>{group_type}</dcsset:groupType>') + if item.get('items'): + for sub in item['items']: + if isinstance(sub, str): + parsed = parse_filter_shorthand(sub) + obj = {'field': parsed['field'], 'op': parsed['op']} + if parsed['use'] is False: + obj['use'] = False + if parsed['value'] is not None: + obj['value'] = parsed['value'] + if parsed.get('valueType'): + obj['valueType'] = parsed['valueType'] + if parsed.get('userSettingID'): + obj['userSettingID'] = parsed['userSettingID'] + if parsed.get('viewMode'): + obj['viewMode'] = parsed['viewMode'] + sub = obj + emit_filter_item(lines, sub, f'{indent}\t') + if item.get('presentation'): + emit_us_presentation(lines, f'{indent}\t', 'dcsset:presentation', item['presentation']) + if item.get('viewMode'): + lines.append(f'{indent}\t<dcsset:viewMode>{esc_xml(str(item["viewMode"]))}</dcsset:viewMode>') + if item.get('userSettingID'): + guid = new_uuid() if str(item['userSettingID']) == 'auto' else str(item['userSettingID']) + lines.append(f'{indent}\t<dcsset:userSettingID>{esc_xml(guid)}</dcsset:userSettingID>') + if item.get('userSettingPresentation'): + emit_us_presentation(lines, f'{indent}\t', 'dcsset:userSettingPresentation', item['userSettingPresentation']) + lines.append(f'{indent}</dcsset:item>') + return + + lines.append(f'{indent}<dcsset:item xsi:type="dcsset:FilterItemComparison">') + if item.get('use') is False: + lines.append(f'{indent}\t<dcsset:use>false</dcsset:use>') + lines.append(f'{indent}\t<dcsset:left xsi:type="dcscor:Field">{esc_xml(str(item.get("field", "")))}</dcsset:left>') + # Регистронезависимый лукап (зеркало PS): Like/LIKE/ПОДОБНО → канон; иначе — как есть + comp_type = _COMPARISON_TYPES_CI.get(str(item.get('op')).lower()) + if not comp_type: + comp_type = str(item.get('op')) + lines.append(f'{indent}\t<dcsset:comparisonType>{esc_xml(comp_type)}</dcsset:comparisonType>') + val = item.get('value') + if isinstance(val, list): + if len(val) == 0: + lines.append(f'{indent}\t<dcsset:right xsi:type="v8:ValueListType">') + lines.append(f'{indent}\t\t<v8:valueType/>') + lines.append(f'{indent}\t\t<v8:lastId xsi:type="xs:decimal">-1</v8:lastId>') + lines.append(f'{indent}\t</dcsset:right>') + else: + for v in val: + vt = _value_type_for(v, item.get('valueType')) + v_str = str(v).lower() if isinstance(v, bool) else esc_xml(str(v)) + ns_attr = _value_type_ns_attr(vt, v) + lines.append(f'{indent}\t<dcsset:right{ns_attr} xsi:type="{vt}">{v_str}</dcsset:right>') + elif val is not None and ( + re.search(r'Standard(Beginning|End)Date$', str(item.get('valueType') or '')) or + (not item.get('valueType') and isinstance(val, str) and re.match(r'^\d{4}-\d{2}-\d{2}T', val))): + # Стандартная дата начала/окончания. Формы: объект {variant, date?} (Custom несёт <v8:date>); + # строка-вариант "BeginningOfThisDay" (именованный без даты); голая ISO-дата без valueType — + # шорткат для Custom+date (дата в фильтре почти всегда SBD Custom, корпус 268 vs 2 xs:dateTime). + sd_type = re.sub(r'^v8:', '', str(item['valueType'])) if item.get('valueType') else 'StandardBeginningDate' + if isinstance(val, dict): + variant = str(val.get('variant', '')); date_v = val.get('date') + elif isinstance(val, str) and re.match(r'^\d{4}-\d{2}-\d{2}T', val): + variant = 'Custom'; date_v = val + else: + variant = str(val); date_v = None + lines.append(f'{indent}\t<dcsset:right xsi:type="v8:{sd_type}">') + lines.append(f'{indent}\t\t<v8:variant xsi:type="v8:{sd_type}Variant">{esc_xml(variant)}</v8:variant>') + if date_v is not None: + lines.append(f'{indent}\t\t<v8:date>{esc_xml(str(date_v))}</v8:date>') + lines.append(f'{indent}\t</dcsset:right>') + elif str(val) == '_': + # "_" — маркер пустого значения: платформа эмитит пустой self-closing <dcsset:right> + # (напр. <dcsset:right xsi:type="dcscor:Field"/> — сравнение с незаданным полем). + vt = str(item['valueType']) if item.get('valueType') else 'xs:string' + lines.append(f'{indent}\t<dcsset:right xsi:type="{vt}"/>') + elif val is not None: + vt = _value_type_for(val, item.get('valueType')) + v_str = str(val).lower() if isinstance(val, bool) else esc_xml(str(val)) + ns_attr = _value_type_ns_attr(vt, val) + lines.append(f'{indent}\t<dcsset:right{ns_attr} xsi:type="{vt}">{v_str}</dcsset:right>') + if item.get('presentation'): + emit_us_presentation(lines, f'{indent}\t', 'dcsset:presentation', item['presentation']) + if item.get('viewMode'): + lines.append(f'{indent}\t<dcsset:viewMode>{esc_xml(str(item["viewMode"]))}</dcsset:viewMode>') + if item.get('userSettingID'): + uid = new_uuid() if str(item['userSettingID']) == 'auto' else str(item['userSettingID']) + lines.append(f'{indent}\t<dcsset:userSettingID>{esc_xml(uid)}</dcsset:userSettingID>') + if item.get('userSettingPresentation'): + emit_us_presentation(lines, f'{indent}\t', 'dcsset:userSettingPresentation', item['userSettingPresentation']) + lines.append(f'{indent}</dcsset:item>') + + +def emit_filter(lines, items, indent, block_view_mode=None, block_user_setting_id=None, block_user_setting_presentation=None): + has_items = bool(items) and len(items) > 0 + has_block_meta = (block_view_mode is not None) or (block_user_setting_id is not None) or (block_user_setting_presentation is not None) + if not has_items and not has_block_meta: + return + lines.append(f'{indent}<dcsset:filter>') + for item in (items or []): + if isinstance(item, str): + parsed = parse_filter_shorthand(item) + obj = {'field': parsed['field'], 'op': parsed['op']} + if parsed['use'] is False: + obj['use'] = False + if parsed['value'] is not None: + obj['value'] = parsed['value'] + if parsed.get('valueType'): + obj['valueType'] = parsed['valueType'] + if parsed.get('userSettingID'): + obj['userSettingID'] = parsed['userSettingID'] + if parsed.get('viewMode'): + obj['viewMode'] = parsed['viewMode'] + emit_filter_item(lines, obj, f'{indent}\t') + else: + emit_filter_item(lines, item, f'{indent}\t') + if block_view_mode is not None: + lines.append(f'{indent}\t<dcsset:viewMode>{esc_xml(str(block_view_mode))}</dcsset:viewMode>') + if block_user_setting_id is not None: + uid = new_uuid() if str(block_user_setting_id) == 'auto' else str(block_user_setting_id) + lines.append(f'{indent}\t<dcsset:userSettingID>{esc_xml(uid)}</dcsset:userSettingID>') + if block_user_setting_presentation is not None: + emit_us_presentation(lines, f'{indent}\t', 'dcsset:userSettingPresentation', block_user_setting_presentation) + lines.append(f'{indent}</dcsset:filter>') + + +def emit_order(lines, items, indent, skip_auto=False, block_view_mode=None, block_user_setting_id=None, block_user_setting_presentation=None): + has_items = bool(items) and len(items) > 0 + has_block_meta = (block_view_mode is not None) or (block_user_setting_id is not None) or (block_user_setting_presentation is not None) + if not has_items and not has_block_meta: + return + lines.append(f'{indent}<dcsset:order>') + for item in (items or []): + if isinstance(item, str): + if item == 'Auto': + if not skip_auto: + lines.append(f'{indent}\t<dcsset:item xsi:type="dcsset:OrderItemAuto"/>') + else: + parts = re.split(r'\s+', item) + field = parts[0] + direction = 'Asc' + if len(parts) > 1 and re.match(r'(?i)^(desc|убыв)', parts[1]): + direction = 'Desc' + elif len(parts) > 1 and re.match(r'(?i)^(asc|возр)', parts[1]): + direction = 'Asc' + lines.append(f'{indent}\t<dcsset:item xsi:type="dcsset:OrderItemField">') + lines.append(f'{indent}\t\t<dcsset:field>{esc_xml(field)}</dcsset:field>') + lines.append(f'{indent}\t\t<dcsset:orderType>{direction}</dcsset:orderType>') + lines.append(f'{indent}\t</dcsset:item>') + else: + if item.get('field') == 'Auto' or item.get('type') == 'auto': + if not skip_auto: + lines.append(f'{indent}\t<dcsset:item xsi:type="dcsset:OrderItemAuto"/>') + continue + direction = str(item['direction']) if item.get('direction') else 'Asc' + if re.match(r'(?i)^(desc|убыв)', direction): + direction = 'Desc' + elif re.match(r'(?i)^(asc|возр)', direction): + direction = 'Asc' + lines.append(f'{indent}\t<dcsset:item xsi:type="dcsset:OrderItemField">') + if item.get('use') is False: + lines.append(f'{indent}\t\t<dcsset:use>false</dcsset:use>') + lines.append(f'{indent}\t\t<dcsset:field>{esc_xml(str(item.get("field", "")))}</dcsset:field>') + lines.append(f'{indent}\t\t<dcsset:orderType>{direction}</dcsset:orderType>') + if item.get('viewMode'): + lines.append(f'{indent}\t\t<dcsset:viewMode>{esc_xml(str(item["viewMode"]))}</dcsset:viewMode>') + lines.append(f'{indent}\t</dcsset:item>') + if block_view_mode is not None: + lines.append(f'{indent}\t<dcsset:viewMode>{esc_xml(str(block_view_mode))}</dcsset:viewMode>') + if block_user_setting_id is not None: + uid = new_uuid() if str(block_user_setting_id) == 'auto' else str(block_user_setting_id) + lines.append(f'{indent}\t<dcsset:userSettingID>{esc_xml(uid)}</dcsset:userSettingID>') + if block_user_setting_presentation is not None: + emit_us_presentation(lines, f'{indent}\t', 'dcsset:userSettingPresentation', block_user_setting_presentation) + lines.append(f'{indent}</dcsset:order>') + + +def emit_appearance_value(lines, key, val, indent): + lines.append(f'{indent}<dcscor:item xsi:type="dcsset:SettingsParameterValue">') + + def _has_key(o, k): + return isinstance(o, dict) and (k in o) + + def _get(o, k): + return o.get(k) if isinstance(o, dict) else None + + is_top_level_line = _has_key(val, '@type') and (str(_get(val, '@type')) == 'Line') + use_wrapper = False + inner_val = val + nested_items = None + if is_top_level_line: + if _has_key(val, 'use') and (_get(val, 'use') is False): + use_wrapper = True + if _has_key(val, 'items'): + nested_items = _get(val, 'items') + elif _has_key(val, 'value') and isinstance(val, dict): + inner_val = _get(val, 'value') + if _has_key(val, 'use') and (_get(val, 'use') is False): + use_wrapper = True + if _has_key(val, 'items'): + nested_items = _get(val, 'items') + if use_wrapper: + lines.append(f'{indent}\t<dcscor:use>false</dcscor:use>') + lines.append(f'{indent}\t<dcscor:parameter>{esc_xml(key)}</dcscor:parameter>') + + is_font_dict = isinstance(inner_val, dict) and inner_val.get('@type') is not None and str(inner_val.get('@type')) == 'Font' + is_line_dict = _has_key(inner_val, '@type') and (str(_get(inner_val, '@type')) == 'Line') + is_dict = isinstance(inner_val, dict) + if is_line_dict: + lw = _get(inner_val, 'width') if _has_key(inner_val, 'width') else 0 + lg = ('true' if _get(inner_val, 'gap') else 'false') if _has_key(inner_val, 'gap') else 'false' + ls = str(_get(inner_val, 'style')) if _has_key(inner_val, 'style') else 'None' + lines.append(f'{indent}\t<dcscor:value xsi:type="v8ui:Line" width="{lw}" gap="{lg}">') + lines.append(f'{indent}\t\t<v8ui:style xsi:type="v8ui:SpreadsheetDocumentCellLineType">{esc_xml(ls)}</v8ui:style>') + lines.append(f'{indent}\t</dcscor:value>') + elif is_font_dict: + attr_parts = [] + for attr_name in ('ref', 'faceName', 'height', 'bold', 'italic', 'underline', 'strikeout', 'kind', 'scale'): + if attr_name in inner_val: + av = inner_val[attr_name] + if av is not None: + attr_parts.append(f'{attr_name}="{esc_xml(str(av))}"') + lines.append(f'{indent}\t<dcscor:value xsi:type="v8ui:Font" {" ".join(attr_parts)}/>') + elif is_dict and _has_key(inner_val, 'field'): + # Ссылка на поле (dcscor:Field) — значение параметра оформления = поле компоновки + lines.append(f'{indent}\t<dcscor:value xsi:type="dcscor:Field">{esc_xml(str(_get(inner_val, "field")))}</dcscor:value>') + elif is_dict: + # Локализуемый текст параметра оформления: платформа объявляет xsi:type на dcscor:value + emit_mltext(lines, f'{indent}\t', 'dcscor:value', inner_val, xsi_type='v8:LocalStringType') + else: + actual_val = str(inner_val) + key_type_map = { + 'Размещение': 'dcscor:DataCompositionTextPlacementType', + 'ГоризонтальноеПоложение': 'v8ui:HorizontalAlign', + 'ВертикальноеПоложение': 'v8ui:VerticalAlign', + 'ОриентацияТекста': 'xs:decimal', + 'РасположениеИтогов': 'dcscor:DataCompositionTotalPlacement', + 'ТипМакета': 'dcsset:DataCompositionGroupTemplateType', + } + key_type = key_type_map.get(key) + if key_type: + lines.append(f'{indent}\t<dcscor:value xsi:type="{key_type}">{esc_xml(actual_val)}</dcscor:value>') + elif re.match(r'^(style|web|win):', actual_val): + lines.append(f'{indent}\t<dcscor:value xsi:type="v8ui:Color">{esc_xml(actual_val)}</dcscor:value>') + elif actual_val == 'true' or actual_val == 'false': + lines.append(f'{indent}\t<dcscor:value xsi:type="xs:boolean">{actual_val}</dcscor:value>') + elif key == 'Текст' or key == 'Заголовок' or key == 'Формат': + # Голая строка = плоский xs:string (нелокализованный литерал). Локализуемый → объект {ru,en}. + # Пустая строка → самозакрывающийся тег (как у платформы). + if actual_val == '': + lines.append(f'{indent}\t<dcscor:value xsi:type="xs:string"/>') + else: + lines.append(f'{indent}\t<dcscor:value xsi:type="xs:string">{esc_xml(actual_val)}</dcscor:value>') + elif re.match(r'^-?\d+(\.\d+)?$', actual_val): + lines.append(f'{indent}\t<dcscor:value xsi:type="xs:decimal">{actual_val}</dcscor:value>') + elif key == 'ЦветТекста' or key == 'ЦветФона' or key == 'ЦветГраницы': + lines.append(f'{indent}\t<dcscor:value xsi:type="v8ui:Color">{esc_xml(actual_val)}</dcscor:value>') + else: + lines.append(f'{indent}\t<dcscor:value xsi:type="xs:string">{esc_xml(actual_val)}</dcscor:value>') + if nested_items: + if isinstance(nested_items, dict): + for nk, nv in nested_items.items(): + emit_appearance_value(lines, nk, nv, f'{indent}\t') + lines.append(f'{indent}</dcscor:item>') + + +# === Группировка строк динамического списка (DCS-структура ListSettings) === +# Линейная цепочка <dcsset:item StructureItemGroup> (каждый уровень = одно поле в groupItems; +# вложенность — через дочерний <dcsset:item>). Плоская модель уровней (список всегда линеен). +def get_list_grouping_value(s): + for k in ('grouping', 'structure', 'группировка'): + if s.get(k): + return s[k] + return None + + +def parse_list_grouping(grouping): + # Шорткат "A > B > C" → массив имён; массив строк/объектов → как есть. + if not grouping: + return [] + if isinstance(grouping, str): + return [p.strip() for p in re.split(r'\s*>\s*', grouping) if p.strip()] + return list(grouping) + + +def emit_group_item_field(lines, level, indent): + if isinstance(level, str): + field, gt, pat = level, 'Items', 'None' + pab = pae = '0001-01-01T00:00:00' + else: + field = str(level.get('field', '')) + gt = str(level.get('groupType') or 'Items') + pat = str(level.get('periodAdditionType') or 'None') + pab = str(level.get('periodAdditionBegin') or '0001-01-01T00:00:00') + pae = str(level.get('periodAdditionEnd') or '0001-01-01T00:00:00') + lines.append(f'{indent}<dcsset:item xsi:type="dcsset:GroupItemField">') + lines.append(f'{indent}\t<dcsset:field>{esc_xml(field)}</dcsset:field>') + lines.append(f'{indent}\t<dcsset:groupType>{esc_xml(gt)}</dcsset:groupType>') + lines.append(f'{indent}\t<dcsset:periodAdditionType>{esc_xml(pat)}</dcsset:periodAdditionType>') + # Авто-детект: ISO-дата → xs:dateTime, иначе путь → dcscor:Field. + pab_t = 'xs:dateTime' if re.match(r'^\d{4}-\d{2}-\d{2}T', pab) else 'dcscor:Field' + pae_t = 'xs:dateTime' if re.match(r'^\d{4}-\d{2}-\d{2}T', pae) else 'dcscor:Field' + lines.append(f'{indent}\t<dcsset:periodAdditionBegin xsi:type="{pab_t}">{esc_xml(pab)}</dcsset:periodAdditionBegin>') + lines.append(f'{indent}\t<dcsset:periodAdditionEnd xsi:type="{pae_t}">{esc_xml(pae)}</dcsset:periodAdditionEnd>') + lines.append(f'{indent}</dcsset:item>') + + +def emit_list_grouping_levels(lines, levels, i, indent): + lines.append(f'{indent}<dcsset:item xsi:type="dcsset:StructureItemGroup">') + lines.append(f'{indent}\t<dcsset:groupItems>') + emit_group_item_field(lines, levels[i], f'{indent}\t\t') + lines.append(f'{indent}\t</dcsset:groupItems>') + if i < len(levels) - 1: + emit_list_grouping_levels(lines, levels, i + 1, f'{indent}\t') + lines.append(f'{indent}</dcsset:item>') + + +def emit_list_grouping(lines, grouping, indent): + levels = parse_list_grouping(grouping) + if not levels: + return + emit_list_grouping_levels(lines, levels, 0, indent) + + +# === Вычисляемые поля DataSet динамического списка (<CalculatedField>) === +# Зеркало skd calculatedFields: shorthand "Имя [Заголовок]: тип = Выражение #noField #noFilter +# #noGroup #noOrder" или объект. Форм-специфика: dcssch:-теги + presentationExpression/orderExpression. +_CALC_RESTRICT_MAP = {'noField': 'field', 'noFilter': 'condition', 'noCondition': 'condition', + 'noGroup': 'group', 'noOrder': 'order'} +_DCS_COMMON_NS = 'http://v8.1c.ru/8.1/data-composition-system/common' + + +def parse_calc_shorthand(s): + restrict = re.findall(r'#(noField|noFilter|noCondition|noGroup|noOrder)\b', s) + s = re.sub(r'\s*#(?:noField|noFilter|noCondition|noGroup|noOrder)\b', '', s) + eq = s.find('=') + lhs, rhs = (s[:eq], s[eq + 1:].strip()) if eq > 0 else (s, '') + title = '' + m = re.search(r'\[([^\]]+)\]', lhs) + if m: + title = m.group(1) + lhs = re.sub(r'\s*\[[^\]]+\]', '', lhs) + lhs = lhs.strip() + typ, data_path = '', lhs + if ':' in lhs: + data_path, t = lhs.split(':', 1) + data_path, typ = data_path.strip(), resolve_type_str(t.strip()) + return {'dataPath': data_path, 'expression': rhs, 'type': typ, 'title': title, 'restrict': restrict} + + +def emit_calc_fields(lines, calc_fields, indent): + if not calc_fields: + return + for cf in calc_fields: + if isinstance(cf, str): + p = parse_calc_shorthand(cf) + data_path, expression, title = p['dataPath'], p['expression'], p['title'] + type_str = p['type'] + restrict = [_CALC_RESTRICT_MAP[r] for r in p['restrict'] if r in _CALC_RESTRICT_MAP] + pres_expr = order_expr = None + else: + data_path = str(cf.get('dataPath') or cf.get('field') or cf.get('name', '')) + expression = str(cf.get('expression', '')) + title = cf.get('title') + type_str = cf.get('valueType') or cf.get('type') + type_str = str(type_str) if type_str else None + ur = cf.get('useRestriction') or cf.get('restrict') + if isinstance(ur, dict): + restrict = [k for k in ('field', 'condition', 'group', 'order') if ur.get(k)] + elif isinstance(ur, str): + restrict = [_CALC_RESTRICT_MAP.get(t.strip().lstrip('#'), t.strip().lstrip('#')) for t in ur.split() if t.strip()] + elif isinstance(ur, list): + restrict = [_CALC_RESTRICT_MAP.get(str(r), str(r)) for r in ur] + else: + restrict = [] + pres_expr = cf.get('presentationExpression') + order_expr = cf.get('orderExpression') + ci = f'{indent}\t' + lines.append(f'{indent}<CalculatedField>') + lines.append(f'{ci}<dcssch:dataPath>{esc_xml(data_path)}</dcssch:dataPath>') + lines.append(f'{ci}<dcssch:expression>{esc_xml(expression)}</dcssch:expression>') + if title: + emit_mltext(lines, ci, 'dcssch:title', title, xsi_type='v8:LocalStringType') + if restrict: + lines.append(f'{ci}<dcssch:useRestriction>') + for r in ('field', 'condition', 'group', 'order'): + if r in restrict: + lines.append(f'{ci}\t<dcssch:{r}>true</dcssch:{r}>') + lines.append(f'{ci}</dcssch:useRestriction>') + if pres_expr: + lines.append(f'{ci}<dcssch:presentationExpression>{esc_xml(str(pres_expr))}</dcssch:presentationExpression>') + if order_expr: + for oe in (order_expr if isinstance(order_expr, list) else [order_expr]): + if isinstance(oe, str): + expr_v, otype, auto = oe, 'Asc', 'false' + else: + expr_v = str(oe.get('expression', '')) + otype = str(oe.get('orderType', 'Asc')) + auto = 'true' if oe.get('autoOrder') else 'false' + lines.append(f'{ci}<dcssch:orderExpression>') + lines.append(f'{ci}\t<expression xmlns="{_DCS_COMMON_NS}">{esc_xml(expr_v)}</expression>') + lines.append(f'{ci}\t<orderType xmlns="{_DCS_COMMON_NS}">{otype}</orderType>') + lines.append(f'{ci}\t<autoOrder xmlns="{_DCS_COMMON_NS}">{auto}</autoOrder>') + lines.append(f'{ci}</dcssch:orderExpression>') + if type_str: + emit_dl_value_type(lines, type_str, ci) + lines.append(f'{indent}</CalculatedField>') + + +# Ограничения использования поля/вычисляемого поля (useRestriction / attributeUseRestriction). +# Значение: объект {field?,condition?,group?,order?} | флаг-строка "#noField …" | массив. +def parse_restrict(ur): + if not ur: + return [] + if isinstance(ur, dict): + return [k for k in ('field', 'condition', 'group', 'order') if ur.get(k)] + if isinstance(ur, str): + return [_CALC_RESTRICT_MAP.get(t.strip().lstrip('#'), t.strip().lstrip('#')) for t in ur.split() if t.strip()] + if isinstance(ur, list): + return [_CALC_RESTRICT_MAP.get(str(r), str(r)) for r in ur] + return [] + + +def emit_restrict_block(lines, tag, ur, indent): + r = parse_restrict(ur) + if not r: + return + lines.append(f'{indent}<dcssch:{tag}>') + for k in ('field', 'condition', 'group', 'order'): + if k in r: + lines.append(f'{indent}\t<dcssch:{k}>true</dcssch:{k}>') + lines.append(f'{indent}</dcssch:{tag}>') + + +def emit_conditional_appearance(lines, items, indent, block_view_mode=None, block_user_setting_id=None, wrap_tag='dcsset:conditionalAppearance', block_user_setting_presentation=None): + has_items = bool(items) and len(items) > 0 + has_block_meta = (block_view_mode is not None) or (block_user_setting_id is not None) or (block_user_setting_presentation is not None) + if not has_items and not has_block_meta: + return + lines.append(f'{indent}<{wrap_tag}>') + for ca in (items or []): + lines.append(f'{indent}\t<dcsset:item>') + if ca.get('use') is False: + lines.append(f'{indent}\t\t<dcsset:use>false</dcsset:use>') + if ca.get('selection') and len(ca['selection']) > 0: + lines.append(f'{indent}\t\t<dcsset:selection>') + for sel in ca['selection']: + lines.append(f'{indent}\t\t\t<dcsset:item>') + lines.append(f'{indent}\t\t\t\t<dcsset:field>{esc_xml(str(sel))}</dcsset:field>') + lines.append(f'{indent}\t\t\t</dcsset:item>') + lines.append(f'{indent}\t\t</dcsset:selection>') + else: + lines.append(f'{indent}\t\t<dcsset:selection/>') + if ca.get('filter') and len(ca['filter']) > 0: + emit_filter(lines, ca['filter'], f'{indent}\t\t') + else: + lines.append(f'{indent}\t\t<dcsset:filter/>') + if ca.get('appearance'): + lines.append(f'{indent}\t\t<dcsset:appearance>') + for k, v in ca['appearance'].items(): + emit_appearance_value(lines, k, v, f'{indent}\t\t\t') + lines.append(f'{indent}\t\t</dcsset:appearance>') + if ca.get('presentation'): + if isinstance(ca['presentation'], dict): + # Мультиязык → LocalStringType (платформа объявляет тип у локализованного presentation) + lines.append(f'{indent}\t\t<dcsset:presentation xsi:type="v8:LocalStringType">') + emit_ml_items(lines, f'{indent}\t\t\t', ca['presentation']) + lines.append(f'{indent}\t\t</dcsset:presentation>') + else: + lines.append(f'{indent}\t\t<dcsset:presentation xsi:type="xs:string">{esc_xml(str(ca["presentation"]))}</dcsset:presentation>') + if ca.get('viewMode'): + lines.append(f'{indent}\t\t<dcsset:viewMode>{esc_xml(str(ca["viewMode"]))}</dcsset:viewMode>') + if ca.get('userSettingID'): + uid = new_uuid() if str(ca['userSettingID']) == 'auto' else str(ca['userSettingID']) + lines.append(f'{indent}\t\t<dcsset:userSettingID>{esc_xml(uid)}</dcsset:userSettingID>') + if ca.get('userSettingPresentation'): + emit_us_presentation(lines, f'{indent}\t\t', 'dcsset:userSettingPresentation', ca['userSettingPresentation']) + if ca.get('useInDontUse') and len(ca['useInDontUse']) > 0: + use_in_order = ['group', 'hierarchicalGroup', 'overall', 'fieldsHeader', 'header', + 'parameters', 'filter', 'resourceFieldsHeader', 'overallHeader', + 'overallResourceFieldsHeader'] + sset = {str(n): True for n in ca['useInDontUse']} + for n in use_in_order: + if n in sset: + tag = 'useIn' + n[0].upper() + n[1:] + lines.append(f'{indent}\t\t<dcsset:{tag}>DontUse</dcsset:{tag}>') + lines.append(f'{indent}\t</dcsset:item>') + if block_view_mode is not None: + lines.append(f'{indent}\t<dcsset:viewMode>{esc_xml(str(block_view_mode))}</dcsset:viewMode>') + if block_user_setting_id is not None: + uid = new_uuid() if str(block_user_setting_id) == 'auto' else str(block_user_setting_id) + lines.append(f'{indent}\t<dcsset:userSettingID>{esc_xml(uid)}</dcsset:userSettingID>') + if block_user_setting_presentation is not None: + emit_us_presentation(lines, f'{indent}\t', 'dcsset:userSettingPresentation', block_user_setting_presentation) + lines.append(f'{indent}</{wrap_tag}>') + + +def write_utf8_bom(path, content): + with open(path, 'w', encoding='utf-8-sig', newline='') as f: + f.write(content) + + +# --- ID allocator --- +_next_id = 0 + +def new_id(): + global _next_id + _next_id += 1 + return _next_id + + +# Уникальность имён внутри коллекции (1С: элементы/реквизиты/команды/параметры/колонки — каждое своё +# пространство имён). Дубль → битый XML, форма не открывается, поэтому fail-fast. +_seen_element_names = set() # пул имён элементов (глобально по всей форме) + +def _ensure_unique(name, seen, kind): + if name in seen: + print(f"[ERROR] Duplicate {kind} name '{name}' — names must be unique within their collection in a 1C form (set a unique 'name')", file=sys.stderr) + sys.exit(1) + seen.add(name) + + +# --- Event handler name generator --- + +EVENT_SUFFIX_MAP = { + "OnChange": "\u041f\u0440\u0438\u0418\u0437\u043c\u0435\u043d\u0435\u043d\u0438\u0438", + "StartChoice": "\u041d\u0430\u0447\u0430\u043b\u043e\u0412\u044b\u0431\u043e\u0440\u0430", + "ChoiceProcessing": "\u041e\u0431\u0440\u0430\u0431\u043e\u0442\u043a\u0430\u0412\u044b\u0431\u043e\u0440\u0430", + "AutoComplete": "\u0410\u0432\u0442\u043e\u041f\u043e\u0434\u0431\u043e\u0440", + "Clearing": "\u041e\u0447\u0438\u0441\u0442\u043a\u0430", + "Opening": "\u041e\u0442\u043a\u0440\u044b\u0442\u0438\u0435", + "Click": "\u041d\u0430\u0436\u0430\u0442\u0438\u0435", + "OnActivateRow": "\u041f\u0440\u0438\u0410\u043a\u0442\u0438\u0432\u0438\u0437\u0430\u0446\u0438\u0438\u0421\u0442\u0440\u043e\u043a\u0438", + "BeforeAddRow": "\u041f\u0435\u0440\u0435\u0434\u041d\u0430\u0447\u0430\u043b\u043e\u043c\u0414\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0438\u044f", + "BeforeDeleteRow": "\u041f\u0435\u0440\u0435\u0434\u0423\u0434\u0430\u043b\u0435\u043d\u0438\u0435\u043c", + "BeforeRowChange": "\u041f\u0435\u0440\u0435\u0434\u041d\u0430\u0447\u0430\u043b\u043e\u043c\u0418\u0437\u043c\u0435\u043d\u0435\u043d\u0438\u044f", + "OnStartEdit": "\u041f\u0440\u0438\u041d\u0430\u0447\u0430\u043b\u0435\u0420\u0435\u0434\u0430\u043a\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044f", + "OnEndEdit": "\u041f\u0440\u0438\u041e\u043a\u043e\u043d\u0447\u0430\u043d\u0438\u0438\u0420\u0435\u0434\u0430\u043a\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044f", + "Selection": "\u0412\u044b\u0431\u043e\u0440\u0421\u0442\u0440\u043e\u043a\u0438", + "OnCurrentPageChange": "\u041f\u0440\u0438\u0421\u043c\u0435\u043d\u0435\u0421\u0442\u0440\u0430\u043d\u0438\u0446\u044b", + "TextEditEnd": "\u041e\u043a\u043e\u043d\u0447\u0430\u043d\u0438\u0435\u0412\u0432\u043e\u0434\u0430\u0422\u0435\u043a\u0441\u0442\u0430", + "URLProcessing": "\u041e\u0431\u0440\u0430\u0431\u043e\u0442\u043a\u0430\u041d\u0430\u0432\u0438\u0433\u0430\u0446\u0438\u043e\u043d\u043d\u043e\u0439\u0421\u0441\u044b\u043b\u043a\u0438", + "DragStart": "\u041d\u0430\u0447\u0430\u043b\u043e\u041f\u0435\u0440\u0435\u0442\u0430\u0441\u043a\u0438\u0432\u0430\u043d\u0438\u044f", + "Drag": "\u041f\u0435\u0440\u0435\u0442\u0430\u0441\u043a\u0438\u0432\u0430\u043d\u0438\u0435", + "DragCheck": "\u041f\u0440\u043e\u0432\u0435\u0440\u043a\u0430\u041f\u0435\u0440\u0435\u0442\u0430\u0441\u043a\u0438\u0432\u0430\u043d\u0438\u044f", + "Drop": "\u041f\u043e\u043c\u0435\u0449\u0435\u043d\u0438\u0435", + "AfterDeleteRow": "\u041f\u043e\u0441\u043b\u0435\u0423\u0434\u0430\u043b\u0435\u043d\u0438\u044f", +} + +KNOWN_EVENTS = { + "input": ["OnChange", "StartChoice", "ChoiceProcessing", "AutoComplete", "TextEditEnd", "Clearing", "Creating", "EditTextChange"], + "check": ["OnChange"], + "radio": ["OnChange"], + "label": ["Click", "URLProcessing"], + "labelField": ["OnChange", "StartChoice", "ChoiceProcessing", "Click", "URLProcessing", "Clearing"], + "table": ["Selection", "BeforeAddRow", "AfterDeleteRow", "BeforeDeleteRow", "OnActivateRow", "OnEditEnd", "OnStartEdit", "BeforeRowChange", "BeforeEditEnd", "ValueChoice", "OnActivateCell", "OnActivateField", "Drag", "DragStart", "DragCheck", "DragEnd", "OnGetDataAtServer", "BeforeLoadUserSettingsAtServer", "OnUpdateUserSettingSetAtServer", "OnChange"], + "pages": ["OnCurrentPageChange"], + "page": ["OnCurrentPageChange"], + "button": ["Click"], + "picField": ["OnChange", "StartChoice", "ChoiceProcessing", "Click", "Clearing"], + "calendar": ["OnChange", "OnActivate"], + "picture": ["Click"], + "cmdBar": [], + "popup": [], + "group": [], +} + +KNOWN_FORM_EVENTS = [ + "OnCreateAtServer", "OnOpen", "BeforeClose", "OnClose", "NotificationProcessing", + "ChoiceProcessing", "OnReadAtServer", "AfterWriteAtServer", "BeforeWriteAtServer", + "AfterWrite", "BeforeWrite", "OnWriteAtServer", "FillCheckProcessingAtServer", + "OnLoadDataFromSettingsAtServer", "BeforeLoadDataFromSettingsAtServer", + "OnSaveDataInSettingsAtServer", "ExternalEvent", "OnReopen", "Opening", +] + +KNOWN_KEYS = { + "group", "columnGroup", "buttonGroup", "input", "check", "radio", "label", "labelField", "table", "pages", "page", + "button", "picture", "picField", "calendar", "cmdBar", "popup", + "showInHeader", + "radioButtonType", "choiceList", "columnsCount", "checkBoxType", "editMode", + "name", "path", "title", "tooltip", "tooltipRepresentation", "extendedTooltip", + "visible", "hidden", "enabled", "disabled", "readOnly", "userVisible", + "events", "on", "handlers", + "selectionMode", "showCurrentDate", "widthInMonths", "heightInMonths", "showMonthsPanel", + "titleLocation", "representation", "width", "height", + "horizontalStretch", "verticalStretch", "autoMaxWidth", "autoMaxHeight", + "maxWidth", "maxHeight", + "groupHorizontalAlign", "groupVerticalAlign", "horizontalAlign", + "multiLine", "passwordMode", "choiceButton", "clearButton", + "spinButton", "dropListButton", "markIncomplete", "skipOnInput", "inputHint", + "textEdit", "choiceList", + "wrap", "openButton", "listChoiceMode", "showInHeader", "showInFooter", + "extendedEditMultipleValues", "chooseType", "autoCellHeight", + "choiceButtonRepresentation", "footerHorizontalAlign", "headerHorizontalAlign", + "headerDataPath", "headerFormat", + "format", "editFormat", "choiceParameters", "choiceParameterLinks", "typeLink", + "hyperlink", "formatted", + "collapsedTitle", "showTitle", "united", "collapsed", "behavior", + "children", "columns", + "changeRowSet", "changeRowOrder", "autoInsertNewRow", "rowFilter", "header", "footer", + "commandBarLocation", "searchStringLocation", "viewStatusLocation", "searchControlLocation", + "excludedCommands", + "pagesRepresentation", + "type", "command", "commandName", "stdCommand", "parameter", "defaultButton", "locationInCommandBar", "displayImportance", + "commandBar", "contextMenu", "commandSource", + "src", "valuesPicture", "loadTransparent", "headerPicture", "footerPicture", + "autofill", + "choiceMode", "initialTreeView", "enableDrag", "enableStartDrag", + "rowSelectionMode", "verticalLines", "horizontalLines", + "rowPictureDataPath", "tableAutofill", "heightInTableRows", + "multipleChoice", "searchOnInput", "shortcut", + # dynamic-list table block + "defaultItem", "useAlternationRowColor", "fileDragMode", "autoRefresh", + "autoRefreshPeriod", "choiceFoldersAndItems", "restoreCurrentRow", "showRoot", + "allowRootChoice", "updateOnDataChange", "allowGettingCurrentRowURL", + "userSettingsGroup", "rowsPicture", + # AutoCommandBar-маркер (autofill heuristic) на элементе/таблице + "autoCmdBar", + # дополнения командной панели таблицы (тип-ключи + свойства) + "searchString", "viewStatus", "searchControl", "source", "horizontalLocation", "additions", + # generic-скаляры (pass-through) + "verticalAlign", "throughAlign", "enableContentChange", "pictureSize", "titleHeight", + "childItemsWidth", "showLeftMargin", "cellHyperlink", "viewMode", "verticalScrollBar", + "rowInputMode", "mask", "createButton", "fixingInTable", "verticalSpacing", + # InputField choice-скаляры + "choiceListButton", "quickChoice", "autoChoiceIncomplete", + "choiceForm", "choiceHistoryOnInput", "footerDataPath", "minValue", "maxValue", + # Button — пометка toggle-кнопки + "checked", + # спец-поля (документ/датчик/диаграмма) — тип-ключи + типоспец. скаляры + "spreadsheet", "html", "textDoc", "formattedDoc", "progressBar", "trackBar", + "chart", "ganttChart", "graphicalSchema", "planner", "periodField", "dendrogram", "ganttTable", + "showPercent", "largeStep", "markingStep", "step", + "horizontalScrollBar", "viewScalingMode", "output", "selectionShowMode", "protection", + "edit", "showGrid", "showGroups", "showHeaders", "showRowAndColumnNames", "showCellNames", + "pointerType", "drawingSelectionShowMode", "warningOnEditRepresentation", "markingAppearance", + # report-form контекст (generic-скаляры элементов) + "horizontalSpacing", "representationInContextMenu", "settingsNamedItemDetailedRepresentation", + # хвост: высота элемента списка / ширина выпадающего списка / картинка кнопки выбора / прозрачный пиксель + "itemHeight", "dropListWidth", "choiceButtonPicture", "transparentPixel", + # хвост CI-форм: динамический заголовок / расширенное редактирование / высота таблицы + "titleDataPath", "extendedEdit", "maxRowsCount", "autoMaxRowsCount", "heightControlVariant", + "warningOnEdit", "nonselectedPictureText", "editTextUpdate", "footerText", +} + +# picture/picField — НИЗКИЙ приоритет: 'picture' это и тип (PictureDecoration), и свойство-иконка +# у popup/button/cmdBar. Тип-ключ владельца (popup/button/…) должен выиграть. +# pages/page ПЕРЕД group: у Page/Pages ключ 'group' — это направление раскладки детей +# (<Group>Horizontal</Group>), а не тип UsualGroup. Реальная UsualGroup ключа page/pages не несёт. +TYPE_KEYS = ["columnGroup", "buttonGroup", "pages", "page", "group", "input", "check", "radio", "label", "labelField", "table", + "button", "calendar", "cmdBar", "popup", "searchString", "viewStatus", "searchControl", "picField", "picture", + "spreadsheet", "html", "textDoc", "formattedDoc", "progressBar", "trackBar", + "chart", "ganttChart", "graphicalSchema", "planner", "periodField", "dendrogram"] + +# Synonyms: model often writes XML name or Russian (ПолеПереключателя/RadioButtonField → radio) +ELEMENT_TYPE_SYNONYMS = { + "commandBar": "cmdBar", + "autoCommandBar": "autoCmdBar", + "КоманднаяПанель": "cmdBar", + "InputField": "input", + "ПолеВвода": "input", + "CheckBoxField": "check", + "ПолеФлажка": "check", + "RadioButtonField": "radio", + "ПолеПереключателя": "radio", + "radioButton": "radio", + "PictureField": "picField", + "ПолеКартинки": "picField", + "LabelField": "labelField", + "ПолеНадписи": "labelField", + "CalendarField": "calendar", + "ПолеКалендаря": "calendar", + "LabelDecoration": "label", + "Надпись": "label", + "PictureDecoration": "picture", + "Картинка": "picture", + "UsualGroup": "group", + "Группа": "group", + "ОбычнаяГруппа": "group", + "ColumnGroup": "columnGroup", + "ГруппаКолонок": "columnGroup", + "Pages": "pages", + "ГруппаСтраниц": "pages", + "Page": "page", + "Страница": "page", + "Table": "table", + "Таблица": "table", + "Button": "button", + "Кнопка": "button", + "Popup": "popup", + "ВсплывающееМеню": "popup", + # дополнения командной панели таблицы — forgiving: XML-тег/Type/рус.имя → канон + "SearchStringAddition": "searchString", + "SearchStringRepresentation": "searchString", + "строкаПоиска": "searchString", + "отображениеСтрокиПоиска": "searchString", + "Отображение строки поиска": "searchString", + "ViewStatusAddition": "viewStatus", + "ViewStatusRepresentation": "viewStatus", + "состояниеПросмотра": "viewStatus", + "Состояние просмотра": "viewStatus", + "SearchControlAddition": "searchControl", + "SearchControl": "searchControl", + "управлениеПоиском": "searchControl", + "Управление поиском": "searchControl", + # Спец-поля (документ/датчик) — XML-имя/рус. → канон + "SpreadSheetDocumentField": "spreadsheet", + "ПолеТабличногоДокумента": "spreadsheet", + "HTMLDocumentField": "html", + "ПолеHTMLДокумента": "html", + "TextDocumentField": "textDoc", + "ПолеТекстовогоДокумента": "textDoc", + "FormattedDocumentField": "formattedDoc", + "ПолеФорматированногоДокумента": "formattedDoc", + "ProgressBarField": "progressBar", + "ПолеИндикатора": "progressBar", + "TrackBarField": "trackBar", + "ПолеПолосыРегулирования": "trackBar", + "ChartField": "chart", + "ПолеДиаграммы": "chart", + "GanttChartField": "ganttChart", + "ПолеДиаграммыГанта": "ganttChart", + "GraphicalSchemaField": "graphicalSchema", + "ПолеГрафическойСхемы": "graphicalSchema", + "PlannerField": "planner", + "ПолеПланировщика": "planner", + "PeriodField": "periodField", + "ПолеПериода": "periodField", + "DendrogramField": "dendrogram", + "ПолеДендрограммы": "dendrogram", +} + +# Тип-синонимы, применяемые ТОЛЬКО к строковому значению (имя элемента); объект/массив +# у того же слова — companion-панель (свойство), см. normalize_panel_synonyms. +STR_ONLY_TYPE_SYNONYMS = {"commandBar", "autoCommandBar", "КоманднаяПанель"} + +# Companion-панели как СВОЙСТВА (значение объект/массив): синоним → каноника. +PANEL_SYNONYMS = { + 'commandBar': ['commandBar', 'autoCommandBar', 'AutoCommandBar', 'autoCmdBar', 'cmdBar', 'КоманднаяПанель'], + 'contextMenu': ['contextMenu', 'ContextMenu', 'КонтекстноеМеню'], +} + + +def normalize_panel_synonyms(el): + if not isinstance(el, dict): + return + for canon, syns in PANEL_SYNONYMS.items(): + for syn in syns: + if syn in el and isinstance(el[syn], (list, dict)): + if syn != canon and canon not in el: + el[canon] = el.pop(syn) + break + + +# Maps Russian/English root of typed reference path to canonical English root +REF_ROOT_SYNONYMS = { + "Перечисление": "Enum", + "Справочник": "Catalog", + "Документ": "Document", + "ПланСчетов": "ChartOfAccounts", + "ПланВидовХарактеристик": "ChartOfCharacteristicTypes", + "ПланВидовРасчета": "ChartOfCalculationTypes", + "ПланВидовРасчёта": "ChartOfCalculationTypes", + "ПланОбмена": "ExchangePlan", + "БизнесПроцесс": "BusinessProcess", + "Задача": "Task", + "РегистрСведений": "InformationRegister", + "РегистрНакопления": "AccumulationRegister", + "РегистрБухгалтерии": "AccountingRegister", + "РегистрРасчета": "CalculationRegister", + "РегистрРасчёта": "CalculationRegister", + "ЖурналДокументов": "DocumentJournal", + "КритерийОтбора": "FilterCriterion", +} +ENUM_VALUE_SYNONYMS = {"EnumValue", "ЗначениеПеречисления"} + + +def normalize_meta_type_ref(ref): + # "Справочник.Контрагенты" → "Catalog.Контрагенты"; уже англ — без изменений + if not ref: + return ref + dot = ref.find('.') + if dot < 1: + return ref + root = ref[:dot] + if root in REF_ROOT_SYNONYMS: + return REF_ROOT_SYNONYMS[root] + ref[dot:] + return ref + + +def normalize_choice_value(value): + """Returns dict {xsi_type, text} for a choiceList item value.""" + if isinstance(value, bool): + return {"xsi_type": "xs:boolean", "text": "true" if value else "false"} + if isinstance(value, (int, float)): + return {"xsi_type": "xs:decimal", "text": str(value)} + + s = "" if value is None else str(value) + if not s: + return {"xsi_type": "xs:string", "text": ""} + + # ISO datetime ("2020-01-01T00:00:00") → xs:dateTime + if re.fullmatch(r'\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}', s): + return {"xsi_type": "xs:dateTime", "text": s} + + parts = s.split(".") + if len(parts) >= 2: + root = parts[0] + canon_root = None + if root in REF_ROOT_SYNONYMS: + canon_root = REF_ROOT_SYNONYMS[root] + elif root in REF_ROOT_SYNONYMS.values(): + canon_root = root + + if canon_root: + type_name = parts[1] + normalized = None + if canon_root == "Enum": + if len(parts) == 3 and parts[2] == 'EmptyRef': + # "Enum.X.EmptyRef" — пустая ссылка, НЕ значение перечисления (без .EnumValue.) + normalized = f"Enum.{type_name}.EmptyRef" + elif len(parts) == 3: + normalized = f"Enum.{type_name}.EnumValue.{parts[2]}" + elif len(parts) >= 4: + member = parts[2] + if member in ENUM_VALUE_SYNONYMS: + rest = ".".join(parts[3:]) + else: + rest = ".".join(parts[2:]) + normalized = f"Enum.{type_name}.EnumValue.{rest}" + else: + if len(parts) >= 3: + tail = ".".join(parts[1:]) + normalized = f"{canon_root}.{tail}" + + if normalized: + return {"xsi_type": "xr:DesignTimeRef", "text": normalized} + + return {"xsi_type": "xs:string", "text": s} + + +def emit_choice_presentation(lines, pres, indent): + """Accepts None/empty → <Presentation/>; str → ru only; dict → multi-lang.""" + if pres is None or (isinstance(pres, str) and pres == ""): + lines.append(f"{indent}<Presentation/>") + 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}<Presentation>") + for lang, content in pairs: + lines.append(f"{indent}\t<v8:item>") + lines.append(f"{indent}\t\t<v8:lang>{lang}</v8:lang>") + lines.append(f"{indent}\t\t<v8:content>{esc_xml(content)}</v8:content>") + lines.append(f"{indent}\t</v8:item>") + lines.append(f"{indent}</Presentation>") + + +def choice_value_tag(norm): + # <Value> для choiceList/choiceParameters: пустой текст → самозакрывающийся тег (зеркало платформы). + if not norm["text"]: + return f'<Value xsi:type="{norm["xsi_type"]}"/>' + return f'<Value xsi:type="{norm["xsi_type"]}">{esc_xml(norm["text"])}</Value>' + + +def emit_choice_list(lines, el, indent): + # <ChoiceList> — у RadioButtonField и InputField. Элемент: { value, presentation?/title? }. + choice_list = el.get('choiceList') or [] + if not choice_list: + return + lines.append(f'{indent}<ChoiceList>') + item_indent = f'{indent}\t' + for item in choice_list: + if not isinstance(item, dict): + continue + val_raw = item.get('value', item.get('значение')) + has_pres = any(k in item for k in ('presentation', 'представление', 'title')) + pres_raw = item.get('presentation', item.get('представление', item.get('title'))) + + # valueType: явный xsi:type значения (системное перечисление ent:*, иной не-примитив) — + # переопределяет авто-детект (normalize_choice_value вывела бы xs:string). + vt_raw = item.get('valueType') + if vt_raw: + norm = {'xsi_type': str(vt_raw), 'text': '' if val_raw is None else str(val_raw)} + else: + norm = normalize_choice_value(val_raw) + + if not has_pres: + if norm['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}<xr:Item>') + val_indent = f'{item_indent}\t' + lines.append(f'{val_indent}<xr:Presentation/>') + lines.append(f'{val_indent}<xr:CheckState>0</xr:CheckState>') + lines.append(f'{val_indent}<xr:Value xsi:type="FormChoiceListDesTimeValue">') + emit_choice_presentation(lines, pres_raw, f'{val_indent}\t') + lines.append(f'{val_indent}\t{choice_value_tag(norm)}') + lines.append(f'{val_indent}</xr:Value>') + lines.append(f'{item_indent}</xr:Item>') + lines.append(f'{indent}</ChoiceList>') + + +def get_el_prop(obj, names): + # Читает свойство из dict по списку синонимов (первый найденный, иначе None). + if not isinstance(obj, dict): + return None + for n in names: + if n in obj: + return obj[n] + return None + + +def to_scalar_literal(s): + # Литерал shorthand → тип: true/false → bool, целое/дробное → число, иначе строка. + t = str(s).strip() + if t.lower() == 'true': + return True + if t.lower() == 'false': + return False + if re.fullmatch(r'-?\d+', t): + return int(t) + if re.fullmatch(r'-?\d+\.\d+', t): + return float(t) + return t + + +def from_choice_param_shorthand(s): + # "name=value" либо "name=v1, v2, …" (запятые → массив). → {name, value}. + eq = s.find('=') + if eq < 0: + return {'name': s.strip()} + name = s[:eq].strip() + rest = s[eq + 1:] + if ',' in rest: + return {'name': name, 'value': [to_scalar_literal(p) for p in rest.split(',')]} + return {'name': name, 'value': to_scalar_literal(rest)} + + +def from_choice_param_link_shorthand(s): + # "name=dataPath" либо "name=dataPath:DontChange". → {name, dataPath, valueChange?}. + eq = s.find('=') + if eq < 0: + return {'name': s.strip()} + o = {'name': s[:eq].strip()} + rest = s[eq + 1:].strip() + m = re.fullmatch(r'(.*):(Clear|DontChange|очистить|неизменять)', rest, re.IGNORECASE) + if m: + o['dataPath'] = m.group(1).strip() + o['valueChange'] = m.group(2) + else: + o['dataPath'] = rest + return o + + +def from_type_link_shorthand(s): + # "dataPath" либо "dataPath#linkItem". → {dataPath, linkItem}. + m = re.fullmatch(r'(.*)#(\d+)', str(s)) + if m: + return {'dataPath': m.group(1).strip(), 'linkItem': int(m.group(2))} + return {'dataPath': str(s).strip()} + + +def emit_choice_param_value(lines, value, indent): + # Внутреннее значение параметра выбора (FormChoiceListDesTimeValue): <Presentation/> + <Value>. + # Скаляр → один Value; массив → v8:FixedArray из вложенных FormChoiceListDesTimeValue. + lines.append(f'{indent}<Presentation/>') + if isinstance(value, (list, tuple)): + lines.append(f'{indent}<Value xsi:type="v8:FixedArray">') + for v in value: + norm = normalize_choice_value(v) + lines.append(f'{indent}\t<v8:Value xsi:type="FormChoiceListDesTimeValue">') + lines.append(f'{indent}\t\t<Presentation/>') + lines.append(f'{indent}\t\t{choice_value_tag(norm)}') + lines.append(f'{indent}\t</v8:Value>') + lines.append(f'{indent}</Value>') + else: + norm = normalize_choice_value(value) + lines.append(f'{indent}{choice_value_tag(norm)}') + + +def emit_choice_parameters(lines, el, indent): + # <ChoiceParameters> (параметры выбора поля ввода) — [{name, value}]. value через + # normalize_choice_value; массив значений → FixedArray. Рус. синонимы имя/значение. + cp = el.get('choiceParameters') or [] + if not cp: + return + lines.append(f'{indent}<ChoiceParameters>') + for item in cp: + if isinstance(item, str): + item = from_choice_param_shorthand(item) + name = get_el_prop(item, ('name', 'имя')) + has_val = isinstance(item, dict) and ('value' in item or 'значение' in item) + val = get_el_prop(item, ('value', 'значение')) + name_s = '' if name is None else str(name) + lines.append(f'{indent}\t<app:item name="{esc_xml(name_s)}">') + # Параметр выбора без значения → <app:value xsi:nil="true"/> (платформа, 13 в корпусе); + # со значением (в т.ч. пустой строкой) → FormChoiceListDesTimeValue. + if not has_val: + lines.append(f'{indent}\t\t<app:value xsi:nil="true"/>') + else: + lines.append(f'{indent}\t\t<app:value xsi:type="FormChoiceListDesTimeValue">') + emit_choice_param_value(lines, val, f'{indent}\t\t\t') + lines.append(f'{indent}\t\t</app:value>') + lines.append(f'{indent}\t</app:item>') + lines.append(f'{indent}</ChoiceParameters>') + + +def emit_choice_parameter_links(lines, el, indent): + # <ChoiceParameterLinks> (связи параметров выбора) — [{name, dataPath, valueChange?}]. + # valueChange всегда эмитится, дефолт Clear; forgiving Clear/DontChange + рус. синонимы. + cpl = el.get('choiceParameterLinks') or [] + if not cpl: + return + lines.append(f'{indent}<ChoiceParameterLinks>') + for lk in cpl: + if isinstance(lk, str): + lk = from_choice_param_link_shorthand(lk) + name = get_el_prop(lk, ('name', 'имя')) + dp = get_el_prop(lk, ('dataPath', 'path', 'путь')) + vc_raw = get_el_prop(lk, ('valueChange', 'режимИзменения')) + vc = 'Clear' + if vc_raw: + s = str(vc_raw).lower() + if s in ('clear', 'очистить', 'очистка'): + vc = 'Clear' + elif s in ('dontchange', 'неизменять', 'неменять', 'нет'): + vc = 'DontChange' + else: + vc = str(vc_raw) + name_s = '' if name is None else str(name) + dp_s = '' if dp is None else str(dp) + lines.append(f'{indent}\t<xr:Link>') + lines.append(f'{indent}\t\t<xr:Name>{esc_xml(name_s)}</xr:Name>') + lines.append(f'{indent}\t\t<xr:DataPath xsi:type="xs:string">{esc_xml(dp_s)}</xr:DataPath>') + lines.append(f'{indent}\t\t<xr:ValueChange>{vc}</xr:ValueChange>') + lines.append(f'{indent}\t</xr:Link>') + lines.append(f'{indent}</ChoiceParameterLinks>') + + +def emit_type_link(lines, el, indent): + # <TypeLink> (связь по типу) — {dataPath, linkItem}. linkItem дефолт 0. + tl = el.get('typeLink') + if not tl: + return + if isinstance(tl, str): + tl = from_type_link_shorthand(tl) + dp = get_el_prop(tl, ('dataPath', 'path', 'путь')) + li = get_el_prop(tl, ('linkItem', 'элементСвязи')) + if li is None: + li = 0 + dp_s = '' if dp is None else str(dp) + lines.append(f'{indent}<TypeLink>') + lines.append(f'{indent}\t<xr:DataPath>{esc_xml(dp_s)}</xr:DataPath>') + lines.append(f'{indent}\t<xr:LinkItem>{li}</xr:LinkItem>') + lines.append(f'{indent}</TypeLink>') + + +def normalize_radio_button_type(raw): + if not raw: + return "Auto" + s = str(raw).strip().lower() + if s in ("auto", "авто"): + return "Auto" + if s in ("radiobutton", "radiobuttons", "переключатель", "радио"): + return "RadioButtons" + if s in ("tumbler", "тумблер"): + return "Tumbler" + return str(raw).strip() + + +def get_handler_name(element_name, event_name): + suffix = EVENT_SUFFIX_MAP.get(event_name) + if suffix: + return f"{element_name}{suffix}" + return f"{element_name}{event_name}" + + +def get_element_name(el, type_key): + if el.get('name'): + return str(el['name']) + return str(el.get(type_key, '')) + + +# Собрать упорядоченный список событий элемента (имя, обработчик) из DSL. +# Основной формат: el['events'] = { Событие: ИмяОбработчика } (None/"" → авто-имя по конвенции). +# Legacy (принимается ради совместимости): el['on'] (массив) + el['handlers'] (переопределение имён). +def get_event_pairs(el, element_name): + pairs = [] + events = el.get('events') + if events: + for ev_name, val in events.items(): + handler = '' if val is None else str(val) + if not handler: + handler = get_handler_name(element_name, ev_name) + pairs.append((ev_name, handler)) + elif el.get('on'): + handlers = el.get('handlers') or {} + for evt in el['on']: + evt_name = str(evt) + if handlers.get(evt_name): + handler = str(handlers[evt_name]) + else: + handler = get_handler_name(element_name, evt_name) + pairs.append((evt_name, handler)) + return pairs + + +# Проверить, подключено ли событие к элементу (в любом из форматов). +def test_element_event(el, event_name): + events = el.get('events') + if events and event_name in events: + return True + return event_name in (el.get('on') or []) + + +def emit_events(lines, el, element_name, indent, type_key): + pairs = get_event_pairs(el, element_name) + if not pairs: + return + + # Validate event names + if type_key and type_key in KNOWN_EVENTS: + allowed = KNOWN_EVENTS[type_key] + for ev_name, _ in pairs: + if allowed and str(ev_name) not in allowed: + print(f"[WARN] Unknown event '{ev_name}' for {type_key} '{element_name}'. Known: {', '.join(allowed)}") + + lines.append(f"{indent}<Events>") + for ev_name, handler in pairs: + lines.append(f'{indent}\t<Event name="{ev_name}">{handler}</Event>') + lines.append(f"{indent}</Events>") + + +# Детектор «настоящей» inline-разметки (1С: <link>/<b>/<color>/… и </>). Должен быть +# идентичен form-decompile/form-compile.ps1, иначе гибрид-раундтрип поедет. +_FMT_MARKUP_RE = re.compile(r'</>|<\s*(?:link|b|i|u|s|color|colorStyle|bgColor|bgColorStyle|font|fontSize|fontStyle|img)(?:\s|>)', re.I) + + +def _has_real_markup(text): + if text is None: + return False + vals = list(text.values()) if isinstance(text, dict) else [text] + return any(_FMT_MARKUP_RE.search(str(v)) for v in vals) + + +def resolve_ml_formatted(val): + # {text, formatted} = явный override; строка/мапа → авто-детект formatted + if isinstance(val, dict) and 'text' in val: + return val['text'], bool(val.get('formatted')) + return val, _has_real_markup(val) + + +# ExtendedTooltip — это LabelDecoration: own-content (layout/оформление/флаги/hyperlink) ±текст. +# Признак структурированной формы: объект с любым НЕ-текстовым ключом ({text,formatted}/{ru,en} → текст). +COMPANION_STRUCT_KEYS = { + 'width', 'autoMaxWidth', 'maxWidth', 'height', 'autoMaxHeight', 'maxHeight', 'verticalAlign', 'titleHeight', + 'horizontalStretch', 'verticalStretch', 'horizontalAlign', 'groupHorizontalAlign', 'groupVerticalAlign', + 'visible', 'hidden', 'enabled', 'disabled', 'hyperlink', 'events', 'tooltip', + 'textColor', 'backColor', 'borderColor', 'font', 'border', 'цветтекста', 'цветфона', 'цветрамки', 'шрифт', 'рамка', +} + + +def emit_companion_title(lines, content, indent): + text, fmt = resolve_ml_formatted(content) + lines.append(f'{indent}<Title formatted="{"true" if fmt else "false"}">') + emit_ml_items(lines, f'{indent}\t', text) + lines.append(f'{indent}') + + +def emit_companion(lines, tag, name, indent, content=None): + cid = new_id() + has_content = content is not None and not (isinstance(content, str) and content == '') + if not has_content: + lines.append(f'{indent}<{tag} name="{name}" id="{cid}"/>') + return + inner = f'{indent}\t' + lines.append(f'{indent}<{tag} name="{name}" id="{cid}">') + if isinstance(content, dict) and any(k in content for k in COMPANION_STRUCT_KEYS): + # own-content ПЕРЕД Title (в корпусе layout-first 582 vs 10). + emit_common_flags(lines, content, inner) + if content.get('hyperlink') is True: + lines.append(f'{inner}true') + emit_layout(lines, content, inner) + emit_appearance(lines, content, inner, 'decoration') + if 'text' in content: + emit_companion_title(lines, content, inner) + # ToolTip компаньона (подсказка самой расширенной подсказки) — после Title (порядок схемы LabelDecoration) + if content.get('tooltip'): + emit_mltext(lines, inner, 'ToolTip', content['tooltip']) + # События компаньона (ExtendedTooltip = LabelDecoration: напр. URLProcessing у hyperlink-подсказки) + emit_events(lines, content, name, inner, 'label') + else: + emit_companion_title(lines, content, inner) + lines.append(f'{indent}') + + +def emit_companion_panel(lines, tag, name, indent, panel): + # Companion-командная-панель (ContextMenu/AutoCommandBar) с контентом: { autofill?, horizontalAlign?, children?[] } + # или массив = shorthand для { children }. Пусто/нет → self-closing. + cid = new_id() + autofill = None + halign = None + children = None + if isinstance(panel, list): + children = panel + elif panel is not None: + if panel.get('autofill') is not None: + autofill = bool(panel.get('autofill')) + if panel.get('horizontalAlign'): + halign = str(panel.get('horizontalAlign')) + children = panel.get('children') + has_children = bool(children) and len(children) > 0 + # Платформа пишет только при false; true = дефолт (тег опускается). + emit_af_false = (autofill is False) + if not emit_af_false and not has_children and not halign: + lines.append(f'{indent}<{tag} name="{name}" id="{cid}"/>') + return + lines.append(f'{indent}<{tag} name="{name}" id="{cid}">') + if halign: + lines.append(f'{indent}\t{halign}') + if emit_af_false: + lines.append(f'{indent}\tfalse') + if has_children: + lines.append(f'{indent}\t') + for c in children: + emit_element(lines, c, f'{indent}\t\t', in_cmd_bar=True) + lines.append(f'{indent}\t') + lines.append(f'{indent}') + + +# Дополнения командной панели таблицы: тип DSL → XML-тег + AdditionSource.Type + суффикс имени. +ADDITION_TYPE_MAP = { + 'searchString': {'tag': 'SearchStringAddition', 'type': 'SearchStringRepresentation', 'suffix': 'СтрокаПоиска'}, + 'viewStatus': {'tag': 'ViewStatusAddition', 'type': 'ViewStatusRepresentation', 'suffix': 'СостояниеПросмотра'}, + 'searchControl': {'tag': 'SearchControlAddition', 'type': 'SearchControl', 'suffix': 'УправлениеПоиском'}, +} +ADDITION_KEY_SYNONYMS = { + 'searchString': ['SearchStringAddition', 'SearchStringRepresentation', 'строкаПоиска', 'отображениеСтрокиПоиска'], + 'viewStatus': ['ViewStatusAddition', 'ViewStatusRepresentation', 'состояниеПросмотра'], + 'searchControl': ['SearchControlAddition', 'SearchControl', 'управлениеПоиском'], +} +# Имя текущей таблицы — дефолт source для кастомных дополнений в commandBar. +_current_table_name = {'name': None} + + +def get_hlocation(el): + # HorizontalLocation: auto (дефолт, опускаем) / left / right; forgiving + рус. + if not isinstance(el, dict): + return None + v = el.get('horizontalLocation') + if not v: + return None + s = str(v).lower() + if s in ('auto', 'авто'): + return None + if s in ('left', 'слева', 'лево'): + return 'Left' + if s in ('right', 'справа', 'право'): + return 'Right' + if s in ('center', 'центр', 'по центру'): + return 'Center' + return str(v) + + +def emit_addition_body(lines, props, source, src_type, add_name, indent): + # Тело дополнения: AdditionSource + свойства (как у поля) + companions. props может быть None. + inner = f'{indent}\t' + lines.append(f'{inner}') + lines.append(f'{inner}\t{source}') + lines.append(f'{inner}\t{src_type}') + lines.append(f'{inner}') + if props: + if props.get('title'): + emit_mltext(lines, inner, 'Title', props['title']) + emit_common_flags(lines, props, inner) + if props.get('tooltip'): + emit_mltext(lines, inner, 'ToolTip', props['tooltip']) + if props.get('tooltipRepresentation'): + lines.append(f'{inner}{props["tooltipRepresentation"]}') + hl = get_hlocation(props) + if hl: + lines.append(f'{inner}{hl}') + emit_layout(lines, props, inner) + emit_appearance(lines, props, inner, 'field') + emit_companion(lines, 'ContextMenu', f'{add_name}КонтекстноеМеню', inner) + emit_companion(lines, 'ExtendedTooltip', f'{add_name}РасширеннаяПодсказка', inner) + + +def emit_addition(lines, el, name, eid, type_key, indent): + # Кастомное дополнение (тип-элемент в commandBar): source дефолтит в текущую таблицу. + m = ADDITION_TYPE_MAP[type_key] + source = el.get('source') or _current_table_name['name'] or '' + lines.append(f'{indent}<{m["tag"]} name="{name}" id="{eid}"{di_attr(el)}>') + emit_addition_body(lines, el, source, m['type'], name, indent) + lines.append(f'{indent}') + + +def emit_table_addition(lines, type_key, table_name, indent, override=None): + # Стандартное табличное дополнение (авто-генерация). override — объект отклонений из карты additions. + m = ADDITION_TYPE_MAP[type_key] + add_name = f'{table_name}{m["suffix"]}' + aid = new_id() + lines.append(f'{indent}<{m["tag"]} name="{add_name}" id="{aid}">') + emit_addition_body(lines, override, table_name, m['type'], add_name, indent) + lines.append(f'{indent}') + + +def get_addition_override(additions, type_key): + # Прочитать override-объект для типа из per-table карты additions (с синонимами). + if not isinstance(additions, dict): + return None + for k in [type_key] + ADDITION_KEY_SYNONYMS[type_key]: + if k in additions: + return additions[k] + return None + + +# Role-adjustable boolean (xr:Common + 0..N xr:Value name="Role.X"). +# Единый механизм платформы: UserVisible (элементы), View/Edit (атрибуты), Use (команды/кнопки). +# Значение DSL: скаляр bool → только ; объект { common, roles:{ Имя: bool } } → +пер-ролевые исключения. +# Имя роли принимаем с/без префикса "Role." (forgiving); на выход всегда с префиксом. +def emit_xr_flag(lines, tag, val, indent): + if val is None: + return + if isinstance(val, bool): + lines.append(f"{indent}<{tag}>") + lines.append(f"{indent}\t{'true' if val else 'false'}") + lines.append(f"{indent}") + return + # объектная форма { common, roles } + common = bool(val.get('common')) if val.get('common') is not None else False + lines.append(f"{indent}<{tag}>") + lines.append(f"{indent}\t{'true' if common else 'false'}") + roles = val.get('roles') + if roles: + for rname, rval in roles.items(): + # Forgiving: имя без префикса, с "Role." или кириллическим "Роль." → нормализуем в "Role.". + # Роль по GUID (заимствованная/расширение — name="" без префикса) эмитим как есть. + rn = re.sub(r'^(Role|Роль)\.', '', rname) + if not re.match(r'^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$', rn): + rn = "Role." + rn + lines.append(f"{indent}\t{'true' if rval else 'false'}") + lines.append(f"{indent}") + + +def emit_common_flags(lines, el, indent): + if el.get('visible') is False or el.get('hidden') is True: + lines.append(f"{indent}false") + if el.get('userVisible') is not None: + emit_xr_flag(lines, 'UserVisible', el.get('userVisible'), indent) + if el.get('enabled') is False or el.get('disabled') is True: + lines.append(f"{indent}false") + if el.get('readOnly') is True: + lines.append(f"{indent}true") + + +# Общие свойства элемента (любой тип, включая Button/cmdBar): default/skip/drag. +def emit_common_element_props(lines, el, indent): + if el.get('defaultItem') is True: + lines.append(f"{indent}true") + if 'skipOnInput' in el and el['skipOnInput'] is not None: + siv = 'true' if el['skipOnInput'] is True else 'false' + lines.append(f"{indent}{siv}") + # EnableStartDrag — фактическое значение (платформа эмитит и явный false, напр. SpreadSheet) + if el.get('enableStartDrag') is not None: + lines.append(f'{indent}{"true" if el["enableStartDrag"] else "false"}') + if el.get('fileDragMode'): + lines.append(f"{indent}{el['fileDragMode']}") + # Cell-свойства поля в таблице (общие для Input/Label/Picture/CheckBox): захват «как есть» + for key, tag in (('showInHeader', 'ShowInHeader'), ('showInFooter', 'ShowInFooter'), ('autoCellHeight', 'AutoCellHeight')): + if el.get(key) is not None: + lines.append(f'{indent}<{tag}>{"true" if el[key] else "false"}') + # Динамический заголовок колонки-группы из данных (HeaderDataPath) — перед HeaderHorizontalAlign (порядок XSD) + if el.get('headerDataPath'): + lines.append(f"{indent}{esc_xml(str(el['headerDataPath']))}") + if el.get('footerHorizontalAlign'): + lines.append(f"{indent}{el['footerHorizontalAlign']}") + if el.get('headerHorizontalAlign'): + lines.append(f"{indent}{el['headerHorizontalAlign']}") + # Формат заголовка колонки-группы (ML-текст) — после HeaderHorizontalAlign (порядок XSD) + if el.get('headerFormat'): + emit_mltext(lines, indent, 'HeaderFormat', el['headerFormat']) + + +def emit_picture_ref(lines, val, pic_tag, indent): + """Картинка-ссылка с прозрачностью (HeaderPicture/FooterPicture/ValuesPicture/Page Picture). + Платформа ВСЕГДА эмитит → пишем всегда (false по умолчанию). + Значение: скаляр (Ref) ИЛИ объект {src, loadTransparent, transparentPixel}. + src с префиксом "abs:" → встроенная картинка ; иначе .""" + if not val: + return + tpx = None + if isinstance(val, str): + src, lt = val, False + else: + src = val.get('src') + lt = val.get('loadTransparent') is True + tpx = val.get('transparentPixel') + if not src: + return + src_str = str(src) + lines.append(f"{indent}<{pic_tag}>") + if src_str.startswith('abs:'): + lines.append(f"{indent}\t{esc_xml(src_str[4:])}") + else: + lines.append(f"{indent}\t{esc_xml(src_str)}") + lines.append(f'{indent}\t{"true" if lt else "false"}') + if tpx: + lines.append(f'{indent}\t') + lines.append(f"{indent}") + + +def emit_column_pics(lines, el, indent): + """Картинки заголовка/подвала колонки поля — по схеме сразу после , + перед тип-специфичными элементами и layout (порядок XDTO строгий именно здесь).""" + emit_picture_ref(lines, el.get('headerPicture'), 'HeaderPicture', indent) + emit_picture_ref(lines, el.get('footerPicture'), 'FooterPicture', indent) + + +def emit_command_picture(lines, pic, elem_lt, indent): + """ кнопки/попапа/команды. Дефолт LoadTransparent=true, отклонение false + (обратная конвенция относительно header/values-картинок). Прощающий ввод: + принимает скаляр (Ref) ИЛИ объект {src, loadTransparent} — на случай если модель + опишет картинку объектно по аналогии с headerPicture. elem_lt — legacy + элемент-уровневый ключ loadTransparent (если в объекте флаг не задан).""" + if not pic: + return + lt = None + tpx = None + if isinstance(pic, str): + src = pic + else: + src = pic.get('src') + if pic.get('loadTransparent') is not None: + lt = bool(pic.get('loadTransparent')) + tpx = pic.get('transparentPixel') + if not src: + return + if lt is None and elem_lt is not None: + lt = bool(elem_lt) + src_str = str(src) + lines.append(f'{indent}') + if src_str.startswith('abs:'): + lines.append(f'{indent}\t{esc_xml(src_str[4:])}') + else: + lines.append(f'{indent}\t{esc_xml(src_str)}') + lines.append(f'{indent}\t{"false" if lt is False else "true"}') + if tpx: + lines.append(f'{indent}\t') + lines.append(f'{indent}') + + +# --- Оформление элемента: цвета / шрифты / граница (зеркало form-compile.ps1 Emit-Appearance) --- +# Прямые свойства элемента (// + header/footer у полей). Ключи англ. +# camelCase 1:1 с тегами + приём рус. синонимов. Цвет — verbatim-строка (style:/web:/win:/#RRGGBB); +# шрифт — строка-ref/объект-атрибуты; граница — строка-ref/ +# объект {width,style}. Порядок тегов — XSD (профиль по базовому типу). +APPEARANCE_SPEC = { + 'titleTextColor': ('TitleTextColor', 'color'), + 'titleBackColor': ('TitleBackColor', 'color'), + 'titleFont': ('TitleFont', 'font'), + 'footerTextColor': ('FooterTextColor', 'color'), + 'footerBackColor': ('FooterBackColor', 'color'), + 'footerFont': ('FooterFont', 'font'), + 'textColor': ('TextColor', 'color'), + 'backColor': ('BackColor', 'color'), + 'borderColor': ('BorderColor', 'color'), + 'border': ('Border', 'border'), + 'font': ('Font', 'font'), +} +APPEARANCE_SYNONYMS = { + 'цветтекста': 'textColor', 'цветфона': 'backColor', 'цветрамки': 'borderColor', + 'цветтекстазаголовка': 'titleTextColor', 'цветфоназаголовка': 'titleBackColor', 'шрифтзаголовка': 'titleFont', + 'цветтекстаподвала': 'footerTextColor', 'цветфонаподвала': 'footerBackColor', 'шрифтподвала': 'footerFont', + 'шрифт': 'font', 'рамка': 'border', +} +# Синонимы ключей-свойств: русские имена свойств 1С (как в Конфигураторе) → канон. англ. ключ. +# Ключи нормализованы (lowercase, без пробелов); сопоставление в emit_element тоже. Англ. ключ +# работает всегда (доп. слой прощающего ввода). Видимость/Доступность НЕ включаем (hidden/disabled инвертирован). +PROP_SYNONYMS = { + 'пометка': 'checked', + 'кнопкавыбора': 'choiceButton', 'кнопкаочистки': 'clearButton', 'кнопкарегулирования': 'spinButton', + 'кнопкавыпадающегосписка': 'dropListButton', 'кнопкасписковоговыбора': 'choiceListButton', + 'кнопкаоткрытия': 'openButton', 'кнопкапоумолчанию': 'defaultButton', + 'быстрыйвыбор': 'quickChoice', 'формавыбора': 'choiceForm', 'историявыборапривводе': 'choiceHistoryOnInput', + 'выборгруппиэлементов': 'choiceFoldersAndItems', 'фиксациявтаблице': 'fixingInTable', + 'путькданнымподвала': 'footerDataPath', 'автоотметканезаполненного': 'markIncomplete', + 'многострочныйрежим': 'multiLine', 'режимпароля': 'passwordMode', 'переноспословам': 'wrap', + 'расположениезаголовка': 'titleLocation', 'пропускатьпривводе': 'skipOnInput', + 'заголовок': 'title', 'ширина': 'width', 'высота': 'height', 'подсказкаввода': 'inputHint', +} +APP_ORDER_FIELD =['titleTextColor', 'titleBackColor', 'titleFont', 'footerTextColor', 'footerBackColor', 'footerFont', 'textColor', 'backColor', 'borderColor', 'border', 'font'] +APP_ORDER_DECORATION = ['textColor', 'font', 'backColor', 'borderColor', 'border'] +APP_ORDER_BUTTON = ['textColor', 'backColor', 'borderColor', 'font'] + + +def get_appearance_value(el, canonical): + if not isinstance(el, dict): + return None + if canonical in el: + return el[canonical] + lowmap = {k.lower(): k for k in el.keys()} + if canonical.lower() in lowmap: + return el[lowmap[canonical.lower()]] + for syn, canon in APPEARANCE_SYNONYMS.items(): + if canon == canonical and syn in lowmap: + return el[lowmap[syn]] + return None + + +def emit_font_tag(lines, tag, val, indent): + if isinstance(val, str): + lines.append(f'{indent}<{tag} ref="{esc_xml(val)}" kind="StyleItem"/>') + return + attrs = [] + for a in ('ref', 'faceName', 'height', 'bold', 'italic', 'underline', 'strikeout', 'kind', 'scale'): + if a in val and val[a] is not None: + v = val[a] + if isinstance(v, bool): + v = 'true' if v else 'false' + attrs.append(f'{a}="{esc_xml(str(v))}"') + lines.append(f'{indent}<{tag} {" ".join(attrs)}/>') + + +def emit_border_tag(lines, val, indent): + if isinstance(val, str): + lines.append(f'{indent}') + return + if val.get('ref'): + lines.append(f'{indent}') + return + width = val['width'] if val.get('width') is not None else 1 + style = str(val['style']) if 'style' in val else None + lines.append(f'{indent}') + if style: + lines.append(f'{indent}\t{esc_xml(style)}') + lines.append(f'{indent}') + + +# ───────────────────────────────────────────────────────────────────────────── +# Planner design-time — зеркало Emit-PlannerSettings (ps1). +PLANNER_NS = 'http://v8.1c.ru/8.3/data/planner' +CHART_NS = 'http://v8.1c.ru/8.2/data/chart' + + +def _pl_get(o, k, default=None): + if isinstance(o, dict) and o.get(k) is not None: + return o[k] + return default + + +def _pl_bool(v): + if isinstance(v, bool): + return 'true' if v else 'false' + if str(v) == 'True': + return 'true' + if str(v) == 'False': + return 'false' + return str(v) + + +def emit_planner_color(lines, tag, o, key, ind): + lines.append(f'{ind}{esc_xml(str(_pl_get(o, key, "auto")))}') + + +def emit_planner_text(lines, tag, v, ind): + if v is None or str(v) == '': + lines.append(f'{ind}') + else: + lines.append(f'{ind}{esc_xml(str(v))}') + + +_PLANNER_REF_RE = re.compile( + r'^(Enum|Catalog|Document|ChartOfAccounts|ChartOfCalculationTypes|ChartOfCharacteristicTypes|ExchangePlan|BusinessProcess|Task)\.' + r'|\.EnumValue\.|EmptyRef$' + r'|^(Перечисление|Справочник|Документ|ПланСчетов|ПланВидовХарактеристик|ПланВидовРасчета|ПланОбмена|БизнесПроцесс|Задача)\.') + + +def test_planner_ref(v): + return bool(_PLANNER_REF_RE.search(str(v))) + + +def emit_planner_value(lines, v, ind): + if v is None or str(v) == '': + lines.append(f'{ind}') + return + t = 'xr:DesignTimeRef' if test_planner_ref(v) else 'xs:string' + lines.append(f'{ind}{esc_xml(str(v))}') + + +def emit_planner_font(lines, o, ind): + f = _pl_get(o, 'font') + if f is None: + lines.append(f'{ind}') + return + emit_font_tag(lines, 'pl:font', f, ind) + + +def emit_planner_border(lines, o, ind, key='border'): + b = _pl_get(o, key) + bw = _pl_get(b, 'width', 1) if b else 1 + bs = _pl_get(b, 'style', 'Single') if b else 'Single' + lines.append(f'{ind}') + lines.append(f'{ind}\t{esc_xml(str(bs))}') + lines.append(f'{ind}') + + +def emit_planner_level(lines, lv, cns, ind): + li = f'{ind}\t' + lines.append(f'{ind}') + lines.append(f'{li}{esc_xml(str(_pl_get(lv, "measure", "Hour")))}') + lines.append(f'{li}{_pl_get(lv, "interval", 1)}') + lines.append(f'{li}{_pl_bool(_pl_get(lv, "show", True))}') + line = _pl_get(lv, 'line') + lw = _pl_get(line, 'width', 1) if line else 1 + lg = _pl_get(line, 'gap', False) if line else False + lst = _pl_get(line, 'style', 'Solid') if line else 'Solid' + lines.append(f'{li}') + lines.append(f'{li}\t{esc_xml(str(lst))}') + lines.append(f'{li}') + lines.append(f'{li}{esc_xml(str(_pl_get(lv, "scaleColor", "auto")))}') + lines.append(f'{li}{esc_xml(str(_pl_get(lv, "dayFormatRule", "MonthDayWeekDay")))}') + fmt = _pl_get(lv, 'format') + if fmt is None: + fmt = {'#': 'DF="HH:mm"', 'ru': 'DF="HH:mm"'} + lines.append(f'{li}') + emit_ml_items(lines, f'{li}\t', fmt) + lines.append(f'{li}') + labels = _pl_get(lv, 'labels') + ticks = _pl_get(labels, 'ticks', 0) if labels else 0 + lines.append(f'{li}') + lines.append(f'{li}\t{ticks}') + lines.append(f'{li}') + lines.append(f'{li}{esc_xml(str(_pl_get(lv, "backColor", "auto")))}') + lines.append(f'{li}{esc_xml(str(_pl_get(lv, "textColor", "auto")))}') + lines.append(f'{li}{_pl_bool(_pl_get(lv, "showPereodicalLabels", True))}') + lines.append(f'{ind}') + + +def emit_planner_timescale(lines, ts, ind): + cns = CHART_NS + ci = f'{ind}\t' + lines.append(f'{ind}') + placement = _pl_get(ts, 'placement', 'Left') if ts else 'Left' + lines.append(f'{ci}{esc_xml(str(placement))}') + levels = _pl_get(ts, 'levels', []) if ts else [] + if not levels: + levels = [None] + for lv in levels: + emit_planner_level(lines, lv, cns, ci) + transp = _pl_get(ts, 'transparent', False) if ts else False + lines.append(f'{ci}{_pl_bool(transp)}') + tbc = _pl_get(ts, 'backColor', 'auto') if ts else 'auto' + ttc = _pl_get(ts, 'textColor', 'auto') if ts else 'auto' + tcl = _pl_get(ts, 'currentLevel', 0) if ts else 0 + lines.append(f'{ci}{esc_xml(str(tbc))}') + lines.append(f'{ci}{esc_xml(str(ttc))}') + lines.append(f'{ci}{tcl}') + lines.append(f'{ind}') + + +def emit_planner_item(lines, it, ind): + lines.append(f'{ind}') + ii = f'{ind}\t' + emit_planner_value(lines, _pl_get(it, 'value'), ii) + emit_planner_text(lines, 'text', _pl_get(it, 'text', ''), ii) + emit_planner_text(lines, 'tooltip', _pl_get(it, 'tooltip', ''), ii) + lines.append(f'{ii}{_pl_get(it, "begin", "0001-01-01T00:00:00")}') + lines.append(f'{ii}{_pl_get(it, "end", "0001-01-01T00:00:00")}') + emit_planner_color(lines, 'borderColor', it, 'borderColor', ii) + emit_planner_color(lines, 'backColor', it, 'backColor', ii) + emit_planner_color(lines, 'textColor', it, 'textColor', ii) + emit_planner_font(lines, it, ii) + lines.append(f'{ii}') + lines.append(f'{ii}{_pl_get(it, "replacementDate", "0001-01-01T00:00:00")}') + lines.append(f'{ii}{_pl_bool(_pl_get(it, "deleted", False))}') + iid = _pl_get(it, 'id') + if iid is None: + import uuid + iid = str(uuid.uuid4()) + lines.append(f'{ii}{iid}') + lines.append(f'{ii}{_pl_bool(_pl_get(it, "textFormatted", False))}') + emit_planner_border(lines, it, ii, 'border') + lines.append(f'{ii}{esc_xml(str(_pl_get(it, "editMode", "EnableEdit")))}') + lines.append(f'{ind}') + + +def emit_planner_dim_element(lines, el, ind): + lines.append(f'{ind}') + ii = f'{ind}\t' + emit_planner_value(lines, _pl_get(el, 'value'), ii) + emit_planner_text(lines, 'text', _pl_get(el, 'text', ''), ii) + emit_planner_color(lines, 'borderColor', el, 'borderColor', ii) + emit_planner_color(lines, 'backColor', el, 'backColor', ii) + emit_planner_color(lines, 'textColor', el, 'textColor', ii) + emit_planner_font(lines, el, ii) + for sub in _pl_get(el, 'elements', []): + emit_planner_dim_element(lines, sub, ii) + lines.append(f'{ii}{_pl_bool(_pl_get(el, "showOnlySubordinatesAreas", True))}') + lines.append(f'{ii}{_pl_bool(_pl_get(el, "textFormatted", False))}') + lines.append(f'{ind}') + + +def emit_planner_dimension(lines, d, ind): + lines.append(f'{ind}') + di = f'{ind}\t' + emit_planner_value(lines, _pl_get(d, 'value'), di) + emit_planner_text(lines, 'text', _pl_get(d, 'text', ''), di) + emit_planner_color(lines, 'borderColor', d, 'borderColor', di) + emit_planner_color(lines, 'backColor', d, 'backColor', di) + emit_planner_color(lines, 'textColor', d, 'textColor', di) + emit_planner_font(lines, d, di) + for el in _pl_get(d, 'elements', []): + emit_planner_dim_element(lines, el, di) + lines.append(f'{di}{_pl_bool(_pl_get(d, "textFormatted", False))}') + lines.append(f'{ind}') + + +def emit_planner_settings(lines, pl, ind): + lines.append(f'{ind}') + si = f'{ind}\t' + for it in _pl_get(pl, 'items', []): + emit_planner_item(lines, it, si) + for d in _pl_get(pl, 'dimensions', []): + emit_planner_dimension(lines, d, si) + emit_planner_color(lines, 'borderColor', pl, 'borderColor', si) + emit_planner_color(lines, 'backColor', pl, 'backColor', si) + emit_planner_color(lines, 'textColor', pl, 'textColor', si) + emit_planner_color(lines, 'lineColor', pl, 'lineColor', si) + emit_planner_font(lines, pl, si) + lines.append(f'{si}{_pl_get(pl, "beginOfRepresentationPeriod", "0001-01-01T00:00:00")}') + lines.append(f'{si}{_pl_get(pl, "endOfRepresentationPeriod", "0001-01-01T00:00:00")}') + lines.append(f'{si}{_pl_bool(_pl_get(pl, "alignElementsOfTimeScale", True))}') + lines.append(f'{si}{_pl_bool(_pl_get(pl, "displayTimeScaleWrapHeaders", True))}') + lines.append(f'{si}{_pl_bool(_pl_get(pl, "displayWrapHeaders", True))}') + wfmt = _pl_get(pl, 'timeScaleWrapHeadersFormat') + if wfmt is None: + wfmt = {'#': 'DLF="DD"', 'ru': 'DLF="DD"'} + emit_mltext(lines, si, 'pl:timeScaleWrapHeadersFormat', wfmt) + lines.append(f'{si}{esc_xml(str(_pl_get(pl, "periodicVariantUnit", "Day")))}') + lines.append(f'{si}{_pl_get(pl, "periodicVariantRepetition", 1)}') + lines.append(f'{si}{_pl_get(pl, "timeScaleWrapBeginIndent", 0)}') + lines.append(f'{si}{_pl_get(pl, "timeScaleWrapEndIndent", 0)}') + emit_planner_timescale(lines, _pl_get(pl, 'timeScale'), si) + period = _pl_get(pl, 'period') + if period: + lines.append(f'{si}') + lines.append(f'{si}\t{_pl_get(period, "begin", "0001-01-01T00:00:00")}') + lines.append(f'{si}\t{_pl_get(period, "end", "0001-01-01T00:00:00")}') + lines.append(f'{si}') + lines.append(f'{si}{_pl_bool(_pl_get(pl, "displayCurrentDate", True))}') + lines.append(f'{si}{esc_xml(str(_pl_get(pl, "itemsTimeRepresentation", "BeginTime")))}') + lines.append(f'{si}{esc_xml(str(_pl_get(pl, "itemsBehaviorWhenSpaceInsufficient", "CollapseItems")))}') + lines.append(f'{si}{_pl_bool(_pl_get(pl, "autoMinColumnWidth", True))}') + lines.append(f'{si}{_pl_bool(_pl_get(pl, "autoMinRowHeight", True))}') + lines.append(f'{si}{_pl_get(pl, "minColumnWidth", 0)}') + lines.append(f'{si}{_pl_get(pl, "minRowHeight", 0)}') + lines.append(f'{si}{esc_xml(str(_pl_get(pl, "fixDimensionsHeader", "auto")))}') + lines.append(f'{si}{esc_xml(str(_pl_get(pl, "fixTimeScaleHeader", "auto")))}') + emit_planner_border(lines, pl, si, 'border') + lines.append(f'{si}{esc_xml(str(_pl_get(pl, "newItemsTextType", "String")))}') + lines.append(f'{ind}') + + +# ───────────────────────────────────────────────────────────────────────────── +# Chart design-time — генерик-эмиттер (зеркало +# Build-ChartNode декомпилятора + Emit-ChartNode ps1). +CHART_ML_FIELDS = {'title', 'lbFormat', 'lbpFormat', 'vsFormat', 'dtFormat', 'dataSourceDescription', 'labelFormat', 'text'} +CHART_ATTR_FIELDS = {'gaugeQualityBands'} +CHART_FONT_KEYS = ('ref', 'faceName', 'height', 'bold', 'italic', 'underline', 'strikeout', 'kind', 'scale') + + +def emit_chart_node(lines, name, val, ind): + if name in CHART_ML_FIELDS: + if val is None or str(val) == '': + lines.append(f'{ind}') + return + lines.append(f'{ind}') + emit_ml_items(lines, f'{ind}\t', val) + lines.append(f'{ind}') + return + if isinstance(val, list): + for e in val: + emit_chart_node(lines, name, e, ind) + return + if isinstance(val, dict): + keys = list(val.keys()) + if name in CHART_ATTR_FIELDS: + attrs = ' '.join(f'{k}="{esc_xml(_pl_bool(val[k]) if isinstance(val[k], bool) else str(val[k]))}"' for k in keys) + lines.append(f'{ind}') + return + if 'gap' in val: + lines.append(f'{ind}') + lines.append(f'{ind}\t{esc_xml(str(val.get("style")))}') + lines.append(f'{ind}') + return + if 'style' in val and 'width' in val: + lines.append(f'{ind}') + lines.append(f'{ind}\t{esc_xml(str(val.get("style")))}') + lines.append(f'{ind}') + return + if any(fk in val for fk in CHART_FONT_KEYS): + attrs = ' '.join(f'{fk}="{esc_xml(_pl_bool(val[fk]) if isinstance(val[fk], bool) else str(val[fk]))}"' for fk in CHART_FONT_KEYS if fk in val) + lines.append(f'{ind}') + return + if not keys: + lines.append(f'{ind}') + return + lines.append(f'{ind}') + for k in keys: + emit_chart_node(lines, k, val[k], f'{ind}\t') + lines.append(f'{ind}') + return + if val is None or str(val) == '': + lines.append(f'{ind}') + return + if isinstance(val, bool): + lines.append(f'{ind}{_pl_bool(val)}') + return + lines.append(f'{ind}{esc_xml(str(val))}') + + +def emit_chart_settings(lines, chart, ind, ctype='d4p1:Chart'): + lines.append(f'{ind}') + for k in list(chart.keys()): + emit_chart_node(lines, k, chart[k], f'{ind}\t') + lines.append(f'{ind}') + + +def emit_appearance(lines, el, indent, profile='field'): + if not isinstance(el, dict): + return + order = {'decoration': APP_ORDER_DECORATION, 'button': APP_ORDER_BUTTON}.get(profile, APP_ORDER_FIELD) + for key in order: + val = get_appearance_value(el, key) + if val is None or (isinstance(val, str) and val == ''): + continue + tag, kind = APPEARANCE_SPEC[key] + if kind == 'color': + lines.append(f'{indent}<{tag}>{esc_xml(str(val))}') + elif kind == 'font': + emit_font_tag(lines, tag, val, indent) + else: + emit_border_tag(lines, val, indent) + + +# Простые скаляры элемента (pass-through, зеркало $script:genericScalars). kind bool/value. +GENERIC_SCALARS = [ + ('VerticalAlign', 'verticalAlign', 'value'), + ('ThroughAlign', 'throughAlign', 'value'), + ('EnableContentChange', 'enableContentChange', 'bool'), + ('PictureSize', 'pictureSize', 'value'), + ('TitleHeight', 'titleHeight', 'value'), + ('ChildItemsWidth', 'childItemsWidth', 'value'), + ('ShowLeftMargin', 'showLeftMargin', 'bool'), + ('CellHyperlink', 'cellHyperlink', 'bool'), + ('ViewMode', 'viewMode', 'value'), + ('VerticalScrollBar', 'verticalScrollBar', 'value'), + ('RowInputMode', 'rowInputMode', 'value'), + ('Mask', 'mask', 'value'), + ('CreateButton', 'createButton', 'bool'), + ('FixingInTable', 'fixingInTable', 'value'), + ('VerticalSpacing', 'verticalSpacing', 'value'), + # Spec-fields (document/gauge) - type-specific enum/bool scalars pass-through + ('HorizontalScrollBar', 'horizontalScrollBar', 'value'), + ('ViewScalingMode', 'viewScalingMode', 'value'), + ('Output', 'output', 'value'), + ('SelectionShowMode', 'selectionShowMode', 'value'), + ('PointerType', 'pointerType', 'value'), + ('DrawingSelectionShowMode', 'drawingSelectionShowMode', 'value'), + ('WarningOnEditRepresentation', 'warningOnEditRepresentation', 'value'), + ('MarkingAppearance', 'markingAppearance', 'value'), + ('Protection', 'protection', 'bool'), + ('Edit', 'edit', 'bool'), + ('ShowGrid', 'showGrid', 'bool'), + ('ShowGroups', 'showGroups', 'bool'), + ('ShowHeaders', 'showHeaders', 'bool'), + ('ShowRowAndColumnNames', 'showRowAndColumnNames', 'bool'), + ('ShowCellNames', 'showCellNames', 'bool'), + ('ShowPercent', 'showPercent', 'bool'), + # Report-form контекст: интервал группы / представление кнопки в контекстном меню / детальное представление настройки таблицы + ('HorizontalSpacing', 'horizontalSpacing', 'value'), + ('RepresentationInContextMenu', 'representationInContextMenu', 'value'), + ('SettingsNamedItemDetailedRepresentation', 'settingsNamedItemDetailedRepresentation', 'bool'), + # Хвост: высота элемента списка (radio) / ширина выпадающего списка (input) + ('ItemHeight', 'itemHeight', 'value'), + ('DropListWidth', 'dropListWidth', 'value'), + # Хвост CI-форм: динамический заголовок (Page/Group) / расширенное ред. (input) / высота таблицы по строкам + ('TitleDataPath', 'titleDataPath', 'value'), + ('ExtendedEdit', 'extendedEdit', 'bool'), + ('MaxRowsCount', 'maxRowsCount', 'value'), + ('AutoMaxRowsCount', 'autoMaxRowsCount', 'bool'), + ('HeightControlVariant', 'heightControlVariant', 'value'), + ('EditTextUpdate', 'editTextUpdate', 'value'), + # Корпусный хвост: свёртка группы / форма попапа / авто-добавление / выделение отрицательных / + # нач. позиция списка / высота списка выбора / три состояния / прокрутка страницы при сжатии + ('ControlRepresentation', 'controlRepresentation', 'value'), + ('ShapeRepresentation', 'shapeRepresentation', 'value'), + ('AutoAddIncomplete', 'autoAddIncomplete', 'bool'), + ('MarkNegatives', 'markNegatives', 'bool'), + ('InitialListView', 'initialListView', 'value'), + ('ChoiceListHeight', 'choiceListHeight', 'value'), + ('ThreeState', 'threeState', 'bool'), + ('ScrollOnCompress', 'scrollOnCompress', 'bool'), + # Сочетание клавиш — общее свойство (команда — отдельный путь) + ('Shortcut', 'shortcut', 'value'), + # Батч простых скаляров (input/radio/group/picDecoration/button; Table-специфичные — отдельно) + ('IncompleteChoiceMode', 'incompleteChoiceMode', 'value'), + ('EqualColumnsWidth', 'equalColumnsWidth', 'bool'), + ('ChildrenAlign', 'childrenAlign', 'value'), + ('ImageScale', 'imageScale', 'value'), + ('Zoomable', 'zoomable', 'bool'), + ('Shape', 'shape', 'value'), + ('PictureLocation', 'pictureLocation', 'value'), + # Равная ширина элементов (check/radio) / высота заголовка пункта (radio) + ('EqualItemsWidth', 'equalItemsWidth', 'bool'), + ('ItemTitleHeight', 'itemTitleHeight', 'value'), + # Спец-режим ввода текста (input, моб.: Email/PhoneNumber/...) — листовой enum-скаляр + ('SpecialTextInputMode', 'specialTextInputMode', 'value'), +] + + +def emit_generic_scalars(lines, el, indent): + for tag, key, kind in GENERIC_SCALARS: + if key not in el or el[key] is None: + continue + if kind == 'bool': + lines.append(f'{indent}<{tag}>{"true" if el[key] else "false"}') + else: + v = str(el[key]) + if v == '': + continue + lines.append(f'{indent}<{tag}>{esc_xml(v)}') + + +def emit_layout(lines, el, indent, skip_height=False, multi_line_default=False): + # Общие layout-свойства — применимы ко всем элементам. Порядок согласован + # с историческим выводом input/label, чтобы не сдвигать существующие снапшоты. + # skip_height: подавить (зарезервирован; Table теперь эмитит generic-ом + свой ). + # multi_line_default: input без явного autoMaxWidth при multiLine → AutoMaxWidth=false. + # CommandSet (отключённые команды редактора) — общее свойство поля; в схеме рано (после TitleLocation). + if el.get('excludedCommands') and len(el['excludedCommands']) > 0: + lines.append(f'{indent}') + for cmd in el['excludedCommands']: + lines.append(f'{indent}\t{cmd}') + lines.append(f'{indent}') + emit_common_element_props(lines, el, indent) + if 'autoMaxWidth' in el: + if el.get('autoMaxWidth') is False: + lines.append(f"{indent}false") + elif multi_line_default: + lines.append(f"{indent}false") + if el.get('maxWidth') is not None: + lines.append(f"{indent}{el['maxWidth']}") + if el.get('autoMaxHeight') is False: + lines.append(f"{indent}false") + if el.get('maxHeight') is not None: + lines.append(f"{indent}{el['maxHeight']}") + if el.get('width'): + lines.append(f"{indent}{el['width']}") + if not skip_height and el.get('height'): + lines.append(f"{indent}{el['height']}") + if el.get('horizontalStretch') is not None: + lines.append(f'{indent}{"true" if el["horizontalStretch"] else "false"}') + if el.get('verticalStretch') is not None: + lines.append(f'{indent}{"true" if el["verticalStretch"] else "false"}') + if el.get('groupHorizontalAlign'): + lines.append(f"{indent}{el['groupHorizontalAlign']}") + if el.get('groupVerticalAlign'): + lines.append(f"{indent}{el['groupVerticalAlign']}") + if el.get('horizontalAlign'): + lines.append(f"{indent}{el['horizontalAlign']}") + emit_generic_scalars(lines, el, indent) + + +def title_from_name(name): + """СуммаДокумента → 'Сумма документа'. НДСВключен → 'НДС включен'.""" + if not name: + return '' + s = re.sub(r'([А-ЯA-Z])([А-ЯA-Z][а-яa-z])', r'\1 \2', name) + s = re.sub(r'([а-яa-z0-9])([А-ЯA-Z])', r'\1 \2', s) + parts = s.split(' ') + if not parts: + return s + out = [parts[0]] + for p in parts[1:]: + out.append(p if (len(p) > 1 and p.isupper()) else p.lower()) + return ' '.join(out) + + +def emit_title(lines, el, name, indent, auto=False): + # Нет ключа title → авто-вывод из имени (помощь модели). + # Явный title "" (или None) → подавить. Явный непустой → как есть. + if 'title' in el: + if el.get('title'): + emit_mltext(lines, indent, 'Title', el['title']) + elif auto and name: + emit_mltext(lines, indent, 'Title', title_from_name(name)) + # ToolTip элемента (всплывающая подсказка) — по схеме сразу после Title. + if el.get('tooltip'): + emit_mltext(lines, indent, 'ToolTip', el['tooltip']) + # ToolTipRepresentation — режим показа подсказки (None/Button/ShowBottom/…), после ToolTip. + if el.get('tooltipRepresentation'): + lines.append(f'{indent}{el["tooltipRepresentation"]}') + + +_TITLE_LOC_MAP = {'none': 'None', 'left': 'Left', 'right': 'Right', 'top': 'Top', 'bottom': 'Bottom', 'auto': 'Auto'} + + +def map_title_loc(v): + return _TITLE_LOC_MAP.get(str(v).lower(), str(v)) + + +def emit_title_location(lines, el, indent, smart_default): + # Нет ключа → умный дефолт (Right/None), эмитится. "" → подавить (дефолт платформы). + # Значение → эмитить с маппингом регистра. + if 'titleLocation' in el: + if el.get('titleLocation'): + lines.append(f"{indent}{map_title_loc(el['titleLocation'])}") + elif smart_default: + lines.append(f"{indent}{smart_default}") + + +# --- Type emitter --- + +V8_TYPES = { + "ValueTable": "v8:ValueTable", + "ValueTree": "v8:ValueTree", + "ValueList": "v8:ValueListType", + "TypeDescription": "v8:TypeDescription", + "Universal": "v8:Universal", + "FixedArray": "v8:FixedArray", + "FixedStructure": "v8:FixedStructure", +} + +UI_TYPES = { + "FormattedString": "v8ui:FormattedString", + "Picture": "v8ui:Picture", + "Color": "v8ui:Color", + "Font": "v8ui:Font", +} + +DCS_MAP = { + "DataCompositionSettings": "dcsset:DataCompositionSettings", + "DataCompositionSchema": "dcssch:DataCompositionSchema", + "DataCompositionComparisonType": "dcscor:DataCompositionComparisonType", +} + +CFG_REF_PATTERN = re.compile( + r'^(CatalogRef|CatalogObject|DocumentRef|DocumentObject|EnumRef|' + r'ChartOfAccountsRef|ChartOfAccountsObject|ChartOfCharacteristicTypesRef|ChartOfCharacteristicTypesObject|' + r'ChartOfCalculationTypesRef|ChartOfCalculationTypesObject|' + r'ExchangePlanRef|ExchangePlanObject|BusinessProcessRef|BusinessProcessObject|TaskRef|TaskObject|' + r'InformationRegisterRecordSet|InformationRegisterRecordManager|' + r'AccumulationRegisterRecordSet|AccountingRegisterRecordSet|' + r'ConstantsSet|DataProcessorObject|ReportObject)\.' +) + +KNOWN_INVALID_TYPES = { + 'FormDataStructure': 'Runtime type. Use object type without cfg: prefix (e.g. CatalogObject.Контрагенты, DocumentObject.Приход)', + 'FormDataCollection': 'Runtime type. Use ValueTable', + 'FormDataTree': 'Runtime type. Use ValueTree', + 'FormDataTreeItem': 'Runtime type, not valid in XML', + 'FormDataCollectionItem': 'Runtime type, not valid in XML', + 'FormGroup': 'UI element type, not a data type', + 'FormField': 'UI element type, not a data type', + 'FormButton': 'UI element type, not a data type', + 'FormDecoration': 'UI element type, not a data type', + 'FormTable': 'UI element type, not a data type', +} + + +_FORM_TYPE_SYNONYMS = { + "строка": "string", "число": "decimal", "булево": "boolean", + "дата": "date", "датавремя": "dateTime", + "number": "decimal", "bool": "boolean", + "справочникссылка": "CatalogRef", "справочникобъект": "CatalogObject", + "документссылка": "DocumentRef", "документобъект": "DocumentObject", + "перечислениессылка": "EnumRef", + "плансчетовссылка": "ChartOfAccountsRef", + "планвидовхарактеристикссылка": "ChartOfCharacteristicTypesRef", + "планвидоврасчётассылка": "ChartOfCalculationTypesRef", + "планвидоврасчетассылка": "ChartOfCalculationTypesRef", + "планобменассылка": "ExchangePlanRef", + "бизнеспроцессссылка": "BusinessProcessRef", + "задачассылка": "TaskRef", + "определяемыйтип": "DefinedType", + "характеристика": "Characteristic", + "любаяссылка": "AnyRef", + "любаяссылкаиб": "AnyIBRef", + # Платформенные v8-типы (forgiving: англ. без префикса + рус.) → каноничный с префиксом v8: + "standardperiod": "v8:StandardPeriod", + "стандартныйпериод": "v8:StandardPeriod", + "standardbeginningdate": "v8:StandardBeginningDate", + "стандартнаядатаначала": "v8:StandardBeginningDate", + "uuid": "v8:UUID", + "уникальныйидентификатор": "v8:UUID", + "списокзначений": "ValueList", +} + + +def resolve_type_str(type_str): + if not type_str: + return type_str + # Lenient: strip leading cfg: prefix if user passed it (canonical form is without prefix) + if type_str.startswith('cfg:'): + type_str = type_str[4:] + m = re.match(r'^([^(]+)\((.+)\)$', type_str) + if m: + base, params = m.group(1).strip(), m.group(2) + r = _FORM_TYPE_SYNONYMS.get(base.lower()) + return f"{r}({params})" if r else type_str + if '.' in type_str: + i = type_str.index('.') + prefix, suffix = type_str[:i], type_str[i:] + r = _FORM_TYPE_SYNONYMS.get(prefix.lower()) + return f"{r}{suffix}" if r else type_str + r = _FORM_TYPE_SYNONYMS.get(type_str.lower()) + return r if r else type_str + + +def emit_single_type(lines, type_str, indent): + type_str = resolve_type_str(type_str) + # TypeId — тип, заданный глобальным стабильным GUID (, не ). Платформа так + # сериализует типы, чьё имя в этом контексте недоступно (определяемые/характеристики). GUID + # глобально стабилен → эмитим verbatim (как роль-по-GUID). Маркер декомпилятора: 'typeid:GUID'. + m = re.match(r'^typeid:([0-9a-fA-F-]{36})$', type_str) + if m: + lines.append(f'{indent}{m.group(1)}') + return + # boolean + if type_str == 'boolean': + lines.append(f'{indent}xs:boolean') + return + + # string or string(N) or string(N,fixed) (AllowedLength: Variable дефолт / Fixed) + m = re.match(r'^string(\((\d+)(\s*,\s*(fixed|variable))?\))?$', type_str, re.IGNORECASE) + if m: + length = m.group(2) if m.group(2) else '0' + al = 'Fixed' if (m.group(4) and m.group(4).lower() == 'fixed') else 'Variable' + lines.append(f'{indent}xs:string') + lines.append(f'{indent}') + lines.append(f'{indent}\t{length}') + lines.append(f'{indent}\t{al}') + lines.append(f'{indent}') + return + + # decimal(D,F) or decimal(D,F,nonneg) + m = re.match(r'^decimal\((\d+),(\d+)(,nonneg)?\)$', type_str) + if m: + digits = m.group(1) + fraction = m.group(2) + sign = 'Nonnegative' if m.group(3) else 'Any' + lines.append(f'{indent}xs:decimal') + lines.append(f'{indent}') + lines.append(f'{indent}\t{digits}') + lines.append(f'{indent}\t{fraction}') + lines.append(f'{indent}\t{sign}') + lines.append(f'{indent}') + return + + # date / dateTime / time + m = re.match(r'^(date|dateTime|time)$', type_str) + if m: + fractions_map = {'date': 'Date', 'dateTime': 'DateTime', 'time': 'Time'} + fractions = fractions_map[type_str] + lines.append(f'{indent}xs:dateTime') + lines.append(f'{indent}') + lines.append(f'{indent}\t{fractions}') + lines.append(f'{indent}') + return + + # V8 types + if type_str in V8_TYPES: + lines.append(f'{indent}{V8_TYPES[type_str]}') + return + + # UI types + if type_str in UI_TYPES: + lines.append(f'{indent}{UI_TYPES[type_str]}') + return + + # DCS types + if type_str.startswith('DataComposition'): + if type_str in DCS_MAP: + lines.append(f'{indent}{DCS_MAP[type_str]}') + return + + # Голые конфигурационные типы (cfg: без .Имя): дин-список, набор констант, общий объект отчёта. + # Корпус (acc+erp 8.3.24): DynamicList 5205, ConstantsSet 103, ReportObject 10. + if type_str in ('DynamicList', 'ConstantsSet', 'ReportObject'): + lines.append(f'{indent}cfg:{type_str}') + return + + # TypeSet (набор типов) → : определяемый тип / характеристика (именованные) + # + «любая ссылка вида» (голый ref-вид без .Имя). Развязка с обычным типом — по наличию точки. + if re.match(r'^(DefinedType|Characteristic)\.', type_str): + lines.append(f'{indent}cfg:{type_str}') + return + if re.match(r'^(AnyRef|AnyIBRef|CatalogRef|DocumentRef|EnumRef|ExchangePlanRef|TaskRef|BusinessProcessRef|ChartOfAccountsRef|ChartOfCharacteristicTypesRef|ChartOfCalculationTypesRef)$', type_str): + lines.append(f'{indent}cfg:{type_str}') + return + + # cfg: references + if CFG_REF_PATTERN.match(type_str): + lines.append(f'{indent}cfg:{type_str}') + return + + # Спец-типы платформы с собственным namespace (объявляется ЛОКАЛЬНО на ). + # Префикс d5p1 неоднозначен (5 разных URI), поэтому маппинг по полному значению типа. + # К таким типам привязаны спец-поля: mxl→SpreadSheetDocumentField, fd→FormattedDocumentField, + # d5p1:TextDocument→TextDocumentField, pdfdoc→PDF, pl→Planner, chart/geo/graphscheme/data-analysis. + special_type_ns = { + "mxl:SpreadsheetDocument": "http://v8.1c.ru/8.2/data/spreadsheet", + "fd:FormattedDocument": "http://v8.1c.ru/8.2/data/formatted-document", + "d5p1:TextDocument": "http://v8.1c.ru/8.1/data/txtedt", + "d5p1:Chart": "http://v8.1c.ru/8.2/data/chart", + "d5p1:GanttChart": "http://v8.1c.ru/8.2/data/chart", + "d5p1:Dendrogram": "http://v8.1c.ru/8.2/data/chart", + "d5p1:FlowchartContextType": "http://v8.1c.ru/8.2/data/graphscheme", + "d5p1:DataAnalysisTimeIntervalUnitType": "http://v8.1c.ru/8.2/data/data-analysis", + "d5p1:GeographicalSchema": "http://v8.1c.ru/8.2/data/geo", + "pdfdoc:PDFDocument": "http://v8.1c.ru/8.3/data/pdf", + "pl:Planner": "http://v8.1c.ru/8.3/data/planner", + } + if type_str in special_type_ns: + pref = type_str.split(':', 1)[0] + lines.append(f'{indent}{type_str}') + return + + # Fallback with validation + if type_str in KNOWN_INVALID_TYPES: + raise ValueError(f"Invalid form attribute type '{type_str}': {KNOWN_INVALID_TYPES[type_str]}") + # Платформенный тип с префиксом (v8:/v8ui:/xs:/dcs*:) — verbatim (напр. v8:UUID, v8:StandardPeriod). + if re.match(r'^(v8|v8ui|xs|ent|style|sys|web|win|dcs\w*):', type_str): + lines.append(f'{indent}{type_str}') + elif '.' in type_str: + lines.append(f'{indent}cfg:{type_str}') + else: + print(f"WARNING: Unrecognized bare type '{type_str}' — will be emitted without namespace prefix", file=sys.stderr) + lines.append(f'{indent}{type_str}') + + +def emit_type(lines, type_str, indent, tag="Type", tag_attrs=""): + # tag/tag_attrs — обёртка (по умолчанию ); для valueType ValueList вызывается с + # tag="Settings", tag_attrs=' xsi:type="v8:TypeDescription"'. + if not type_str: + lines.append(f'{indent}<{tag}{tag_attrs}/>') + return + + type_string = str(type_str) + parts = [p.strip() for p in re.split(r'[|+]', type_string)] + + lines.append(f'{indent}<{tag}{tag_attrs}>') + for part in parts: + emit_single_type(lines, part, f'{indent}\t') + lines.append(f'{indent}') + + +# --- Element emitters --- + +def emit_element(lines, el, indent, in_cmd_bar=False): + # Companion-панели (объект/массив-значение) → commandBar/contextMenu, до тип-синонимов. + normalize_panel_synonyms(el) + + # Silent synonyms: model often writes XML name or Russian (ПолеПереключателя/RadioButtonField → radio). + # commandBar/autoCommandBar/КоманднаяПанель → тип-элемент ТОЛЬКО при строковом значении (имя). + for src, dst in ELEMENT_TYPE_SYNONYMS.items(): + if src in el and dst not in el: + if src in STR_ONLY_TYPE_SYNONYMS and not isinstance(el[src], str): + continue + el[dst] = el.pop(src) + + # Синонимы ключей-свойств (русские имена 1С → канон. англ.). Case/space-insensitive. + # Канон побеждает: если задан и русский, и англ. ключ — англ. остаётся, русский отбрасываем. + for p_name in list(el.keys()): + norm = p_name.replace(' ', '').lower() + canon = PROP_SYNONYMS.get(norm) + if canon and p_name != canon: + val = el.pop(p_name) + if canon not in el: + el[canon] = val + + type_key = None + for key in TYPE_KEYS: + if el.get(key) is not None: + type_key = key + break + + if not type_key: + print("WARNING: Unknown element type, skipping", file=sys.stderr) + return + + # Validate known keys (внутренние маркеры на _ пропускаем). Оформление (цвета/шрифты/граница) + # проверяем против самих структур appearance — канонические ключи + forgiving-синонимы, чтобы + # allowlist не дрейфовал при добавлении новых. + for p_name in el.keys(): + if p_name.startswith('_'): + continue + if p_name not in KNOWN_KEYS and p_name not in APPEARANCE_SPEC and p_name not in APPEARANCE_SYNONYMS: + print(f"WARNING: Element '{el.get(type_key, '')}': unknown key '{p_name}' -- ignored. Check SKILL.md for valid keys.", file=sys.stderr) + + name = get_element_name(el, type_key) + _ensure_unique(name, _seen_element_names, 'element') + eid = new_id() + + emitters = { + 'group': emit_group, + 'columnGroup': emit_column_group, + 'buttonGroup': emit_button_group, + 'input': emit_input, + 'check': emit_check, + 'radio': emit_radio_button_field, + 'label': emit_label, + 'labelField': emit_label_field, + 'table': emit_table, + 'pages': emit_pages, + 'page': emit_page, + 'button': emit_button, + 'picture': emit_picture_decoration, + 'picField': emit_picture_field, + 'calendar': emit_calendar, + 'cmdBar': emit_command_bar, + 'popup': emit_popup, + 'searchString': lambda lines, el, name, eid, indent: emit_addition(lines, el, name, eid, 'searchString', indent), + 'viewStatus': lambda lines, el, name, eid, indent: emit_addition(lines, el, name, eid, 'viewStatus', indent), + 'searchControl': lambda lines, el, name, eid, indent: emit_addition(lines, el, name, eid, 'searchControl', indent), + 'spreadsheet': lambda lines, el, name, eid, indent: emit_simple_field(lines, el, name, eid, indent, 'SpreadSheetDocumentField', 'spreadsheet'), + 'html': lambda lines, el, name, eid, indent: emit_simple_field(lines, el, name, eid, indent, 'HTMLDocumentField', 'html'), + 'textDoc': lambda lines, el, name, eid, indent: emit_simple_field(lines, el, name, eid, indent, 'TextDocumentField', 'textDoc'), + 'formattedDoc': lambda lines, el, name, eid, indent: emit_simple_field(lines, el, name, eid, indent, 'FormattedDocumentField', 'formattedDoc'), + 'progressBar': lambda lines, el, name, eid, indent: emit_simple_field(lines, el, name, eid, indent, 'ProgressBarField', 'progressBar'), + 'trackBar': lambda lines, el, name, eid, indent: emit_simple_field(lines, el, name, eid, indent, 'TrackBarField', 'trackBar'), + 'chart': lambda lines, el, name, eid, indent: emit_simple_field(lines, el, name, eid, indent, 'ChartField', 'chart'), + 'graphicalSchema': lambda lines, el, name, eid, indent: emit_simple_field(lines, el, name, eid, indent, 'GraphicalSchemaField', 'graphicalSchema'), + 'planner': lambda lines, el, name, eid, indent: emit_simple_field(lines, el, name, eid, indent, 'PlannerField', 'planner'), + 'periodField': lambda lines, el, name, eid, indent: emit_simple_field(lines, el, name, eid, indent, 'PeriodField', 'periodField'), + 'dendrogram': lambda lines, el, name, eid, indent: emit_simple_field(lines, el, name, eid, indent, 'DendrogramField', 'dendrogram'), + 'ganttChart': emit_gantt_chart, + } + + emitter = emitters.get(type_key) + if emitter: + if type_key == 'button': + emitter(lines, el, name, eid, indent, in_cmd_bar=in_cmd_bar) + else: + emitter(lines, el, name, eid, indent) + + +def emit_group(lines, el, name, eid, indent): + lines.append(f'{indent}') + inner = f'{indent}\t' + + emit_title(lines, el, name, inner) + + # Group orientation + # Group orientation (направление). Legacy: group:'collapsible' = Vertical + behavior collapsible. + group_val = str(el.get('group', '')).lower() + orientation_map = { + 'horizontal': 'Horizontal', + 'vertical': 'Vertical', + 'alwayshorizontal': 'AlwaysHorizontal', + 'alwaysvertical': 'AlwaysVertical', + 'horizontalifpossible': 'HorizontalIfPossible', + 'collapsible': 'Vertical', + } + orientation = orientation_map.get(group_val) + if orientation: + lines.append(f'{inner}{orientation}') + + # Behavior: ключ behavior (usual/collapsible/popup) → ; отсутствие = Авто (не эмитим). + behavior_val = str(el['behavior']).lower() if el.get('behavior') else ('collapsible' if group_val == 'collapsible' else None) + bmap = {'usual': 'Usual', 'collapsible': 'Collapsible', 'popup': 'PopUp'} + if behavior_val and behavior_val in bmap: + lines.append(f'{inner}{bmap[behavior_val]}') + # Collapsed — у Collapsible и PopUp (не привязано к одному behavior) + if el.get('collapsed') is True: + lines.append(f'{inner}true') + + # Representation + if el.get('representation'): + repr_map = { + 'none': 'None', + 'normal': 'NormalSeparation', + 'weak': 'WeakSeparation', + 'strong': 'StrongSeparation', + } + repr_val = repr_map.get(str(el['representation']), str(el['representation'])) + lines.append(f'{inner}{repr_val}') + + # ShowTitle + if el.get('showTitle') is not None: + lines.append(f'{inner}{"true" if el["showTitle"] else "false"}') + # Заголовок свёрнутого представления (collapsible/popup) — мультиязычный текст + if el.get('collapsedTitle'): + emit_mltext(lines, inner, 'CollapsedRepresentationTitle', el['collapsedTitle']) + + # United + if el.get('united') is False: + lines.append(f'{inner}false') + + # Формат значения пути к данным заголовка (; парный к titleDataPath группы) + if el.get('format'): + emit_mltext(lines, inner, 'Format', el['format']) + if el.get('editFormat'): + emit_mltext(lines, inner, 'EditFormat', el['editFormat']) + + emit_common_flags(lines, el, inner) + emit_layout(lines, el, inner) + + # Оформление (цвета/шрифты/граница) — перед компаньоном + emit_appearance(lines, el, inner, 'field') + + # Companion: ExtendedTooltip + emit_companion(lines, 'ExtendedTooltip', f'{name}\u0420\u0430\u0441\u0448\u0438\u0440\u0435\u043d\u043d\u0430\u044f\u041f\u043e\u0434\u0441\u043a\u0430\u0437\u043a\u0430', inner, el.get('extendedTooltip')) + + # Children + if el.get('children') and len(el['children']) > 0: + lines.append(f'{inner}') + for child in el['children']: + emit_element(lines, child, f'{inner}\t') + lines.append(f'{inner}') + + lines.append(f'{indent}') + + +def emit_column_group(lines, el, name, eid, indent): + lines.append(f'{indent}') + inner = f'{indent}\t' + + emit_title(lines, el, name, inner) + + group_val = str(el.get('columnGroup', '')) + 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 not None: + lines.append(f'{inner}{"true" if el["showTitle"] else "false"}') + # showInHeader эмитится общим emit_common_element_props (через emit_layout) + + emit_common_flags(lines, el, inner) + emit_layout(lines, el, inner) + + # Картинка заголовка колонки-группы (после ShowInHeader/Layout, перед оформлением — порядок XSD) + emit_column_pics(lines, el, inner) + + # Оформление (цвета/шрифты/граница) — перед компаньоном + emit_appearance(lines, el, inner, 'field') + + emit_companion(lines, 'ExtendedTooltip', f'{name}РасширеннаяПодсказка', inner, el.get('extendedTooltip')) + + if el.get('children') and len(el['children']) > 0: + lines.append(f'{inner}') + for child in el['children']: + emit_element(lines, child, f'{inner}\t') + lines.append(f'{inner}') + + lines.append(f'{indent}') + + +def emit_input(lines, el, name, eid, indent): + lines.append(f'{indent}') + inner = f'{indent}\t' + + if el.get('path'): + lines.append(f'{inner}{el["path"]}') + + emit_title(lines, el, name, inner, auto=not el.get('path')) + emit_common_flags(lines, el, inner) + + if el.get('titleLocation'): + loc_map = {'none': 'None', 'left': 'Left', 'right': 'Right', 'top': 'Top', 'bottom': 'Bottom'} + loc = loc_map.get(str(el['titleLocation']), str(el['titleLocation'])) + lines.append(f'{inner}{loc}') + + if el.get('multiLine') is not None: + lines.append(f'{inner}{"true" if el["multiLine"] else "false"}') + if el.get('passwordMode') is not None: + lines.append(f'{inner}{"true" if el["passwordMode"] else "false"}') + # ChoiceButton — захват «как есть» (платформа эмитит явное значение; ref-поля выводят сама, + # декомпилятор фиксирует факт. значение). Нет ключа → не эмитим (не додумываем по событию). + if el.get('choiceButton') is not None: + lines.append(f'{inner}{"true" if el["choiceButton"] else "false"}') + # Кнопки поля ввода — захват «как есть» (платформа эмитит явное значение, в т.ч. false) + if el.get('clearButton') is not None: + lines.append(f'{inner}{"true" if el["clearButton"] else "false"}') + if el.get('spinButton') is not None: + lines.append(f'{inner}{"true" if el["spinButton"] else "false"}') + if el.get('dropListButton') is not None: + lines.append(f'{inner}{"true" if el["dropListButton"] else "false"}') + if el.get('choiceListButton') is not None: + lines.append(f'{inner}{"true" if el["choiceListButton"] else "false"}') + if el.get('markIncomplete') is not None: + lines.append(f'{inner}{"true" if el["markIncomplete"] else "false"}') + if el.get('editMode'): + lines.append(f'{inner}{el["editMode"]}') + emit_column_pics(lines, el, inner) + if el.get('textEdit') is False: + lines.append(f'{inner}false') + # InputField-специфичные скаляры (захват «как есть»: платформа эмитит явное не-дефолтное значение) + for key, tag in (('wrap', 'Wrap'), ('openButton', 'OpenButton'), ('listChoiceMode', 'ListChoiceMode'), + ('extendedEditMultipleValues', 'ExtendedEditMultipleValues'), ('chooseType', 'ChooseType'), + ('quickChoice', 'QuickChoice'), ('autoChoiceIncomplete', 'AutoChoiceIncomplete')): + if el.get(key) is not None: + lines.append(f'{inner}<{tag}>{"true" if el[key] else "false"}') + # Ограничение доступных типов (поле на составном типе): домен типов + явный набор. + # availableTypes — формат типа реквизита (§type); emit_type сам разбирает мультитип "a | b". + if el.get('typeDomainEnabled') is not None: + lines.append(f'{inner}{"true" if el["typeDomainEnabled"] else "false"}') + if el.get('availableTypes'): + emit_type(lines, el['availableTypes'], inner, tag='AvailableTypes') + # InputField-специфичные value-скаляры + for key, tag in (('choiceForm', 'ChoiceForm'), ('choiceHistoryOnInput', 'ChoiceHistoryOnInput'), + ('choiceFoldersAndItems', 'ChoiceFoldersAndItems'), ('footerDataPath', 'FooterDataPath')): + if el.get(key): + lines.append(f'{inner}<{tag}>{esc_xml(str(el[key]))}') + # MinValue/MaxValue — типизированное. JSON-число → xs:decimal, строка → xs:string (тип сохранён декомпилятором). + for key, tag in (('minValue', 'MinValue'), ('maxValue', 'MaxValue')): + if el.get(key) is not None: + mvt = 'xs:string' if isinstance(el[key], str) else 'xs:decimal' + lines.append(f'{inner}<{tag} xsi:type="{mvt}">{esc_xml(str(el[key]))}') + if el.get('choiceButtonRepresentation'): + lines.append(f'{inner}{el["choiceButtonRepresentation"]}') + emit_picture_ref(lines, el.get('choiceButtonPicture'), 'ChoiceButtonPicture', inner) + emit_layout(lines, el, inner, multi_line_default=(el.get('multiLine') is True)) + + if el.get('inputHint'): + emit_mltext(lines, inner, 'InputHint', el['inputHint']) + if el.get('warningOnEdit') is not None: + emit_mltext(lines, inner, 'WarningOnEdit', el['warningOnEdit']) + if el.get('footerText') is not None: + emit_mltext(lines, inner, 'FooterText', el['footerText']) + + # Формат / формат редактирования (LocalStringType — строка или {ru,en}) + if el.get('format'): + emit_mltext(lines, inner, 'Format', el['format']) + if el.get('editFormat'): + emit_mltext(lines, inner, 'EditFormat', el['editFormat']) + + emit_choice_list(lines, el, inner) + + # Связи по типу / связи параметров выбора / параметры выбора + emit_type_link(lines, el, inner) + emit_choice_parameter_links(lines, el, inner) + emit_choice_parameters(lines, el, inner) + + # Оформление (цвета/шрифты/граница) — перед компаньонами + emit_appearance(lines, el, inner, 'field') + + # Companions + emit_companion_panel(lines, 'ContextMenu', f'{name}\u041a\u043e\u043d\u0442\u0435\u043a\u0441\u0442\u043d\u043e\u0435\u041c\u0435\u043d\u044e', inner, el.get('contextMenu')) + emit_companion(lines, 'ExtendedTooltip', f'{name}\u0420\u0430\u0441\u0448\u0438\u0440\u0435\u043d\u043d\u0430\u044f\u041f\u043e\u0434\u0441\u043a\u0430\u0437\u043a\u0430', inner, el.get('extendedTooltip')) + + emit_events(lines, el, name, inner, 'input') + + lines.append(f'{indent}') + + +def emit_check(lines, el, name, eid, indent): + lines.append(f'{indent}') + inner = f'{indent}\t' + + if el.get('path'): + lines.append(f'{inner}{el["path"]}') + + emit_title(lines, el, name, inner, auto=not el.get('path')) + emit_common_flags(lines, el, inner) + + if el.get('editMode'): + lines.append(f'{inner}{el["editMode"]}') + emit_column_pics(lines, el, inner) + # CheckBoxType: нет ключа → умный дефолт Auto; "" → подавить; значение → маппинг + _cbt_map = {'auto': 'Auto', 'checkbox': 'CheckBox', 'switcher': 'Switcher', 'tumbler': 'Tumbler'} + if 'checkBoxType' in el: + if el.get('checkBoxType'): + lines.append(f'{inner}{_cbt_map.get(str(el["checkBoxType"]).lower(), el["checkBoxType"])}') + else: + lines.append(f'{inner}Auto') + + emit_title_location(lines, el, inner, 'Right') + + emit_layout(lines, el, inner) + + if el.get('warningOnEdit') is not None: + emit_mltext(lines, inner, 'WarningOnEdit', el['warningOnEdit']) + + # Формат / формат редактирования (LocalStringType — строка или {ru,en}) + if el.get('format'): + emit_mltext(lines, inner, 'Format', el['format']) + if el.get('editFormat'): + emit_mltext(lines, inner, 'EditFormat', el['editFormat']) + + # Оформление (цвета/шрифты/граница) — перед компаньонами + emit_appearance(lines, el, inner, 'field') + + # Companions + emit_companion_panel(lines, 'ContextMenu', f'{name}\u041a\u043e\u043d\u0442\u0435\u043a\u0441\u0442\u043d\u043e\u0435\u041c\u0435\u043d\u044e', inner, el.get('contextMenu')) + emit_companion(lines, 'ExtendedTooltip', f'{name}\u0420\u0430\u0441\u0448\u0438\u0440\u0435\u043d\u043d\u0430\u044f\u041f\u043e\u0434\u0441\u043a\u0430\u0437\u043a\u0430', inner, el.get('extendedTooltip')) + + emit_events(lines, el, name, inner, 'check') + + lines.append(f'{indent}') + + +def emit_radio_button_field(lines, el, name, eid, indent): + lines.append(f'{indent}') + inner = f'{indent}\t' + + if el.get('path'): + lines.append(f'{inner}{el["path"]}') + + emit_title(lines, el, name, inner, auto=not el.get('path')) + emit_common_flags(lines, el, inner) + + if el.get('editMode'): + lines.append(f'{inner}{el["editMode"]}') + emit_title_location(lines, el, inner, 'None') + + rbt = normalize_radio_button_type(el.get('radioButtonType')) + lines.append(f'{inner}{rbt}') + + if el.get('columnsCount') is not None: + lines.append(f'{inner}{el["columnsCount"]}') + + emit_choice_list(lines, el, inner) + + emit_layout(lines, el, inner) + + if el.get('warningOnEdit') is not None: + emit_mltext(lines, inner, 'WarningOnEdit', el['warningOnEdit']) + + # Оформление (цвета/шрифты/граница) — перед компаньонами + emit_appearance(lines, el, inner, 'field') + + emit_companion_panel(lines, 'ContextMenu', f'{name}КонтекстноеМеню', inner, el.get('contextMenu')) + emit_companion(lines, 'ExtendedTooltip', f'{name}РасширеннаяПодсказка', inner, el.get('extendedTooltip')) + + emit_events(lines, el, name, inner, 'radio') + + lines.append(f'{indent}') + + +# Заголовок декорации (Label/Picture): formatted-aware через единую ML-text форму +# (reuse resolve_ml_formatted, как у extendedTooltip). Sibling-ключ formatted — back-compat override. +def emit_decoration_title(lines, el, name, indent, auto=False): + has_key = 'title' in el + title_val = el['title'] if has_key else (title_from_name(name) if (auto and name) else None) + if title_val: + text, fmt = resolve_ml_formatted(title_val) + if 'formatted' in el: + fmt = bool(el['formatted']) + lines.append(f'{indent}<Title formatted="{"true" if fmt else "false"}">') + emit_ml_items(lines, f'{indent}\t', text) + lines.append(f'{indent}') + if el.get('tooltip'): + emit_mltext(lines, indent, 'ToolTip', el['tooltip']) + if el.get('tooltipRepresentation'): + lines.append(f'{indent}{el["tooltipRepresentation"]}') + + +def emit_label(lines, el, name, eid, indent): + lines.append(f'{indent}') + inner = f'{indent}\t' + + # Порядок как у платформы: own-content (флаги/hyperlink/layout/оформление) ПЕРЕД Title + # (корпус layout-first 16970 vs 44 — заодно убирает шум атрибуции харнесса на многострочном Title). + emit_common_flags(lines, el, inner) + if el.get('hyperlink') is True: + lines.append(f'{inner}true') + emit_layout(lines, el, inner) + emit_appearance(lines, el, inner, 'decoration') + + emit_decoration_title(lines, el, name, inner, auto=True) + + # Companions + emit_companion_panel(lines, 'ContextMenu', f'{name}\u041a\u043e\u043d\u0442\u0435\u043a\u0441\u0442\u043d\u043e\u0435\u041c\u0435\u043d\u044e', inner, el.get('contextMenu')) + emit_companion(lines, 'ExtendedTooltip', f'{name}\u0420\u0430\u0441\u0448\u0438\u0440\u0435\u043d\u043d\u0430\u044f\u041f\u043e\u0434\u0441\u043a\u0430\u0437\u043a\u0430', inner, el.get('extendedTooltip')) + + emit_events(lines, el, name, inner, 'label') + + lines.append(f'{indent}') + + +def emit_label_field(lines, el, name, eid, indent): + lines.append(f'{indent}') + inner = f'{indent}\t' + + if el.get('path'): + lines.append(f'{inner}{el["path"]}') + + emit_title(lines, el, name, inner, auto=not el.get('path')) + emit_common_flags(lines, el, inner) + + if el.get('titleLocation'): + lines.append(f'{inner}{map_title_loc(el["titleLocation"])}') + if el.get('editMode'): + lines.append(f'{inner}{el["editMode"]}') + # FooterDataPath — путь данных подвала колонки (общий cell-prop, как у input); после EditMode + if el.get('footerDataPath'): + lines.append(f'{inner}{esc_xml(str(el["footerDataPath"]))}') + # PasswordMode на LabelField — платформа эмитит явный false (редко); факт. значение + if el.get('passwordMode') is not None: + lines.append(f'{inner}{"true" if el["passwordMode"] else "false"}') + emit_column_pics(lines, el, inner) + # ВНИМАНИЕ: у LabelField платформенный тег (опечатка 1С), не . + if el.get('hyperlink') is True: + lines.append(f'{inner}true') + emit_layout(lines, el, inner) + + if el.get('warningOnEdit') is not None: + emit_mltext(lines, inner, 'WarningOnEdit', el['warningOnEdit']) + if el.get('footerText') is not None: + emit_mltext(lines, inner, 'FooterText', el['footerText']) + + # Формат / формат редактирования (LocalStringType — строка или {ru,en}) + if el.get('format'): + emit_mltext(lines, inner, 'Format', el['format']) + if el.get('editFormat'): + emit_mltext(lines, inner, 'EditFormat', el['editFormat']) + + # Оформление (цвета/шрифты/граница + header/footer) — перед компаньонами + emit_appearance(lines, el, inner, 'field') + + # Companions + emit_companion_panel(lines, 'ContextMenu', f'{name}\u041a\u043e\u043d\u0442\u0435\u043a\u0441\u0442\u043d\u043e\u0435\u041c\u0435\u043d\u044e', inner, el.get('contextMenu')) + emit_companion(lines, 'ExtendedTooltip', f'{name}\u0420\u0430\u0441\u0448\u0438\u0440\u0435\u043d\u043d\u0430\u044f\u041f\u043e\u0434\u0441\u043a\u0430\u0437\u043a\u0430', inner, el.get('extendedTooltip')) + + emit_events(lines, el, name, inner, 'labelField') + + lines.append(f'{indent}') + + +# Блок свойств таблицы, привязанной к динамическому списку (Group A defaults + B/C). +def emit_dynlist_table_block(lines, el, indent): + # (useAlternationRowColor — общее свойство таблицы, эмитится в emit_table) + # Group A (гарант. блок): дефолт + override + ar = 'true' if el.get('autoRefresh') is True else 'false' + lines.append(f'{indent}{ar}') + arp = el['autoRefreshPeriod'] if el.get('autoRefreshPeriod') is not None else 60 + lines.append(f'{indent}{arp}') + lines.append(f'{indent}') + lines.append(f'{indent}\tCustom') + lines.append(f'{indent}\t0001-01-01T00:00:00') + lines.append(f'{indent}\t0001-01-01T00:00:00') + lines.append(f'{indent}') + cfi = el.get('choiceFoldersAndItems') or 'Items' + lines.append(f'{indent}{cfi}') + rcr = 'true' if el.get('restoreCurrentRow') is True else 'false' + lines.append(f'{indent}{rcr}') + lines.append(f'{indent}') + sr = 'false' if el.get('showRoot') is False else 'true' + lines.append(f'{indent}{sr}') + arc = 'true' if el.get('allowRootChoice') is True else 'false' + lines.append(f'{indent}{arc}') + uodc = el.get('updateOnDataChange') or 'Auto' + lines.append(f'{indent}{uodc}') + if el.get('userSettingsGroup'): + lines.append(f'{indent}{el["userSettingsGroup"]}') + agcru = 'false' if el.get('allowGettingCurrentRowURL') is False else 'true' + lines.append(f'{indent}{agcru}') + + +def emit_table(lines, el, name, eid, indent): + _current_table_name['name'] = name # дефолт source для кастомных дополнений в commandBar + lines.append(f'{indent}
') + inner = f'{indent}\t' + + if el.get('path'): + lines.append(f'{inner}{el["path"]}') + + emit_title(lines, el, name, inner, auto=not el.get('path')) + emit_common_flags(lines, el, inner) + + if el.get('representation'): + lines.append(f'{inner}{el["representation"]}') + if el.get('titleLocation'): + lines.append(f'{inner}{map_title_loc(el["titleLocation"])}') + # ChangeRowSet/Order — явное значение (в т.ч. false: платформа пишет его на ValueTable) + if 'changeRowSet' in el and el['changeRowSet'] is not None: + lines.append(f'{inner}{"true" if el["changeRowSet"] is True else "false"}') + if 'changeRowOrder' in el and el['changeRowOrder'] is not None: + lines.append(f'{inner}{"true" if el["changeRowOrder"] is True else "false"}') + if el.get('autoInsertNewRow') is True: + lines.append(f'{inner}true') + # RowFilter — nil-плейсхолдер (ключ присутствует → эмитим) + if 'rowFilter' in el: + lines.append(f'{inner}') + # Высота в строках () — отдельное свойство от (высота элемента, + # эмитится generic-ом emit_layout ниже). Таблица может нести оба (237 в корпусе). + if el.get('heightInTableRows'): + lines.append(f'{inner}{el["heightInTableRows"]}') + if el.get('header') is False: + lines.append(f'{inner}
false
') + if el.get('footer') is True: + lines.append(f'{inner}
true
') + + if el.get('commandBarLocation'): + lines.append(f'{inner}{el["commandBarLocation"]}') + if el.get('searchStringLocation'): + lines.append(f'{inner}{el["searchStringLocation"]}') + + if el.get('choiceMode') is True: + lines.append(f'{inner}true') + # Скаляры таблицы (захват «как есть»). Autofill — СВОЁ свойство таблицы (≠ AutoCommandBar autofill = tableAutofill). + if el.get('autofill') is not None: + lines.append(f'{inner}{"true" if el["autofill"] else "false"}') + if el.get('multipleChoice') is True: + lines.append(f'{inner}true') + if el.get('searchOnInput'): + lines.append(f'{inner}{el["searchOnInput"]}') + if el.get('markIncomplete') is not None: + lines.append(f'{inner}{"true" if el["markIncomplete"] else "false"}') + # Высота шапки/подвала в строках (pass-through; 1С толерантна к порядку детей Table) + if el.get('headerHeight') is not None: + lines.append(f'{inner}{el["headerHeight"]}') + if el.get('footerHeight') is not None: + lines.append(f'{inner}{el["footerHeight"]}') + if el.get('useAlternationRowColor') is True: + lines.append(f'{inner}true') + if el.get('selectionMode'): + lines.append(f'{inner}{el["selectionMode"]}') + if el.get('rowSelectionMode'): + lines.append(f'{inner}{el["rowSelectionMode"]}') + if el.get('verticalLines') is False: + lines.append(f'{inner}false') + if el.get('horizontalLines') is False: + lines.append(f'{inner}false') + if el.get('initialTreeView'): + lines.append(f'{inner}{el["initialTreeView"]}') + if el.get('enableDrag') is not None: + lines.append(f'{inner}{"true" if el["enableDrag"] else "false"}') + if el.get('rowPictureDataPath'): + lines.append(f'{inner}{el["rowPictureDataPath"]}') + # RowsPicture — та же конвенция, что ValuesPicture (дефолт LoadTransparent=false; abs/TransparentPixel) + emit_picture_ref(lines, el.get('rowsPicture'), 'RowsPicture', inner) + # Использование текущей строки таблицы (pass-through; в корпусе соседствует с блоком дин-списка) + if el.get('currentRowUse'): + lines.append(f'{inner}{el["currentRowUse"]}') + # Запрос обновления дин-списка (pass-through; в корпусе всегда PullFromTop) + if el.get('refreshRequest'): + lines.append(f'{inner}{el["refreshRequest"]}') + # Блок свойств дин-список-таблицы (помечена эвристикой) + if el.get('_dynList'): + emit_dynlist_table_block(lines, el, inner) + if el.get('viewStatusLocation'): + lines.append(f'{inner}{el["viewStatusLocation"]}') + if el.get('searchControlLocation'): + lines.append(f'{inner}{el["searchControlLocation"]}') + emit_layout(lines, el, inner) + + # CommandSet таблицы эмитится через emit_layout (общий механизм поля) + + # Оформление (цвета/граница таблицы) — перед компаньонами + emit_appearance(lines, el, inner, 'field') + + # Companions + emit_companion_panel(lines, 'ContextMenu', f'{name}\u041a\u043e\u043d\u0442\u0435\u043a\u0441\u0442\u043d\u043e\u0435\u041c\u0435\u043d\u044e', inner, el.get('contextMenu')) + # AutoCommandBar — with optional Autofill control + if el.get('commandBar') is not None: + emit_companion_panel(lines, 'AutoCommandBar', f'{name}\u041a\u043e\u043c\u0430\u043d\u0434\u043d\u0430\u044f\u041f\u0430\u043d\u0435\u043b\u044c', inner, el.get('commandBar')) + elif el.get('tableAutofill') is not None: + acb_id = new_id() + acb_name = f'{name}\u041a\u043e\u043c\u0430\u043d\u0434\u043d\u0430\u044f\u041f\u0430\u043d\u0435\u043b\u044c' + af_val = 'true' if el['tableAutofill'] else 'false' + lines.append(f'{inner}') + lines.append(f'{inner}\t{af_val}') + lines.append(f'{inner}') + else: + emit_companion(lines, 'AutoCommandBar', f'{name}\u041a\u043e\u043c\u0430\u043d\u0434\u043d\u0430\u044f\u041f\u0430\u043d\u0435\u043b\u044c', inner) + emit_companion(lines, 'ExtendedTooltip', f'{name}\u0420\u0430\u0441\u0448\u0438\u0440\u0435\u043d\u043d\u0430\u044f\u041f\u043e\u0434\u0441\u043a\u0430\u0437\u043a\u0430', inner, el.get('extendedTooltip')) + adds = el.get('additions') + emit_table_addition(lines, 'searchString', name, inner, get_addition_override(adds, 'searchString')) + emit_table_addition(lines, 'viewStatus', name, inner, get_addition_override(adds, 'viewStatus')) + emit_table_addition(lines, 'searchControl', name, inner, get_addition_override(adds, 'searchControl')) + + # Columns + if el.get('columns') and len(el['columns']) > 0: + lines.append(f'{inner}') + for col in el['columns']: + emit_element(lines, col, f'{inner}\t') + lines.append(f'{inner}') + + emit_events(lines, el, name, inner, 'table') + + lines.append(f'{indent}
') + + +def emit_pages(lines, el, name, eid, indent): + lines.append(f'{indent}') + inner = f'{indent}\t' + + emit_title(lines, el, name, inner) + + if el.get('pagesRepresentation'): + lines.append(f'{inner}{el["pagesRepresentation"]}') + + emit_common_flags(lines, el, inner) + emit_layout(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, el.get('extendedTooltip')) + + emit_events(lines, el, name, inner, 'pages') + + # Children (pages) + if el.get('children') and len(el['children']) > 0: + lines.append(f'{inner}') + for child in el['children']: + emit_element(lines, child, f'{inner}\t') + lines.append(f'{inner}') + + lines.append(f'{indent}') + + +def emit_page(lines, el, name, eid, indent): + lines.append(f'{indent}') + inner = f'{indent}\t' + + emit_title(lines, el, name, inner, auto=True) + emit_common_flags(lines, el, inner) + + # Картинка страницы (иконка вкладки): после Title/флагов, перед Group (порядок XSD). + # Конвенция как у ValuesPicture (дефолт LoadTransparent=false): скаляр-Ref/'abs:X' или объект. + emit_picture_ref(lines, el.get('picture'), 'Picture', inner) + + if el.get('group'): + orientation_map = { + 'horizontal': 'Horizontal', + 'vertical': 'Vertical', + 'alwaysHorizontal': 'AlwaysHorizontal', + 'alwaysVertical': 'AlwaysVertical', + 'horizontalIfPossible': 'HorizontalIfPossible', + } + orientation = orientation_map.get(str(el['group'])) + if orientation: + lines.append(f'{inner}{orientation}') + if el.get('showTitle') is not None: + lines.append(f'{inner}{"true" if el["showTitle"] else "false"}') + # Формат значения пути к данным заголовка (; парный к titleDataPath страницы) + if el.get('format'): + emit_mltext(lines, inner, 'Format', el['format']) + if el.get('editFormat'): + emit_mltext(lines, inner, 'EditFormat', el['editFormat']) + emit_layout(lines, el, inner) + + # \u041e\u0444\u043e\u0440\u043c\u043b\u0435\u043d\u0438\u0435 \u0441\u0442\u0440\u0430\u043d\u0438\u0446\u044b (BackColor / TitleTextColor / TitleFont) \u2014 \u043f\u043e\u0441\u043b\u0435 ShowTitle, \u043f\u0435\u0440\u0435\u0434 \u043a\u043e\u043c\u043f\u0430\u043d\u044c\u043e\u043d\u043e\u043c + emit_appearance(lines, el, inner, 'field') + + # Companion + emit_companion(lines, 'ExtendedTooltip', f'{name}\u0420\u0430\u0441\u0448\u0438\u0440\u0435\u043d\u043d\u0430\u044f\u041f\u043e\u0434\u0441\u043a\u0430\u0437\u043a\u0430', inner, el.get('extendedTooltip')) + + # Children + if el.get('children') and len(el['children']) > 0: + lines.append(f'{inner}') + for child in el['children']: + emit_element(lines, child, f'{inner}\t') + lines.append(f'{inner}') + + lines.append(f'{indent}') + + +def emit_button(lines, el, name, eid, indent, in_cmd_bar=False): + lines.append(f'{indent}') + + +def emit_picture_decoration(lines, el, name, eid, indent): + lines.append(f'{indent}') + inner = f'{indent}\t' + + emit_decoration_title(lines, el, name, inner) + # Текст при невыбранной картинке (NonselectedPictureText) — после Title (порядок корпуса) + if el.get('nonselectedPictureText') is not None: + emit_mltext(lines, inner, 'NonselectedPictureText', el['nonselectedPictureText']) + emit_common_flags(lines, el, inner) + + # Источник картинки — ТОЛЬКО src (ключ 'picture' = тип/имя элемента, не источник). + # Префикс "abs:" → встроенная картинка ; иначе именованная/стилевая . + if el.get('src'): + src_str = str(el['src']) + lt = 'true' if el.get('loadTransparent') is True else 'false' + lines.append(f'{inner}') + if src_str.startswith('abs:'): + lines.append(f'{inner}\t{esc_xml(src_str[4:])}') + else: + lines.append(f'{inner}\t{esc_xml(src_str)}') + lines.append(f'{inner}\t{lt}') + tpx = el.get('transparentPixel') + if tpx: + lines.append(f'{inner}\t') + lines.append(f'{inner}') + + if el.get('hyperlink') is True: + lines.append(f'{inner}true') + emit_layout(lines, el, inner) + + # Оформление (цвета/шрифт/граница) — профиль декорации (1С толерантна к порядку appearance) + emit_appearance(lines, el, inner, 'decoration') + + # Companions + emit_companion_panel(lines, 'ContextMenu', f'{name}\u041a\u043e\u043d\u0442\u0435\u043a\u0441\u0442\u043d\u043e\u0435\u041c\u0435\u043d\u044e', inner, el.get('contextMenu')) + emit_companion(lines, 'ExtendedTooltip', f'{name}\u0420\u0430\u0441\u0448\u0438\u0440\u0435\u043d\u043d\u0430\u044f\u041f\u043e\u0434\u0441\u043a\u0430\u0437\u043a\u0430', inner, el.get('extendedTooltip')) + + emit_events(lines, el, name, inner, 'picture') + + lines.append(f'{indent}') + + +def emit_picture_field(lines, el, name, eid, indent): + lines.append(f'{indent}') + inner = f'{indent}\t' + + if el.get('path'): + lines.append(f'{inner}{el["path"]}') + + emit_title(lines, el, name, inner) + emit_common_flags(lines, el, inner) + + if el.get('editMode'): + lines.append(f'{inner}{el["editMode"]}') + emit_column_pics(lines, el, inner) + if el.get('titleLocation'): + lines.append(f'{inner}{map_title_loc(el["titleLocation"])}') + if el.get('hyperlink') is True: + lines.append(f'{inner}true') + + emit_layout(lines, el, inner) + + # ValuesPicture — picture (collection) used to render the field's value. + # Required for a Boolean-bound PictureField to actually show an icon. + # Скаляр (Ref) или объект {src, loadTransparent}; LoadTransparent эмитится всегда. + emit_picture_ref(lines, el.get('valuesPicture'), 'ValuesPicture', inner) + if el.get('nonselectedPictureText') is not None: + emit_mltext(lines, inner, 'NonselectedPictureText', el['nonselectedPictureText']) + + # Оформление (цвета/шрифты/граница) — перед компаньонами + emit_appearance(lines, el, inner, 'field') + + # Companions + emit_companion_panel(lines, 'ContextMenu', f'{name}\u041a\u043e\u043d\u0442\u0435\u043a\u0441\u0442\u043d\u043e\u0435\u041c\u0435\u043d\u044e', inner, el.get('contextMenu')) + emit_companion(lines, 'ExtendedTooltip', f'{name}\u0420\u0430\u0441\u0448\u0438\u0440\u0435\u043d\u043d\u0430\u044f\u041f\u043e\u0434\u0441\u043a\u0430\u0437\u043a\u0430', inner, el.get('extendedTooltip')) + + emit_events(lines, el, name, inner, 'picField') + + lines.append(f'{indent}') + + +def emit_simple_field(lines, el, name, eid, indent, xml_tag, type_key): + # Спец-поля "документ/датчик" (SpreadSheet/HTML/Text/Formatted/ProgressBar/TrackBar): + # единый скелет поля. Типоспец. enum/bool скаляры — через generic (emit_layout); + # числовые скаляры датчиков (min/max/шаги) — без xsi:type; enableDrag — фактическое значение. + lines.append(f'{indent}<{xml_tag} name="{name}" id="{eid}"{di_attr(el)}>') + inner = f'{indent}\t' + + if el.get('path'): + lines.append(f'{inner}{el["path"]}') + emit_title(lines, el, name, inner, auto=not el.get('path')) + emit_common_flags(lines, el, inner) + if el.get('titleLocation'): + lines.append(f'{inner}{map_title_loc(el["titleLocation"])}') + if el.get('editMode'): + lines.append(f'{inner}{el["editMode"]}') + + emit_layout(lines, el, inner) + + # EnableDrag — фактическое значение (SpreadSheet; платформа эмитит явный false). enableStartDrag — через emit_layout. + if el.get('enableDrag') is not None: + lines.append(f'{inner}{"true" if el["enableDrag"] else "false"}') + + # Датчики (ProgressBar/TrackBar) — числовые скаляры (без xsi:type) + for key, tag in (('minValue', 'MinValue'), ('maxValue', 'MaxValue'), ('largeStep', 'LargeStep'), ('markingStep', 'MarkingStep'), ('step', 'Step')): + if el.get(key) is not None: + lines.append(f'{inner}<{tag}>{el[key]}') + + # Оформление (цвета/шрифты/граница) — перед компаньонами + emit_appearance(lines, el, inner, 'field') + + # Companions + emit_companion_panel(lines, 'ContextMenu', f'{name}КонтекстноеМеню', inner, el.get('contextMenu')) + emit_companion(lines, 'ExtendedTooltip', f'{name}РасширеннаяПодсказка', inner, el.get('extendedTooltip')) + + emit_events(lines, el, name, inner, type_key) + + lines.append(f'{indent}') + + +def emit_gantt_chart(lines, el, name, eid, indent): + # GanttChartField — скелет поля + вложенная (полноценная таблица, через emit_element). + lines.append(f'{indent}') + inner = f'{indent}\t' + if el.get('path'): + lines.append(f'{inner}{el["path"]}') + emit_title(lines, el, name, inner, auto=not el.get('path')) + emit_common_flags(lines, el, inner) + if el.get('titleLocation'): + lines.append(f'{inner}{map_title_loc(el["titleLocation"])}') + emit_layout(lines, el, inner) + emit_appearance(lines, el, inner, 'field') + emit_companion_panel(lines, 'ContextMenu', f'{name}КонтекстноеМеню', inner, el.get('contextMenu')) + emit_companion(lines, 'ExtendedTooltip', f'{name}РасширеннаяПодсказка', inner, el.get('extendedTooltip')) + # Вложенная таблица диаграммы Ганта (стандартный Table — переиспользуем emit_element) + if el.get('ganttTable'): + emit_element(lines, el['ganttTable'], inner) + emit_events(lines, el, name, inner, 'ganttChart') + lines.append(f'{indent}') + + +def emit_calendar(lines, el, name, eid, indent): + lines.append(f'{indent}') + inner = f'{indent}\t' + + if el.get('path'): + lines.append(f'{inner}{el["path"]}') + + emit_title(lines, el, name, inner, auto=not el.get('path')) + emit_common_flags(lines, el, inner) + + if el.get('titleLocation'): + loc_map = {'none': 'None', 'left': 'Left', 'right': 'Right', 'top': 'Top', 'bottom': 'Bottom', 'auto': 'Auto'} + loc = loc_map.get(str(el['titleLocation']), str(el['titleLocation'])) + lines.append(f'{inner}{loc}') + + emit_layout(lines, el, inner) + + # Календарно-специфичные свойства (порядок схемы: после layout, до companions) + if el.get('selectionMode'): + lines.append(f'{inner}{el["selectionMode"]}') + if el.get('showCurrentDate') is not None: + lines.append(f'{inner}{"true" if el["showCurrentDate"] else "false"}') + if el.get('widthInMonths') is not None: + lines.append(f'{inner}{el["widthInMonths"]}') + if el.get('heightInMonths') is not None: + lines.append(f'{inner}{el["heightInMonths"]}') + if el.get('showMonthsPanel') is not None: + lines.append(f'{inner}{"true" if el["showMonthsPanel"] else "false"}') + + # Оформление (цвета/шрифты/граница) — перед компаньонами + emit_appearance(lines, el, inner, 'field') + + # Companions + emit_companion_panel(lines, 'ContextMenu', f'{name}\u041a\u043e\u043d\u0442\u0435\u043a\u0441\u0442\u043d\u043e\u0435\u041c\u0435\u043d\u044e', inner, el.get('contextMenu')) + emit_companion(lines, 'ExtendedTooltip', f'{name}\u0420\u0430\u0441\u0448\u0438\u0440\u0435\u043d\u043d\u0430\u044f\u041f\u043e\u0434\u0441\u043a\u0430\u0437\u043a\u0430', inner, el.get('extendedTooltip')) + + emit_events(lines, el, name, inner, 'calendar') + + lines.append(f'{indent}') + + +def emit_command_bar(lines, el, name, eid, indent): + lines.append(f'{indent}') + inner = f'{indent}\t' + + emit_title(lines, el, name, inner) + + if el.get('commandSource'): + lines.append(f'{inner}{el["commandSource"]}') + + if el.get('autofill') is True: + lines.append(f'{inner}true') + + _hl = get_hlocation(el) + if _hl: + lines.append(f'{inner}{_hl}') + + emit_common_flags(lines, el, inner) + emit_layout(lines, el, inner) + emit_companion(lines, 'ExtendedTooltip', f'{name}РасширеннаяПодсказка', inner, el.get('extendedTooltip')) + + # Children + if el.get('children') and len(el['children']) > 0: + lines.append(f'{inner}') + for child in el['children']: + emit_element(lines, child, f'{inner}\t', in_cmd_bar=True) + lines.append(f'{inner}') + + lines.append(f'{indent}') + + +def emit_popup(lines, el, name, eid, indent): + lines.append(f'{indent}') + inner = f'{indent}\t' + + emit_title(lines, el, name, inner, auto=True) + emit_common_flags(lines, el, inner) + + emit_command_picture(lines, el.get('picture'), el.get('loadTransparent'), inner) + + if el.get('representation'): + lines.append(f'{inner}{el["representation"]}') + emit_layout(lines, el, inner) + + # Оформление попапа (TitleTextColor / TitleFont) — перед компаньоном + emit_appearance(lines, el, inner, 'field') + + emit_companion(lines, 'ExtendedTooltip', f'{name}РасширеннаяПодсказка', inner, el.get('extendedTooltip')) + + # Children + if el.get('children') and len(el['children']) > 0: + lines.append(f'{inner}') + for child in el['children']: + emit_element(lines, child, f'{inner}\t', in_cmd_bar=True) + lines.append(f'{inner}') + + lines.append(f'{indent}') + + +def emit_button_group(lines, el, name, eid, indent): + lines.append(f'{indent}') + inner = f'{indent}\t' + + emit_title(lines, el, name, inner) + + if el.get('commandSource'): + lines.append(f'{inner}{el["commandSource"]}') + + if el.get('representation'): + lines.append(f'{inner}{el["representation"]}') + + emit_common_flags(lines, el, inner) + emit_layout(lines, el, inner) + + # Companion: ExtendedTooltip + emit_companion(lines, 'ExtendedTooltip', f'{name}РасширеннаяПодсказка', inner, el.get('extendedTooltip')) + + # Children (кнопки в контексте командной панели) + if el.get('children') and len(el['children']) > 0: + lines.append(f'{inner}') + for child in el['children']: + emit_element(lines, child, f'{inner}\t', in_cmd_bar=True) + lines.append(f'{inner}') + + lines.append(f'{indent}') + + +# --- Attribute emitter --- + +def emit_functional_options(lines, fo, indent): + # FunctionalOption.X…> — у Attribute/Command/Column. + # Forgiving: "X"/"FunctionalOption.X" → FunctionalOption.X; GUID (расширение) — как есть. + if not fo: + return + lines.append(f'{indent}') + for opt in fo: + v = str(opt) + if re.match(r'^[0-9a-fA-F]{8}-[0-9a-fA-F-]{27,}$', v): + pass + elif v.startswith('FunctionalOption.'): + pass + else: + v = f'FunctionalOption.{v}' + lines.append(f'{indent}\t{v}') + lines.append(f'{indent}') + + +def emit_attr_column(lines, col, indent): + # Колонка реквизита (ValueTable/Tree или AdditionalColumns): name/Title/Type/FunctionalOptions. + col_id = new_id() + lines.append(f'{indent}') + if col.get('title'): + emit_mltext(lines, f'{indent}\t', 'Title', col['title']) + emit_type(lines, str(col.get('type', '')), f'{indent}\t') + emit_functional_options(lines, col.get('functionalOptions'), f'{indent}\t') + # Ролевой доступ колонки (View/Edit) — xr-флаг, как у самого реквизита + if col.get('view') is not None: + emit_xr_flag(lines, 'View', col['view'], f'{indent}\t') + if col.get('edit') is not None: + emit_xr_flag(lines, 'Edit', col['edit'], f'{indent}\t') + lines.append(f'{indent}') + + +# --- Schema-параметры динамического списка (DataCompositionSchemaParameter) --- +# Зеркало form-compile.ps1 (Emit-DLParameters). Та же сущность, что параметры СКД, но в +# форме: обёртка + дети dcssch:. DSL переиспользует грамматику параметров СКД. +# Контекстные дефолты: useRestriction эмитим ВСЕГДА, дефолт true (в СКД false); title — авто +# из имени; пустое value — всегда xsi:nil (даже при известном типе). Канон. порядок детей +# (по корпусу): name, title, valueType, value, useRestriction, expression, availableValue*, +# valueListAllowed, availableAsField, inputParameters, denyIncompleteValues, use. + +def emit_dl_mltext(lines, indent, tag, text): + # ML-текст с xsi:type="v8:LocalStringType" (в dcssch:* обязателен; emit_mltext его не ставит). + lines.append(f'{indent}<{tag} xsi:type="v8:LocalStringType">') + emit_ml_items(lines, f'{indent}\t', text) + lines.append(f'{indent}') + + +def split_dl_valuelist_csv(s): + result = [] + if s is None: + return result + items = [] + buf = [] + in_quote = None + for ch in s: + if in_quote: + buf.append(ch) + if ch == in_quote: + in_quote = None + elif ch in ("'", '"'): + in_quote = ch + buf.append(ch) + elif ch == ',': + items.append(''.join(buf)); buf = [] + else: + buf.append(ch) + if buf: + items.append(''.join(buf)) + for raw in items: + t = raw.strip() + if len(t) >= 2 and ((t[0] == "'" and t[-1] == "'") or (t[0] == '"' and t[-1] == '"')): + t = t[1:-1] + if t != '': + result.append(t) + return result + + +def parse_dl_param_shorthand(s): + result = {'name': '', 'type': '', 'value': None, 'title': None} + if '@valueList' in s: + result['valueListAllowed'] = True + s = re.sub(r'\s*@valueList', '', s) + if '@hidden' in s: + result['hidden'] = True + s = re.sub(r'\s*@hidden', '', s) + m = re.search(r'\[([^\]]*)\]', s) + if m: + result['title'] = m.group(1).strip() + s = re.sub(r'\s*\[[^\]]*\]\s*', ' ', s).strip() + # Тип может быть СОСТАВНЫМ (A | B | C — с пробелами); значение — после '=' (тип '=' не содержит). + m = re.match(r'^([^:]+):\s*([^=]+?)(\s*=\s*(.*))?$', s) + if m: + result['name'] = m.group(1).strip() + type_raw = m.group(2).strip() + if re.search(r'[|+]', type_raw): + result['type'] = ' | '.join(resolve_type_str(p.strip()) for p in re.split(r'\s*[|+]\s*', type_raw)) + else: + result['type'] = resolve_type_str(type_raw) + if m.group(4): + rhs = m.group(4).strip() + items = split_dl_valuelist_csv(rhs) + if len(items) >= 2: + result['value'] = items + result['valueListAllowed'] = True + elif len(items) == 1: + result['value'] = items[0] + else: + result['value'] = rhs + else: + result['name'] = s.strip() + return result + + +def is_dl_empty_value(v): + if v is None: + return True + sv = str(v).strip() + return sv == '' or sv == '_' or sv.lower() == 'null' + + +def emit_dl_value(lines, type_str, val, indent, value_list_allowed=False): + if is_dl_empty_value(val): + # Дин-список: пустое значение платформа ВСЕГДА пишет как xsi:nil (даже при известном типе). + if value_list_allowed: + return + lines.append(f'{indent}') + return + if isinstance(val, bool): + val_str = 'true' if val else 'false' + else: + val_str = str(val) + t = type_str or '' + if re.match(r'^(date|dateTime|time)', t): + lines.append(f'{indent}{esc_xml(val_str)}') + elif t == 'boolean': + lines.append(f'{indent}{esc_xml(val_str)}') + elif t == 'v8:Type': + ns_attr = _value_type_ns_attr('v8:Type', val_str) + lines.append(f'{indent}{esc_xml(val_str)}') + elif re.match(r'^decimal', t): + lines.append(f'{indent}{esc_xml(val_str)}') + elif re.match(r'^string', t): + lines.append(f'{indent}{esc_xml(val_str)}') + elif re.match(r'^(CatalogRef|DocumentRef|EnumRef|ChartOfAccountsRef|ChartOfCharacteristicTypesRef|ChartOfCalculationTypesRef|BusinessProcessRef|TaskRef|ExchangePlanRef)\.', t): + lines.append(f'{indent}{esc_xml(val_str)}') + else: + if re.match(r'^\d{4}-\d{2}-\d{2}T', val_str): + lines.append(f'{indent}{esc_xml(val_str)}') + elif val_str in ('true', 'false'): + lines.append(f'{indent}{esc_xml(val_str)}') + elif re.match(r'^(ПланСчетов|Справочник|Перечисление|Документ|ПланВидовХарактеристик|ПланВидовРасчета|БизнесПроцесс|Задача|РегистрСведений|ПланОбмена)\.', val_str) or re.match(r'^(ChartOfAccounts|Catalog|Enum|Document|ChartOfCharacteristicTypes|ChartOfCalculationTypes|BusinessProcess|Task|InformationRegister|ExchangePlan)\.', val_str): + lines.append(f'{indent}{esc_xml(val_str)}') + else: + lines.append(f'{indent}{esc_xml(val_str)}') + + +def emit_dl_value_type(lines, type_str, indent): + if not type_str: + return + lines.append(f'{indent}') + for part in re.split(r'\s*[|+]\s*', str(type_str)): + emit_single_type(lines, part.strip(), f'{indent}\t') + lines.append(f'{indent}') + + +def emit_dl_available_value(lines, av, type_str, indent): + lines.append(f'{indent}') + av_val = av.get('value') if isinstance(av, dict) else None + emit_dl_value(lines, type_str, av_val, f'{indent}\t', False) + pres = (av.get('presentation') or av.get('title')) if isinstance(av, dict) else None + if pres: + emit_dl_mltext(lines, f'{indent}\t', 'dcssch:presentation', pres) + lines.append(f'{indent}') + + +def emit_dl_input_parameters(lines, ip, indent): + if ip is None: + return + items = ip if isinstance(ip, list) else [ip] + if len(items) == 0: + return + lines.append(f'{indent}') + for item in items: + lines.append(f'{indent}\t') + if 'use' in item and item.get('use') is not None and not item.get('use'): + lines.append(f'{indent}\t\tfalse') + lines.append(f'{indent}\t\t{esc_xml(str(item.get("parameter", "")))}') + if 'choiceParameters' in item: + cp_items = item.get('choiceParameters') or [] + if len(cp_items) == 0: + lines.append(f'{indent}\t\t') + else: + lines.append(f'{indent}\t\t') + for cp in cp_items: + lines.append(f'{indent}\t\t\t') + lines.append(f'{indent}\t\t\t\t{esc_xml(str(cp.get("name", "")))}') + for v in (cp.get('values') or []): + if isinstance(v, bool): + lines.append(f'{indent}\t\t\t\t{"true" if v else "false"}') + elif isinstance(v, (int, float)): + lines.append(f'{indent}\t\t\t\t{v}') + else: + lines.append(f'{indent}\t\t\t\t{esc_xml(str(v))}') + lines.append(f'{indent}\t\t\t') + lines.append(f'{indent}\t\t') + elif 'choiceParameterLinks' in item: + cpl_items = item.get('choiceParameterLinks') or [] + if len(cpl_items) == 0: + lines.append(f'{indent}\t\t') + else: + lines.append(f'{indent}\t\t') + for cpl in cpl_items: + lines.append(f'{indent}\t\t\t') + lines.append(f'{indent}\t\t\t\t{esc_xml(str(cpl.get("name", "")))}') + lines.append(f'{indent}\t\t\t\t{esc_xml(str(cpl.get("value", "")))}') + mode = str(cpl.get('mode') or 'Auto') + lines.append(f'{indent}\t\t\t\t{mode}') + lines.append(f'{indent}\t\t\t') + lines.append(f'{indent}\t\t') + elif 'value' in item: + val = item.get('value') + if isinstance(val, bool): + lines.append(f'{indent}\t\t{"true" if val else "false"}') + elif isinstance(val, (int, float)): + lines.append(f'{indent}\t\t{val}') + elif isinstance(val, dict): + emit_dl_mltext(lines, f'{indent}\t\t', 'dcscor:value', val) + else: + lines.append(f'{indent}\t\t{esc_xml(str(val))}') + lines.append(f'{indent}\t') + lines.append(f'{indent}') + + +# ── dataParameters (значения параметров запроса в настройках компоновки) — порт из skd ── +def _test_empty_value(v): + if v is None: + return True + s = str(v).strip() + return s == '' or s == '_' or s.lower() == 'null' + + +def emit_empty_value(lines, type_str, indent, tag_prefix='', value_list_allowed=False): + if value_list_allowed: + return + t = type_str or '' + t_bare = t[3:] if t.startswith('xs:') else t + pf = tag_prefix + if t == '': + lines.append(f'{indent}<{pf}value xsi:nil="true"/>') + elif t == 'StandardPeriod': + lines.append(f'{indent}<{pf}value xsi:type="v8:StandardPeriod">') + lines.append(f'{indent}\tCustom') + lines.append(f'{indent}\t0001-01-01T00:00:00') + lines.append(f'{indent}\t0001-01-01T00:00:00') + lines.append(f'{indent}') + elif re.match(r'^string', t_bare): + lines.append(f'{indent}<{pf}value xsi:type="xs:string"/>') + elif re.match(r'^(date|time)', t_bare): + lines.append(f'{indent}<{pf}value xsi:type="xs:dateTime">0001-01-01T00:00:00') + elif re.match(r'^decimal', t_bare): + lines.append(f'{indent}<{pf}value xsi:type="xs:decimal">0') + elif t_bare == 'boolean': + lines.append(f'{indent}<{pf}value xsi:type="xs:boolean">false') + else: + lines.append(f'{indent}<{pf}value xsi:nil="true"/>') + + +_DP_PERIOD_VARIANTS = {"Custom","Today","ThisWeek","ThisTenDays","ThisMonth","ThisQuarter","ThisHalfYear","ThisYear","FromBeginningOfThisWeek","FromBeginningOfThisTenDays","FromBeginningOfThisMonth","FromBeginningOfThisQuarter","FromBeginningOfThisHalfYear","FromBeginningOfThisYear","LastWeek","LastTenDays","LastMonth","LastQuarter","LastHalfYear","LastYear","NextDay","NextWeek","NextTenDays","NextMonth","NextQuarter","NextHalfYear","NextYear","TillEndOfThisWeek","TillEndOfThisTenDays","TillEndOfThisMonth","TillEndOfThisQuarter","TillEndOfThisHalfYear","TillEndOfThisYear"} + + +def parse_data_param_shorthand(s): + result = {'parameter': '', 'value': None, 'use': True, 'userSettingID': None, 'viewMode': None} + if '@user' in s: + result['userSettingID'] = 'auto'; s = re.sub(r'\s*@user', '', s) + if '@off' in s: + result['use'] = False; s = re.sub(r'\s*@off', '', s) + if '@quickAccess' in s: + result['viewMode'] = 'QuickAccess'; s = re.sub(r'\s*@quickAccess', '', s) + if '@normal' in s: + result['viewMode'] = 'Normal'; s = re.sub(r'\s*@normal', '', s) + s = s.strip() + m = re.match(r'^([^=]+)=\s*(.+)$', s) + if m: + result['parameter'] = m.group(1).strip() + val_str = m.group(2).strip() + if val_str in _DP_PERIOD_VARIANTS: + result['value'] = {'variant': val_str} + elif re.match(r'^\d{4}-\d{2}-\d{2}T', val_str): + result['value'] = val_str + elif val_str in ('true', 'false'): + result['value'] = (val_str == 'true') + else: + result['value'] = val_str + else: + result['parameter'] = s + return result + + +def emit_data_parameters(lines, items, indent, block_view_mode=None): + if not items or len(items) == 0: + return + lines.append(f'{indent}') + for dp in items: + if isinstance(dp, str): + parsed = parse_data_param_shorthand(dp) + dp = {'parameter': parsed['parameter']} + if parsed['value'] is not None: + dp['value'] = parsed['value'] + if parsed['use'] is False: + dp['use'] = False + if parsed['userSettingID']: + dp['userSettingID'] = parsed['userSettingID'] + if parsed['viewMode']: + dp['viewMode'] = parsed['viewMode'] + lines.append(f'{indent}\t') + if dp.get('use') is False: + lines.append(f'{indent}\t\tfalse') + lines.append(f'{indent}\t\t{esc_xml(str(dp.get("parameter", "")))}') + vtype = str(dp.get('valueType') or '') + val = dp.get('value') + if dp.get('nilValue') is True: + lines.append(f'{indent}\t\t') + elif _test_empty_value(val) and vtype: + emit_empty_value(lines, vtype, f'{indent}\t\t', tag_prefix='dcscor:', value_list_allowed=False) + elif _test_empty_value(val): + pass # нет значения → не эмитим value-узел (form дин-список: use=false плейсхолдер) + elif val is not None: + if isinstance(val, dict) and val.get('variant'): + variant = str(val.get('variant')) + has_date = 'date' in val + has_sd = 'startDate' in val + is_sbd = has_date or (not has_sd and variant.startswith('BeginningOf')) + if is_sbd: + lines.append(f'{indent}\t\t') + lines.append(f'{indent}\t\t\t{esc_xml(variant)}') + if variant == 'Custom': + d = str(val.get('date') or '0001-01-01T00:00:00') + lines.append(f'{indent}\t\t\t{esc_xml(d)}') + lines.append(f'{indent}\t\t') + else: + lines.append(f'{indent}\t\t') + lines.append(f'{indent}\t\t\t{esc_xml(variant)}') + if variant == 'Custom': + sd = str(val.get('startDate') or '0001-01-01T00:00:00') + ed = str(val.get('endDate') or '0001-01-01T00:00:00') + lines.append(f'{indent}\t\t\t{esc_xml(sd)}') + lines.append(f'{indent}\t\t\t{esc_xml(ed)}') + lines.append(f'{indent}\t\t') + elif re.match(r'^[a-zA-Z]+:', vtype): + v_str = str(val).lower() if isinstance(val, bool) else str(val) + lines.append(f'{indent}\t\t{esc_xml(v_str)}') + elif vtype == 'boolean' or isinstance(val, bool): + lines.append(f'{indent}\t\t{esc_xml(str(val).lower())}') + elif re.match(r'^date', vtype) or re.match(r'^\d{4}-\d{2}-\d{2}T', str(val)): + lines.append(f'{indent}\t\t{esc_xml(str(val))}') + elif re.match(r'^decimal', vtype): + lines.append(f'{indent}\t\t{esc_xml(str(val))}') + elif re.match(r'^string', vtype): + lines.append(f'{indent}\t\t{esc_xml(str(val))}') + elif re.match(r'^(ПланСчетов|Справочник|Перечисление|Документ|ПланВидовХарактеристик|ПланВидовРасчета|БизнесПроцесс|Задача|РегистрСведений|ПланОбмена)\.', str(val)) or re.match(r'^(ChartOfAccounts|Catalog|Enum|Document|ChartOfCharacteristicTypes|ChartOfCalculationTypes|BusinessProcess|Task|InformationRegister|ExchangePlan)\.', str(val)): + lines.append(f'{indent}\t\t{esc_xml(str(val))}') + else: + lines.append(f'{indent}\t\t{esc_xml(str(val))}') + if dp.get('viewMode'): + lines.append(f'{indent}\t\t{esc_xml(str(dp["viewMode"]))}') + if dp.get('userSettingID'): + uid = new_uuid() if str(dp['userSettingID']) == 'auto' else str(dp['userSettingID']) + lines.append(f'{indent}\t\t{esc_xml(uid)}') + if dp.get('userSettingPresentation'): + emit_us_presentation(lines, f'{indent}\t\t', 'dcsset:userSettingPresentation', dp['userSettingPresentation']) + lines.append(f'{indent}\t') + if block_view_mode is not None: + lines.append(f'{indent}\t{esc_xml(str(block_view_mode))}') + lines.append(f'{indent}') + + +def emit_dl_parameter(lines, p, parsed, indent): + is_obj = not isinstance(p, str) + lines.append(f'{indent}') + ci = f'{indent}\t' + lines.append(f'{ci}{esc_xml(parsed["name"])}') + # Title: явный override (shorthand [..] / объект title/presentation) или авто из имени. + title = None + if parsed.get('title'): + title = parsed['title'] + elif is_obj and p.get('title'): + title = p['title'] + elif is_obj and p.get('presentation'): + title = p['presentation'] + if title is None or (isinstance(title, str) and title == ''): + title = title_from_name(parsed['name']) + emit_dl_mltext(lines, ci, 'dcssch:title', title) + # valueType + if parsed.get('type'): + emit_dl_value_type(lines, parsed['type'], ci) + # value (дефолт nil; при valueListAllowed пустое — опускаем) + vla = bool(parsed.get('valueListAllowed')) + pv = parsed.get('value') + if isinstance(pv, list): + for v in pv: + emit_dl_value(lines, parsed.get('type', ''), v, ci, False) + elif vla and is_dl_empty_value(pv) and parsed.get('value_explicit'): + # valueListAllowed + явный пустой (value:null от декомпилятора) → платформа пишет nil + lines.append(f'{ci}') + else: + emit_dl_value(lines, parsed.get('type', ''), pv, ci, vla) + # useRestriction — ВСЕГДА; дефолт true; false только при явном useRestriction:false. + ur = True + if is_obj and 'useRestriction' in p: + ur = bool(p['useRestriction']) + lines.append(f'{ci}{"true" if ur else "false"}') + # expression + expr = str(p['expression']) if (is_obj and p.get('expression')) else None + if expr: + lines.append(f'{ci}{esc_xml(expr)}') + # availableValues + if is_obj and p.get('availableValues'): + for av in p['availableValues']: + emit_dl_available_value(lines, av, parsed.get('type', ''), ci) + # valueListAllowed + if vla: + lines.append(f'{ci}true') + # availableAsField=false (hidden или явный) + aaf = None + if parsed.get('hidden') is True: + aaf = False + if is_obj and 'availableAsField' in p: + aaf = bool(p['availableAsField']) + if aaf is False: + lines.append(f'{ci}false') + # inputParameters + if is_obj and p.get('inputParameters'): + emit_dl_input_parameters(lines, p['inputParameters'], ci) + # denyIncompleteValues + if is_obj and p.get('denyIncompleteValues') is True: + lines.append(f'{ci}true') + # use + if is_obj and p.get('use'): + lines.append(f'{ci}{esc_xml(str(p["use"]))}') + lines.append(f'{indent}') + + +def emit_dl_parameters(lines, params, indent): + if not params: + return + for p in params: + if isinstance(p, str): + parsed = parse_dl_param_shorthand(p) + else: + resolved_type = '' + if p.get('type'): + if isinstance(p['type'], list): + resolved_type = ' | '.join(resolve_type_str(str(x)) for x in p['type']) + else: + resolved_type = resolve_type_str(str(p['type'])) + elif p.get('valueType'): + resolved_type = resolve_type_str(str(p['valueType'])) + parsed = {'name': str(p.get('name', '')), 'type': resolved_type, + 'value': p.get('value') if 'value' in p else None, + 'value_explicit': ('value' in p), 'title': None} + if p.get('valueListAllowed') is True: + parsed['valueListAllowed'] = True + if p.get('hidden') is True: + parsed['hidden'] = True + emit_dl_parameter(lines, p, parsed, indent) + + +def emit_attributes(lines, attrs, indent, conditional_appearance=None): + has_ca = bool(conditional_appearance) and len(conditional_appearance) > 0 + # Платформа ВСЕГДА эмитит (100% корпуса; 162 формы — пустой ). + if (not attrs or len(attrs) == 0) and not has_ca: + lines.append(f'{indent}') + return + if not attrs or len(attrs) == 0: + # Нет реквизитов, но есть условное оформление (последний child ) + lines.append(f'{indent}') + emit_conditional_appearance(lines, conditional_appearance, f'{indent}\t', wrap_tag='ConditionalAppearance') + lines.append(f'{indent}') + return + + lines.append(f'{indent}') + seen_attrs = set() + for attr in attrs: + attr_id = new_id() + attr_name = str(attr['name']) + _ensure_unique(attr_name, seen_attrs, 'attribute') + + lines.append(f'{indent}\t') + inner = f'{indent}\t\t' + + # Title атрибута (зеркало emit_title): нет ключа → авто-вывод из имени (кроме main); + # title "" → подавить; непустой → эмитить как есть. + if 'title' in attr: + if attr.get('title'): + emit_mltext(lines, inner, 'Title', attr['title']) + elif attr.get('main') is not True: + emit_mltext(lines, inner, 'Title', title_from_name(attr_name)) + + # Type + if attr.get('type'): + emit_type(lines, str(attr['type']), inner) + else: + lines.append(f'{inner}') + # valueType: ОписаниеТипов значений ValueList → + # (та же грамматика типа, включая составной "A | B"). Forgiving-синонимы. + # Три состояния: нет ключа → нет Settings; "" → пустой ; тип → с типом. + vt_spec = None + has_vt = False + for k in ('valueType', 'typeDescription', 'описаниеТипов', 'типЗначений'): + if k in attr: + vt_spec = attr[k] + has_vt = True + break + if has_vt: + emit_type(lines, '' if vt_spec is None else str(vt_spec), inner, tag="Settings", tag_attrs=' xsi:type="v8:TypeDescription"') + # Planner design-time (встроенный конфиг планировщика). + if attr.get('planner') is not None: + emit_planner_settings(lines, attr['planner'], inner) + # Chart/GanttChart design-time (тип выводится из типа реквизита). + if attr.get('chart') is not None: + ctype = 'd4p1:GanttChart' if 'GanttChart' in str(attr.get('type', '')) else 'd4p1:Chart' + emit_chart_settings(lines, attr['chart'], inner, ctype) + + if attr.get('main') is True: + lines.append(f'{inner}true') + # Доступ по ролям: просмотр/редактирование (порядок схемы: View → Edit, после MainAttribute) + if attr.get('view') is not None: + emit_xr_flag(lines, 'View', attr.get('view'), inner) + if attr.get('edit') is not None: + emit_xr_flag(lines, 'Edit', attr.get('edit'), inner) + main_saved = False + if attr.get('main') is True and attr.get('type'): + t = str(attr['type']) + main_saved = bool(re.match(r'^(CatalogObject|DocumentObject|ChartOfAccountsObject|ChartOfCalculationTypesObject|ChartOfCharacteristicTypesObject|ExchangePlanObject|BusinessProcessObject|TaskObject)\.', t)) or ('RecordManager.' in t) + # Явный ключ savedData побеждает (в т.ч. False → суппресс авто-вывода main_saved); нет ключа → авто. + emit_saved = (attr['savedData'] is True) if 'savedData' in attr else main_saved + if emit_saved: + lines.append(f'{inner}true') + # Save: сохранение значения реквизита в пользовательских настройках. true → имя; + # строка/массив → под-поля с авто-префиксом "имя." (путь с точкой / UUID / =имя — как есть). + # Нет ключа или false → не эмитим. + if 'save' in attr and attr['save'] is not None: + save_fields = [] + sv = attr['save'] + if isinstance(sv, bool): + if sv: + save_fields.append(attr_name) + else: + for e in (sv if isinstance(sv, (list, tuple)) else [sv]): + fld = str(e) + if not fld: + continue + if fld != attr_name and '.' not in fld and not re.match(r'^\d+/\d+', fld): + fld = f'{attr_name}.{fld}' + if fld not in save_fields: + save_fields.append(fld) + if save_fields: + lines.append(f'{inner}') + for f in save_fields: + lines.append(f'{inner}\t{esc_xml(f)}') + lines.append(f'{inner}') + # Проверка заполнения → (реальный тег; в схеме нет). + # bool true → ShowError; строка → verbatim. Синоним fillChecking. + fc_raw = attr['fillCheck'] if 'fillCheck' in attr else attr.get('fillChecking') + if fc_raw: + fcv = 'ShowError' if isinstance(fc_raw, bool) else str(fc_raw) + lines.append(f'{inner}{fcv}') + + # UseAlways: поля, всегда читаемые. Две формы DSL сливаются: + # attr.useAlways[] (короткие имена) + columns с useAlways:true → ИмяРеквизита.Поле. + ua_fields = [] + for e in (attr.get('useAlways') or []): + fld = str(e) + # Префикс "ИмяРеквизита." добавляем к коротким именам. Поля дин-списка с маркером "~" + # (query-поля, ~13% корпуса) — префикс ставится ПОСЛЕ "~": ~Остановлен → ~Список.Остановлен. + # Полная форма (~Список.Остановлен / Список.Остановлен) — verbatim (forgiving ввод). + if fld.startswith('~'): + bare = fld[1:] + if not re.match(r'^' + re.escape(attr_name) + r'\.', bare): + bare = f'{attr_name}.{bare}' + fld = f'~{bare}' + elif not re.match(r'^' + re.escape(attr_name) + r'\.', fld): + fld = f'{attr_name}.{fld}' + if fld not in ua_fields: + ua_fields.append(fld) + for col in (attr.get('columns') or []): + if col.get('useAlways') is True: + fld = f'{attr_name}.{col["name"]}' + if fld not in ua_fields: + ua_fields.append(fld) + if ua_fields: + lines.append(f'{inner}') + for f in ua_fields: + lines.append(f'{inner}\t{f}') + lines.append(f'{inner}') + + emit_functional_options(lines, attr.get('functionalOptions'), inner) + + # Columns: прямые + (доп. колонки табличных частей объекта). + # Прямые сначала, затем AdditionalColumns-группы. Для дин-списка (settings) прямые НЕ эмитим. + has_direct_cols = bool(attr.get('columns')) and len(attr['columns']) > 0 and not attr.get('settings') + has_add_cols = bool(attr.get('additionalColumns')) and len(attr['additionalColumns']) > 0 + if has_direct_cols or has_add_cols: + lines.append(f'{inner}') + if has_direct_cols: + seen_cols = set() # колонки уникальны в пределах своего реквизита + for col in attr['columns']: + _ensure_unique(str(col['name']), seen_cols, f"column of '{attr_name}'") + emit_attr_column(lines, col, f'{inner}\t') + if has_add_cols: + for ac in attr['additionalColumns']: + ac_cols = ac.get('columns') or [] + if not ac_cols: + # Пустая группа доп.колонок (table-ref без колонок) → self-closing (как платформа) + lines.append(f'{inner}\t') + continue + lines.append(f'{inner}\t') + seen_ac_cols = set() # уникальность в пределах группы AdditionalColumns + for col in ac_cols: + _ensure_unique(str(col['name']), seen_ac_cols, f"column of '{attr_name}'") + emit_attr_column(lines, col, f'{inner}\t\t') + lines.append(f'{inner}\t') + lines.append(f'{inner}') + + # Settings (динамический список) + if attr.get('settings'): + s = attr['settings'] + lines.append(f'{inner}') + si = f'{inner}\t' + # Порядок платформы: AutoFillAvailableFields, ManualQuery, DynamicDataRead, QueryText, Field*, MainTable, ListSettings + # AutoFillAvailableFields — дефолт true; эмитим только при заданном ключе (отклонение). + if s.get('autoFillAvailableFields') is not None: + lines.append(f'{si}{"true" if s["autoFillAvailableFields"] else "false"}') + # Порядок платформы: ManualQuery, DynamicDataRead, QueryText, Field*, MainTable, ListSettings + has_query = bool(s.get('query') and str(s['query']).strip()) + mq = 'true' if (has_query or s.get('manualQuery')) else 'false' + lines.append(f'{si}{mq}') + # DynamicDataRead: дефолт true; false только при явном отключении + ddr = 'false' if s.get('dynamicDataRead') is False else 'true' + lines.append(f'{si}{ddr}') + if has_query: + qtext = resolve_query_value(str(s['query']), QUERY_BASE_DIR) + lines.append(f'{si}{esc_xml(qtext)}') + # Явные поля набора (редко): override title/dataPath + if s.get('fields'): + for fld in s['fields']: + # Тип поля набора: DataSetFieldField (дефолт) vs DataSetFieldNestedDataSet + # (поле-вложенный набор = реквизит табличной части; маркер nested). + ftype = 'DataSetFieldNestedDataSet' if fld.get('nested') else 'DataSetFieldField' + lines.append(f'{si}') + dp = fld.get('dataPath') or fld.get('field') + lines.append(f'{si}\t{esc_xml(str(dp))}') + lines.append(f'{si}\t{esc_xml(str(fld.get("field", "")))}') + if fld.get('title'): + lines.append(f'{si}\t') + emit_ml_items(lines, f'{si}\t\t', fld['title']) + lines.append(f'{si}\t') + # Ограничения использования поля — после title, перед presentationExpression + emit_restrict_block(lines, 'useRestriction', fld.get('useRestriction'), f'{si}\t') + emit_restrict_block(lines, 'attributeUseRestriction', fld.get('attributeUseRestriction'), f'{si}\t') + # presentationExpression поля — перед valueType (порядок исходника) + if fld.get('presentationExpression'): + lines.append(f'{si}\t{esc_xml(str(fld["presentationExpression"]))}') + # valueType поля набора (тип значения; вычисляемые/кастомные поля) + if fld.get('valueType'): + emit_dl_value_type(lines, fld['valueType'], f'{si}\t') + # appearance поля (формат/оформление) — после valueType (порядок исходника) + if fld.get('appearance'): + lines.append(f'{si}\t') + for ak, av in fld['appearance'].items(): + emit_appearance_value(lines, ak, av, f'{si}\t\t') + lines.append(f'{si}\t') + # inputParameters поля (связь по параметрам выбора) — в конце + if fld.get('inputParameters'): + emit_dl_input_parameters(lines, fld['inputParameters'], f'{si}\t') + lines.append(f'{si}') + # Вычисляемые поля DataSet () — после Field*, до Parameter*. + emit_calc_fields(lines, s.get('calculatedFields'), si) + # Schema-параметры дин-списка (DataCompositionSchemaParameter) — после Field*, до MainTable. + emit_dl_parameters(lines, s.get('parameters'), si) + # Ключ набора (query-based список без MainTable): KeyType (RowNumber/FieldValue/RowKey) + # + KeyField* — после Parameter*, до MainTable. Захват/эмит факт. значений. + if s.get('keyType'): + lines.append(f'{si}{esc_xml(str(s["keyType"]))}') + if s.get('keyFields'): + for kf in s['keyFields']: + lines.append(f'{si}{esc_xml(str(kf))}') + if s.get('mainTable'): + lines.append(f'{si}{normalize_meta_type_ref(str(s["mainTable"]))}') + # AutoSaveUserSettings — после MainTable (дефолт true; эмитим только при заданном ключе = отклонении). + if s.get('autoSaveUserSettings') is not None: + lines.append(f'{si}{"true" if s["autoSaveUserSettings"] else "false"}') + # ListSettings: filter/order/conditionalAppearance (skd-грамматика) + каноничные блок-GUID. + # Нет items → контейнеры всё равно эмитятся (blockMeta) = каноничный пустой скелет платформы. + lsi = f'{si}\t' + lines.append(f'{si}') + ls_open_idx = len(lines) - 1 # для self-closing, если внутри ничего не эмитнётся + ls_shape = s.get('listSettings') + if ls_shape is not None: + # Частичная/минимальная форма скелета — эмитим ТОЛЬКО указанные части с их блок-метой. + for tag, pv in ls_shape.items(): + # Значение дескриптора: строка-код "vu" ИЛИ объект {meta, presentation} + # (контейнер несёт собственный userSettingPresentation — подпись настройки). + if isinstance(pv, dict): + meta = str(pv.get('meta', '')); bpres = pv.get('presentation') + else: + meta = str(pv); bpres = None + bvm = 'Normal' if 'v' in meta else None + if tag == 'filter': + bus = CANON_FILTER_ID if 'u' in meta else None + emit_filter(lines, s.get('filter'), lsi, block_view_mode=bvm, block_user_setting_id=bus, block_user_setting_presentation=bpres) + elif tag == 'order': + bus = CANON_ORDER_ID if 'u' in meta else None + emit_order(lines, s.get('order'), lsi, block_view_mode=bvm, block_user_setting_id=bus, block_user_setting_presentation=bpres) + elif tag == 'conditionalAppearance': + bus = CANON_CA_ID if 'u' in meta else None + emit_conditional_appearance(lines, s.get('conditionalAppearance'), lsi, block_view_mode=bvm, block_user_setting_id=bus, block_user_setting_presentation=bpres) + elif tag == 'itemsViewMode': + lines.append(f'{lsi}Normal') + elif tag == 'itemsUserSettingID': + lines.append(f'{lsi}{CANON_ITEMS_ID}') + elif tag == 'structure': + emit_list_grouping(lines, get_list_grouping_value(s), lsi) + else: + # Полный каноничный скелет (умолчание, ~93% форм) — без изменений. + emit_filter(lines, s.get('filter'), lsi, block_view_mode='Normal', block_user_setting_id=CANON_FILTER_ID) + # dataParameters — после filter, до order (XSD-порядок ListSettings) + if 'dataParameters' in s: + emit_data_parameters(lines, s.get('dataParameters'), lsi) + emit_order(lines, s.get('order'), lsi, block_view_mode='Normal', block_user_setting_id=CANON_ORDER_ID) + emit_conditional_appearance(lines, s.get('conditionalAppearance'), lsi, block_view_mode='Normal', block_user_setting_id=CANON_CA_ID) + # Группировка строк списка (авторинг без round-trip дескриптора) — после CA, до itemsViewMode + emit_list_grouping(lines, get_list_grouping_value(s), lsi) + lines.append(f'{lsi}Normal') + lines.append(f'{lsi}{CANON_ITEMS_ID}') + if len(lines) - 1 == ls_open_idx: + # Пустой дескриптор listSettings:{} (оригинал = ) → зеркалим self-closing. + lines[ls_open_idx] = f'{si}' + else: + lines.append(f'{si}') + lines.append(f'{inner}') + + lines.append(f'{indent}\t') + # Условное оформление формы — последний child (та же DCS-грамматика, что settings CA) + emit_conditional_appearance(lines, conditional_appearance, f'{indent}\t', wrap_tag='ConditionalAppearance') + lines.append(f'{indent}') + + +# --- Parameter emitter --- + +def emit_parameters(lines, params, indent): + if not params or len(params) == 0: + return + + lines.append(f'{indent}') + seen_params = set() + for param in params: + _ensure_unique(str(param['name']), seen_params, 'parameter') + lines.append(f'{indent}\t') + inner = f'{indent}\t\t' + + emit_type(lines, str(param.get('type', '')), inner) + + if param.get('key') is True: + lines.append(f'{inner}true') + + lines.append(f'{indent}\t') + lines.append(f'{indent}') + + +# --- Command emitter --- + +def emit_commands(lines, cmds, indent): + if not cmds or len(cmds) == 0: + return + + lines.append(f'{indent}') + seen_cmds = set() + for cmd in cmds: + cmd_id = new_id() + _ensure_unique(str(cmd['name']), seen_cmds, 'command') + lines.append(f'{indent}\t') + inner = f'{indent}\t\t' + + # Заголовок команды (зеркало emit_title): ключ есть+непустой → эмитим; ключ есть+"" → суппресс + # (в оригинале нет — не додумывать); ключ отсутствует → авто-вывод из имени. + if 'title' in cmd: + if cmd['title']: + emit_mltext(lines, inner, 'Title', cmd['title']) + else: + cmd_title = title_from_name(str(cmd['name'])) + if cmd_title: + emit_mltext(lines, inner, 'Title', cmd_title) + + if cmd.get('tooltip'): + emit_mltext(lines, inner, 'ToolTip', cmd['tooltip']) + + # Доступность команды по ролям (после ToolTip, до Action) + if cmd.get('use') is not None: + emit_xr_flag(lines, 'Use', cmd.get('use'), inner) + + if cmd.get('action'): + lines.append(f'{inner}<Action>{cmd["action"]}</Action>') + + if cmd.get('modifiesSavedData') is True: + lines.append(f'{inner}<ModifiesSavedData>true</ModifiesSavedData>') + + emit_functional_options(lines, cmd.get('functionalOptions'), inner) + + if cmd.get('currentRowUse'): + lines.append(f'{inner}<CurrentRowUse>{cmd["currentRowUse"]}</CurrentRowUse>') + + # Используемая таблица — имя элемента-таблицы (xsi:type обязателен). + # Forgiving-ключи: table / associatedTableElementId (XML-тег) / ИспользуемаяТаблица (рус., регистр-незав.) + _cmd_norm = {k.replace(' ', '').lower(): v for k, v in cmd.items()} + cmd_table = (_cmd_norm.get('table') or _cmd_norm.get('associatedtableelementid') + or _cmd_norm.get('используемаятаблица')) + if cmd_table: + lines.append(f'{inner}<AssociatedTableElementId xsi:type="xs:string">{esc_xml(str(cmd_table))}</AssociatedTableElementId>') + + if cmd.get('shortcut'): + lines.append(f'{inner}<Shortcut>{cmd["shortcut"]}</Shortcut>') + + emit_command_picture(lines, cmd.get('picture'), cmd.get('loadTransparent'), inner) + + if cmd.get('representation'): + lines.append(f'{inner}<Representation>{cmd["representation"]}</Representation>') + + lines.append(f'{indent}\t</Command>') + lines.append(f'{indent}</Commands>') + + +# Командный интерфейс формы (<CommandInterface>): панели CommandBar + NavigationPanel. +# Элемент: строка (голый command, Type=Auto) или dict. Порядок тегов: +# Command, Type(деф. Auto), Attribute, CommandGroup, Index, DefaultVisible, Visible(xr-flag). +def _resolve_command_group_key(key, panel_tag): + """Ключ-группа древовидной формы → CommandGroup (зависит от панели); иначе verbatim.""" + k = re.sub(r'\s', '', str(key)).lower() + if panel_tag == 'NavigationPanel': + m = {'important': 'FormNavigationPanelImportant', 'важное': 'FormNavigationPanelImportant', + 'goto': 'FormNavigationPanelGoTo', 'перейти': 'FormNavigationPanelGoTo', + 'seealso': 'FormNavigationPanelSeeAlso', 'смтакже': 'FormNavigationPanelSeeAlso'} + else: + m = {'important': 'FormCommandBarImportant', 'важное': 'FormCommandBarImportant', + 'createbasedon': 'FormCommandBarCreateBasedOn', 'создатьнаосновании': 'FormCommandBarCreateBasedOn'} + return m.get(k, key) + + +def emit_command_interface(lines, ci, indent): + if not ci: + return + inner = f'{indent}\t' + panels = [ + ('CommandBar', ('commandBar', 'команднаяПанель', 'КоманднаяПанель')), + ('NavigationPanel', ('navigationPanel', 'панельНавигации', 'ПанельНавигации')), + ] + present = [] + for tag, syns in panels: + items = None + for syn in syns: + if isinstance(ci, dict) and syn in ci: + items = ci[syn] + break + if items is not None: + present.append((tag, items)) + if not present: + return + lines.append(f'{indent}<CommandInterface>') + for tag, items in present: + lines.append(f'{inner}<{tag}>') + # Нормализация: плоский список пар (элемент, group-из-дерева). dict → древовидная форма. + flat = [] + if isinstance(items, dict): + for gkey, gitems in items.items(): + grp_tree = _resolve_command_group_key(gkey, tag) + for it in gitems: + flat.append((it, grp_tree)) + else: + for it in items: + flat.append((it, None)) + for item, tree_group in flat: + if isinstance(item, str): + cmd, typ, attr, grp, idx, dv, vis = item, 'Auto', None, None, None, None, None + else: + cmd = get_el_prop(item, ('command', 'команда')) + typ = get_el_prop(item, ('type', 'тип')) or 'Auto' + attr = get_el_prop(item, ('attribute', 'реквизит')) + grp = get_el_prop(item, ('group', 'группа', 'группаКоманд')) + idx = get_el_prop(item, ('index', 'индекс')) + dv = get_el_prop(item, ('defaultVisible', 'видимость', 'видимостьПоУмолчанию')) + vis = get_el_prop(item, ('visible', 'видимостьПоРолям', 'настройкаВидимости')) + # group из дерева побеждает (если задан и непустой); явный group элемента — фолбэк + if tree_group: + grp = tree_group + lines.append(f'{inner}\t<Item>') + lines.append(f'{inner}\t\t<Command>{esc_xml(str(cmd))}</Command>') + lines.append(f'{inner}\t\t<Type>{typ}</Type>') + if attr: + lines.append(f'{inner}\t\t<Attribute>{esc_xml(str(attr))}</Attribute>') + if grp: + lines.append(f'{inner}\t\t<CommandGroup>{esc_xml(str(grp))}</CommandGroup>') + if idx is not None: + lines.append(f'{inner}\t\t<Index>{idx}</Index>') + if dv is not None: + lines.append(f'{inner}\t\t<DefaultVisible>{"true" if dv else "false"}</DefaultVisible>') + if vis is not None: + emit_xr_flag(lines, 'Visible', vis, f'{inner}\t\t') + lines.append(f'{inner}\t</Item>') + lines.append(f'{inner}</{tag}>') + lines.append(f'{indent}</CommandInterface>') + + +# --- Properties emitter --- + +PROP_MAP = { + "autoTitle": "AutoTitle", + "windowOpeningMode": "WindowOpeningMode", + "commandBarLocation": "CommandBarLocation", + "saveDataInSettings": "SaveDataInSettings", + "autoSaveDataInSettings": "AutoSaveDataInSettings", + "autoTime": "AutoTime", + "usePostingMode": "UsePostingMode", + "repostOnWrite": "RepostOnWrite", + "autoURL": "AutoURL", + "autoFillCheck": "AutoFillCheck", + "customizable": "Customizable", + "enterKeyBehavior": "EnterKeyBehavior", + "verticalScroll": "VerticalScroll", + "scalingMode": "ScalingMode", + "useForFoldersAndItems": "UseForFoldersAndItems", + "reportResult": "ReportResult", + "detailsData": "DetailsData", + "reportFormType": "ReportFormType", + "autoShowState": "AutoShowState", + "width": "Width", + "height": "Height", + "group": "Group", +} + + +def emit_properties(lines, props, indent): + if not props: + return + + for p_name, p_value in props.items(): + xml_name = PROP_MAP.get(p_name) + if not xml_name: + # Auto PascalCase + xml_name = p_name[0].upper() + p_name[1:] + + # Пустая строка = суппресс-маркер (напр. autoTitle:"" — не эмитить и не додумывать) + if isinstance(p_value, str) and p_value == '': + continue + # Convert boolean to lowercase + if isinstance(p_value, bool): + val = 'true' if p_value else 'false' + else: + val = str(p_value) + lines.append(f'{indent}<{xml_name}>{val}</{xml_name}>') + + + +def detect_format_version(d): + while d: + cfg_path = os.path.join(d, "Configuration.xml") + if os.path.isfile(cfg_path): + with open(cfg_path, "r", encoding="utf-8-sig") as f: + head = f.read(2000) + m = re.search(r'<MetaDataObject[^>]+version="(\d+\.\d+)"', head) + if m: + return m.group(1) + parent = os.path.dirname(d) + if parent == d: + break + d = parent + return "2.17" + + +def _normalize_elements(defn): + """Convert dict-style elements from --from-object generators to list-style expected by compiler. + Generator format: elements = {"ИмяЭлемента": {"element": "input", "path": "..."}, ...} + Compiler format: elements = [{"input": "ИмяЭлемента", "path": "..."}, ...] + Also handles nested 'elements' in groups and 'columns' in tables recursively. + """ + def convert_elements(els): + if isinstance(els, list): + # Already list format — but may have nested dicts inside groups + result = [] + for el in els: + if isinstance(el, dict): + el = dict(el) # copy + if 'elements' in el and isinstance(el['elements'], dict): + el['elements'] = convert_elements(el['elements']) + if 'columns' in el and isinstance(el['columns'], dict): + el['columns'] = convert_columns(el['columns']) + result.append(el) + return result + if isinstance(els, dict): + result = [] + for name, props in els.items(): + if not isinstance(props, dict): + continue + new_el = {} + el_type = props.get('element', 'input') + # Map element type to the key name used in JSON DSL + type_map = { + 'input': 'input', 'check': 'check', 'labelField': 'labelField', + 'table': 'table', 'group': 'group', 'pages': 'pages', + 'page': 'page', 'label': 'label', 'button': 'button', + 'checkBox': 'check', 'radioButton': 'radioButton', + 'pictureField': 'pictureField', + } + mapped_type = type_map.get(el_type, el_type) + new_el[mapped_type] = name + for k, v in props.items(): + if k == 'element': + continue + if k == 'elements' and isinstance(v, dict): + new_el['elements'] = convert_elements(v) + elif k == 'columns' and isinstance(v, dict): + new_el['columns'] = convert_columns(v) + elif k == 'groupType': + # groupType → group property in DSL + new_el['group'] = v + elif k == 'showTitle': + new_el['showTitle'] = v + elif k == 'representation': + new_el['representation'] = v + elif k == 'autoCommandBar': + new_el['autoCommandBar'] = v + elif k == 'commandBarLocation': + new_el['commandBarLocation'] = v + else: + new_el[k] = v + result.append(new_el) + return result + return els + + def convert_columns(cols): + if isinstance(cols, list): + return cols + if isinstance(cols, dict): + result = [] + for name, props in cols.items(): + if not isinstance(props, dict): + continue + new_col = {} + el_type = props.get('element', 'input') + type_map = { + 'input': 'input', 'check': 'check', 'labelField': 'labelField', + 'checkBox': 'check', + } + mapped_type = type_map.get(el_type, el_type) + new_col[mapped_type] = name + for k, v in props.items(): + if k == 'element': + continue + new_col[k] = v + result.append(new_col) + return result + return cols + + if 'elements' in defn: + defn['elements'] = convert_elements(defn['elements']) + return defn + + +def main(): + sys.stdout.reconfigure(encoding="utf-8") + sys.stderr.reconfigure(encoding="utf-8") + global _next_id + + parser = argparse.ArgumentParser(description='Compile 1C managed form from JSON or object metadata', allow_abbrev=False) + parser.add_argument('-JsonPath', type=str, default=None) + parser.add_argument('-OutputPath', type=str, required=True) + parser.add_argument('-FromObject', action='store_true', default=False) + parser.add_argument('-ObjectPath', type=str, default=None) + parser.add_argument('-Purpose', type=str, default=None) + parser.add_argument('-Preset', type=str, default='erp-standard') + parser.add_argument('-EmitDsl', type=str, default=None) + args = parser.parse_args() + + # Form name -> purpose mapping + _FORM_NAME_TO_PURPOSE = { + '\u0424\u043e\u0440\u043c\u0430\u0414\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430': 'Item', # ФормаДокумента + '\u0424\u043e\u0440\u043c\u0430\u042d\u043b\u0435\u043c\u0435\u043d\u0442\u0430': 'Item', # ФормаЭлемента + '\u0424\u043e\u0440\u043c\u0430\u0421\u043f\u0438\u0441\u043a\u0430': 'List', # ФормаСписка + '\u0424\u043e\u0440\u043c\u0430\u0412\u044b\u0431\u043e\u0440\u0430': 'Choice', # ФормаВыбора + '\u0424\u043e\u0440\u043c\u0430\u0413\u0440\u0443\u043f\u043f\u044b': 'Folder', # ФормаГруппы + '\u0424\u043e\u0440\u043c\u0430\u0417\u0430\u043f\u0438\u0441\u0438': 'Record', # ФормаЗаписи + '\u0424\u043e\u0440\u043c\u0430\u0421\u0447\u0435\u0442\u0430': 'Item', # ФормаСчета + '\u0424\u043e\u0440\u043c\u0430\u0423\u0437\u043b\u0430': 'Item', # ФормаУзла + } + + # Mutual exclusion validation + if args.FromObject and args.JsonPath: + print("Cannot use both -JsonPath and -FromObject. Choose one mode.", file=sys.stderr) + sys.exit(1) + if not args.FromObject and not args.JsonPath: + print("Either -JsonPath or -FromObject is required.", file=sys.stderr) + sys.exit(1) + + # Normalize OutputPath in from-object mode: append /Ext/Form.xml if missing + if args.FromObject: + out_norm = args.OutputPath.rstrip('/\\') + if not re.search(r'[/\\]Ext[/\\]Form\.xml$', out_norm): + if re.search(r'[/\\]Ext$', out_norm): + args.OutputPath = out_norm + '/Form.xml' + else: + args.OutputPath = out_norm + '/Ext/Form.xml' + print(f"[resolved] OutputPath -> {args.OutputPath}") + + # --- Detect XML format version --- + out_path_resolved = args.OutputPath if os.path.isabs(args.OutputPath) else os.path.join(os.getcwd(), args.OutputPath) + format_version = detect_format_version(os.path.dirname(out_path_resolved)) + + # --- 0. From-object mode --- + if args.FromObject: + # Resolve object path and purpose from OutputPath convention: + # .../TypePlural/ObjectName/Forms/FormName/Ext/Form.xml + out_abs = out_path_resolved + parts = re.split(r'[/\\]', out_abs) + forms_idx = -1 + for i in range(len(parts) - 1, -1, -1): + if parts[i] == 'Forms': + forms_idx = i + break + + resolved_object_path = None + resolved_purpose = None + + if forms_idx >= 2: + form_name = parts[forms_idx + 1] + object_name = parts[forms_idx - 1] + type_plural_and_above = os.sep.join(parts[:forms_idx - 1]) + + if form_name in _FORM_NAME_TO_PURPOSE: + resolved_purpose = _FORM_NAME_TO_PURPOSE[form_name] + + candidate = os.path.join(type_plural_and_above, f'{object_name}.xml') + if os.path.exists(candidate): + resolved_object_path = candidate + + # Apply: explicit -ObjectPath / -Purpose override resolved + from_obj_path = None + if args.ObjectPath: + from_obj_path = args.ObjectPath if os.path.isabs(args.ObjectPath) else os.path.join(os.getcwd(), args.ObjectPath) + if not from_obj_path.endswith('.xml'): + from_obj_path += '.xml' + elif resolved_object_path: + from_obj_path = resolved_object_path + print(f"[resolved] ObjectPath -> {from_obj_path}") + else: + print("Cannot derive object path from OutputPath. Use -ObjectPath explicitly.", file=sys.stderr) + sys.exit(1) + + if not os.path.exists(from_obj_path): + print(f"Object file not found: {from_obj_path}", file=sys.stderr) + sys.exit(1) + + purpose = args.Purpose or resolved_purpose or 'Item' + if resolved_purpose and not args.Purpose: + print(f"[resolved] Purpose -> {purpose}") + + meta = parse_object_meta(from_obj_path) + print(f"[from-object] Type={meta['Type']}, Name={meta['Name']}, Attrs={len(meta['Attributes'])}, TS={len(meta['TabularSections'])}") + + preset_data = load_preset(args.Preset, os.path.dirname(os.path.abspath(__file__)), out_path_resolved) + + supported = { + 'Document': ['Item', 'List', 'Choice'], + 'Catalog': ['Item', 'Folder', 'List', 'Choice'], + 'InformationRegister': ['Record', 'List'], + 'AccumulationRegister': ['List'], + 'ChartOfCharacteristicTypes': ['Item', 'Folder', 'List', 'Choice'], + 'ExchangePlan': ['Item', 'List', 'Choice'], + 'ChartOfAccounts': ['Item', 'Folder', 'List', 'Choice'], + } + if meta['Type'] not in supported: + print(f"Object type '{meta['Type']}' not supported. Supported: Document, Catalog, InformationRegister, AccumulationRegister, ChartOfCharacteristicTypes, ExchangePlan, ChartOfAccounts.", file=sys.stderr) + sys.exit(1) + if purpose not in supported[meta['Type']]: + print(f"Purpose '{purpose}' not valid for {meta['Type']}. Valid: {', '.join(supported[meta['Type']])}", file=sys.stderr) + sys.exit(1) + + dsl_dispatch = { + 'Document': generate_document_dsl, + 'Catalog': generate_catalog_dsl, + 'InformationRegister': generate_information_register_dsl, + 'AccumulationRegister': generate_accumulation_register_dsl, + 'ChartOfCharacteristicTypes': generate_chart_of_characteristic_types_dsl, + 'ExchangePlan': generate_exchange_plan_dsl, + 'ChartOfAccounts': generate_chart_of_accounts_dsl, + } + dsl = dsl_dispatch[meta['Type']](meta, preset_data, purpose) + + if args.EmitDsl: + dsl_path = args.EmitDsl if os.path.isabs(args.EmitDsl) else os.path.join(os.getcwd(), args.EmitDsl) + os.makedirs(os.path.dirname(dsl_path) or '.', exist_ok=True) + with open(dsl_path, 'w', encoding='utf-8') as f: + json.dump(dsl, f, ensure_ascii=False, indent=2) + print(f"[from-object] DSL saved: {dsl_path}") + + defn = json.loads(json.dumps(dsl)) # normalize OrderedDict to regular dict + # Convert dict-style elements (from generators) to list-style (expected by compiler) + defn = _normalize_elements(defn) + else: + # --- 1. Load and validate JSON --- + json_path = args.JsonPath + if not os.path.exists(json_path): + print(f"File not found: {json_path}", file=sys.stderr) + sys.exit(1) + + with open(json_path, 'r', encoding='utf-8-sig') as f: + defn = json.load(f) + global QUERY_BASE_DIR + QUERY_BASE_DIR = os.path.dirname(os.path.abspath(json_path)) + + # --- 1b. Pre-pass: synonyms, main attribute inference, heuristics, autoCmdBar extraction --- + def _normalize_synonyms(el): + if not isinstance(el, dict): + return + # Companion-панели (объект/массив-значение) → commandBar/contextMenu + normalize_panel_synonyms(el) + # Тип-синонимы: commandBar/autoCommandBar → элемент-тип ТОЛЬКО при строковом значении + synonyms = {'commandBar': 'cmdBar', 'autoCommandBar': 'autoCmdBar', 'extTooltip': 'extendedTooltip'} + for src, dst in synonyms.items(): + if src in el and dst not in el: + if src in STR_ONLY_TYPE_SYNONYMS and not isinstance(el[src], str): + continue + el[dst] = el.pop(src) + # Рекурсия в детей панелей (commandBar/contextMenu) + for pk in ('commandBar', 'contextMenu'): + pv = el.get(pk) + kids = pv if isinstance(pv, list) else (pv.get('children') if isinstance(pv, dict) else None) + if isinstance(kids, list): + for child in kids: + _normalize_synonyms(child) + if isinstance(el.get('children'), list): + for child in el['children']: + _normalize_synonyms(child) + if isinstance(el.get('columns'), list): + for child in el['columns']: + _normalize_synonyms(child) + + def _has_cmd_bar_recursive(el): + if not isinstance(el, dict): + return False + if el.get('cmdBar') is not None: + return True + if isinstance(el.get('children'), list): + for child in el['children']: + if _has_cmd_bar_recursive(child): + return True + if isinstance(el.get('columns'), list): + for child in el['columns']: + if _has_cmd_bar_recursive(child): + return True + return False + + def _apply_dlist_table_heuristic(el, list_name, has_main_table): + if not isinstance(el, dict): + return + if el.get('table') is not None and str(el.get('path', '')) == list_name: + # Маркер дин-список-таблицы → emit_table эмитит блок свойств + el['_dynList'] = True + if 'tableAutofill' not in el: + el['tableAutofill'] = False + if 'commandBarLocation' not in el: + el['commandBarLocation'] = 'None' + # RowPictureDataPath: умный дефолт <Список>.DefaultPicture, если ключ ОТСУТСТВУЕТ. + # Декомпилятор опускает при rpdp == smart-default; реальное отсутствие → ""-маркер (не + # перезатирается). Гейт has_main_table снят: дин-список без mainTable тоже несёт RowPictureDataPath. + if 'rowPictureDataPath' not in el: + el['rowPictureDataPath'] = f'{list_name}.DefaultPicture' + if isinstance(el.get('children'), list): + for child in el['children']: + _apply_dlist_table_heuristic(child, list_name, has_main_table) + + def _is_object_like_type(t): + if not t: + return False + if t == 'DynamicList' or t == 'ConstantsSet': + return True + object_suffixes = ( + 'CatalogObject', 'DocumentObject', 'DataProcessorObject', 'ReportObject', + 'ExternalDataProcessorObject', 'ExternalReportObject', 'BusinessProcessObject', + 'TaskObject', 'ChartOfAccountsObject', 'ChartOfCharacteristicTypesObject', + 'ChartOfCalculationTypesObject', 'ExchangePlanObject', + ) + record_set_prefixes = ( + 'InformationRegisterRecordSet', 'AccumulationRegisterRecordSet', + 'AccountingRegisterRecordSet', 'CalculationRegisterRecordSet', + 'InformationRegisterRecordManager', + ) + for s in object_suffixes: + if t.startswith(s + '.'): + return True + for s in record_set_prefixes: + if t.startswith(s + '.'): + return True + return False + + # 1b.1: Normalize synonyms recursively + if isinstance(defn.get('elements'), list): + for el in defn['elements']: + _normalize_synonyms(el) + + # 1b.2: Extract autoCmdBar element from defn['elements'] + main_acb_def = None + if isinstance(defn.get('elements'), list): + auto_bars = [el for el in defn['elements'] if isinstance(el, dict) and el.get('autoCmdBar') is not None] + if len(auto_bars) > 1: + print(f"form-compile: more than one autoCmdBar in def.elements (found {len(auto_bars)}); only one allowed.", file=sys.stderr) + sys.exit(1) + if len(auto_bars) == 1: + main_acb_def = auto_bars[0] + defn['elements'] = [el for el in defn['elements'] if el is not main_acb_def] + + # 1b.3: Infer main attribute + if isinstance(defn.get('attributes'), list): + has_explicit_main = any(a.get('main') is True for a in defn['attributes'] if isinstance(a, dict)) + if not has_explicit_main: + candidates = [] + for a in defn['attributes']: + if not isinstance(a, dict): + continue + if 'main' in a and a.get('main') is False: + continue + if _is_object_like_type(str(a.get('type', ''))): + candidates.append(a) + if len(candidates) == 1: + candidates[0]['main'] = True + print(f"[INFO] Inferred main attribute: {candidates[0].get('name')} ({candidates[0].get('type')})") + elif len(candidates) > 1: + names = ', '.join(c.get('name', '') for c in candidates) + print(f"[WARN] Multiple main-attribute candidates: {names}; specify \"main\": true explicitly") + + # 1b.4: DynamicList → table heuristic (для ВСЕХ DynamicList-реквизитов, не только main) + if isinstance(defn.get('attributes'), list) and isinstance(defn.get('elements'), list): + for attr in defn['attributes']: + if not isinstance(attr, dict) or str(attr.get('type', '')) != 'DynamicList': + continue + settings = attr.get('settings') or {} + has_mt = bool(isinstance(settings, dict) and settings.get('mainTable')) + for el in defn['elements']: + _apply_dlist_table_heuristic(el, attr.get('name', ''), has_mt) + + # 1b.5: Compute main AutoCommandBar Autofill (B3) + def _compute_main_acb_autofill(): + if main_acb_def is not None: + if 'autofill' in main_acb_def: + return bool(main_acb_def.get('autofill')) + return True + if isinstance(defn.get('elements'), list): + for el in defn['elements']: + if _has_cmd_bar_recursive(el): + return False + return True + + # --- 2. Main compilation --- + _next_id = 0 + _seen_element_names.clear() # пул имён элементов (на случай повторного вызова в одном процессе) + lines = [] + + lines.append('<?xml version="1.0" encoding="UTF-8"?>') + lines.append(f'<Form xmlns="http://v8.1c.ru/8.3/xcf/logform" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:dcscor="http://v8.1c.ru/8.1/data-composition-system/core" xmlns:dcssch="http://v8.1c.ru/8.1/data-composition-system/schema" xmlns:dcsset="http://v8.1c.ru/8.1/data-composition-system/settings" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="{format_version}">') + + # Title + form_title = defn.get('title') + if not form_title and defn.get('properties') and defn['properties'].get('title'): + form_title = defn['properties']['title'] + if form_title: + emit_mltext(lines, '\t', 'Title', form_title) + + # Properties (skip 'title' — handled above) + # When form-level Title is set, default autoTitle=false (≈95% of ERP forms do this; + # otherwise platform appends synonym → "Title: Synonym" double-titles). + props_src = defn.get('properties') or {} + props_clone = OrderedDict() + if form_title and 'autoTitle' not in props_src: + props_clone['autoTitle'] = False + for k, v in props_src.items(): + if k != 'title': + props_clone[k] = v + emit_properties(lines, props_clone, '\t') + + # CommandSet (excluded commands) + if defn.get('excludedCommands') and len(defn['excludedCommands']) > 0: + lines.append('\t<CommandSet>') + for cmd in defn['excludedCommands']: + lines.append(f'\t\t<ExcludedCommand>{cmd}</ExcludedCommand>') + lines.append('\t</CommandSet>') + + # MobileDeviceCommandBarContent — форменный список имён командных панелей/кнопок + # (Presentation пустой, CheckState=0, тип xs:string — константы; варьируется только имя-Value). + # 12 форм корпуса несут один пустой item (Value="") — список присутствует, но не пуст по len. + if defn.get('mobileCommandBarContent') is not None and len(defn['mobileCommandBarContent']) > 0: + lines.append('\t<MobileDeviceCommandBarContent>') + for nm in defn['mobileCommandBarContent']: + lines.append('\t\t<xr:Item>') + lines.append('\t\t\t<xr:Presentation/>') + lines.append('\t\t\t<xr:CheckState>0</xr:CheckState>') + # пустое значение → самозакрывающийся тег (зеркало платформы) + if not str(nm): + lines.append('\t\t\t<xr:Value xsi:type="xs:string"/>') + else: + lines.append(f'\t\t\t<xr:Value xsi:type="xs:string">{esc_xml(str(nm))}</xr:Value>') + lines.append('\t\t</xr:Item>') + lines.append('\t</MobileDeviceCommandBarContent>') + + # AutoCommandBar (always present, id=-1) + acb_autofill = _compute_main_acb_autofill() + acb_name = '\u0424\u043e\u0440\u043c\u0430\u041a\u043e\u043c\u0430\u043d\u0434\u043d\u0430\u044f\u041f\u0430\u043d\u0435\u043b\u044c' + acb_halign = None + if main_acb_def is not None: + v = main_acb_def.get('autoCmdBar') + if v: + acb_name = str(v) + if main_acb_def.get('name'): + acb_name = str(main_acb_def['name']) + if main_acb_def.get('horizontalAlign'): + acb_halign = str(main_acb_def['horizontalAlign']) + has_acb_children = bool(main_acb_def and isinstance(main_acb_def.get('children'), list) and len(main_acb_def['children']) > 0) + has_inner = bool(acb_halign) or (not acb_autofill) or has_acb_children + if has_inner: + lines.append(f'\t<AutoCommandBar name="{acb_name}" id="-1">') + if acb_halign: + lines.append(f'\t\t<HorizontalAlign>{acb_halign}</HorizontalAlign>') + if not acb_autofill: + lines.append('\t\t<Autofill>false</Autofill>') + if has_acb_children: + lines.append('\t\t<ChildItems>') + for child in main_acb_def['children']: + emit_element(lines, child, '\t\t\t', in_cmd_bar=True) + lines.append('\t\t</ChildItems>') + lines.append('\t</AutoCommandBar>') + else: + lines.append(f'\t<AutoCommandBar name="{acb_name}" id="-1"/>') + + # Events + if defn.get('events'): + for evt_name in defn['events']: + if evt_name not in KNOWN_FORM_EVENTS: + print(f"[WARN] Unknown form event '{evt_name}'. Known: {', '.join(KNOWN_FORM_EVENTS)}") + lines.append('\t<Events>') + for evt_name, evt_handler in defn['events'].items(): + lines.append(f'\t\t<Event name="{evt_name}">{evt_handler}</Event>') + lines.append('\t</Events>') + + # ChildItems (elements) + if defn.get('elements') and len(defn['elements']) > 0: + lines.append('\t<ChildItems>') + for el in defn['elements']: + emit_element(lines, el, '\t\t') + lines.append('\t</ChildItems>') + + # Attributes + emit_attributes(lines, defn.get('attributes'), '\t', conditional_appearance=defn.get('conditionalAppearance')) + + # Parameters + emit_parameters(lines, defn.get('parameters'), '\t') + + # Commands + emit_commands(lines, defn.get('commands'), '\t') + + # CommandInterface (командный интерфейс формы — последний дочерний Form) + emit_command_interface(lines, defn.get('commandInterface'), '\t') + + # Close + lines.append('</Form>') + + # --- 3. Write output --- + out_path = args.OutputPath + if not os.path.isabs(out_path): + out_path = os.path.join(os.getcwd(), out_path) + out_dir = os.path.dirname(out_path) + if out_dir and not os.path.exists(out_dir): + os.makedirs(out_dir, exist_ok=True) + + content = '\n'.join(lines) + '\n' + write_utf8_bom(out_path, content) + + # --- 4. Auto-register form in parent object XML --- + # Infer parent from OutputPath: .../TypePlural/ObjectName/Forms/FormName/Ext/Form.xml + form_xml_dir = os.path.dirname(out_path) # Ext + form_name_dir = os.path.dirname(form_xml_dir) # FormName + forms_dir = os.path.dirname(form_name_dir) # Forms + object_dir = os.path.dirname(forms_dir) # ObjectName + type_plural_dir = os.path.dirname(object_dir) # TypePlural + + form_name = os.path.basename(form_name_dir) + object_name = os.path.basename(object_dir) + forms_leaf = os.path.basename(forms_dir) + + if forms_leaf == 'Forms': + object_xml_path = os.path.join(type_plural_dir, f'{object_name}.xml') + if os.path.exists(object_xml_path): + with open(object_xml_path, 'r', encoding='utf-8-sig') as f: + raw_text = f.read() + + # Check if already registered + if f'<Form>{form_name}</Form>' not in raw_text: + # Insert before </ChildObjects> + if '</ChildObjects>' in raw_text: + insert_line = f'\t\t\t<Form>{form_name}</Form>\n' + raw_text = raw_text.replace('</ChildObjects>', insert_line + '\t\t</ChildObjects>', 1) + elif '<ChildObjects/>' in raw_text: + replacement = f'<ChildObjects>\n\t\t\t<Form>{form_name}</Form>\n\t\t</ChildObjects>' + raw_text = raw_text.replace('<ChildObjects/>', replacement, 1) + + write_utf8_bom(object_xml_path, raw_text) + print(f" Registered: <Form>{form_name}</Form> in {object_name}.xml") + + # --- 5. Summary --- + el_count = _next_id + print(f"[OK] Compiled: {args.OutputPath}") + print(f" Elements+IDs: {el_count}") + if defn.get('attributes'): + print(f" Attributes: {len(defn['attributes'])}") + if defn.get('commands'): + print(f" Commands: {len(defn['commands'])}") + if defn.get('parameters'): + print(f" Parameters: {len(defn['parameters'])}") + + +if __name__ == '__main__': + main() diff --git a/.claude/skills/form-decompile/scripts/form-decompile.ps1 b/.claude/skills/form-decompile/scripts/form-decompile.ps1 index 510e3eba..f219b1ec 100644 --- a/.claude/skills/form-decompile/scripts/form-decompile.ps1 +++ b/.claude/skills/form-decompile/scripts/form-decompile.ps1 @@ -1,4 +1,4 @@ -# form-decompile v0.126 — Decompile 1C managed Form.xml to JSON DSL (draft) +# form-decompile v0.127 — Decompile 1C managed Form.xml to JSON DSL (draft) # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # ВНИМАНИЕ: раундтрип не гарантируется. Навык исключён из авто-использования моделью. param( @@ -134,7 +134,14 @@ function Get-ListSettingsShape { if ($tag -in @('filter','order','conditionalAppearance')) { $hasVM = $null -ne $child.SelectSingleNode("dcsset:viewMode", $ns) $hasUS = $null -ne $child.SelectSingleNode("dcsset:userSettingID", $ns) - $shape[$tag] = "$(if ($hasVM) {'v'})$(if ($hasUS) {'u'})" + $code = "$(if ($hasVM) {'v'})$(if ($hasUS) {'u'})" + # Контейнер может нести собственный <dcsset:userSettingPresentation> (кастомная подпись + # настройки) — сохраняем форму по xsi:type (Get-PresByType: ru-only LocalString ≠ xs:string). + $uspNode = $child.SelectSingleNode("dcsset:userSettingPresentation", $ns) + if ($uspNode) { + $usp = Get-PresByType $uspNode + $shape[$tag] = [ordered]@{ meta = $code; presentation = $usp } + } else { $shape[$tag] = $code } } elseif ($tag -eq 'itemsViewMode') { $shape['itemsViewMode'] = $true } elseif ($tag -eq 'itemsUserSettingID') { $shape['itemsUserSettingID'] = $true } elseif ($tag -eq 'item') { if ($hasGrouping) { $shape['structure'] = $true } else { return $null } } diff --git a/docs/form-dsl-spec.md b/docs/form-dsl-spec.md index 28ab3256..3ec416fe 100644 --- a/docs/form-dsl-spec.md +++ b/docs/form-dsl-spec.md @@ -959,7 +959,7 @@ Forgiving-синонимы типа: XML-имя (`SpreadSheetDocumentField`) и Пустой блок настроек компоновщика (`ListSettings`) генерируется автоматически (каноничный полный скелет платформы — filter+order+conditionalAppearance+itemsViewMode+itemsUserSettingID, ~93% форм); указывать ничего не нужно. -| `listSettings` | object | **Дескриптор формы скелета `<ListSettings>`** — только для НЕ-каноничных (частичных/минимальных) форм. Ordered-карта present top-level элементов: контейнеры `filter`/`order`/`conditionalAppearance` → блок-мета (`"vu"`=viewMode+userSettingID, `"u"`=только userSettingID, `"v"`, `""`); `itemsViewMode`/`itemsUserSettingID` → `true`. Компилятор эмитит ТОЛЬКО указанные части (контент берёт из `filter`/`order`/`conditionalAppearance`). Нет ключа → полный каноничный скелет. Пустой объект `{}` → self-closing `<ListSettings/>` (оригинал без скелета). Декомпилятор пишет дескриптор только для отклонений от канона | +| `listSettings` | object | **Дескриптор формы скелета `<ListSettings>`** — только для НЕ-каноничных (частичных/минимальных) форм. Ordered-карта present top-level элементов: контейнеры `filter`/`order`/`conditionalAppearance` → блок-мета (`"vu"`=viewMode+userSettingID, `"u"`=только userSettingID, `"v"`, `""`); `itemsViewMode`/`itemsUserSettingID` → `true`. Если контейнер несёт собственный `userSettingPresentation` (кастомная подпись настройки), значение — объект `{ meta: "u", presentation: "Текст" | {ru,en} }` (presentation по форме значения: строка → `xs:string`, объект → `LocalStringType`). Компилятор эмитит ТОЛЬКО указанные части (контент берёт из `filter`/`order`/`conditionalAppearance`). Нет ключа → полный каноничный скелет. Пустой объект `{}` → self-closing `<ListSettings/>` (оригинал без скелета). Декомпилятор пишет дескриптор только для отклонений от канона | #### parameters — параметры схемы дин-списка