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$wrapTag>"
}
@@ -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}{tag}>")
-
-
-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)}{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 (напр. тип «Неопределено» = :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}{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' — это направление раскладки детей
-# (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}{tag}>')
-
-
-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}{tag}>')
-
-
-# Дополнения командной панели таблицы: тип 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}{m["tag"]}>')
-
-
-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}{m["tag"]}>')
-
-
-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}{tag}>")
- 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}{tag}>")
-
-
-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"}{tag}>')
- # Динамический заголовок колонки-группы из данных (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}{pic_tag}>")
-
-
-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))}{tag}>')
- 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"}{tag}>')
- else:
- v = str(el[key])
- if v == '':
- continue
- lines.append(f'{indent}<{tag}>{esc_xml(v)}{tag}>')
-
-
-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}{tag}>')
-
-
-# --- 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"}{tag}>')
- # Ограничение доступных типов (поле на составном типе): домен типов + явный набор.
- # 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]))}{tag}>')
- # 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]))}{tag}>')
- 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}')
- 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}')
-
- 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]}{tag}>')
-
- # Оформление (цвета/шрифты/граница) — перед компаньонами
- 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}{xml_tag}>')
-
-
-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}{tag}>')
-
-
-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}{pf}value>')
- 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{pf}value>')
- elif re.match(r'^decimal', t_bare):
- lines.append(f'{indent}<{pf}value xsi:type="xs:decimal">0{pf}value>')
- elif t_bare == 'boolean':
- lines.append(f'{indent}<{pf}value xsi:type="xs:boolean">false{pf}value>')
- 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}{cmd["action"]}')
-
- if cmd.get('modifiesSavedData') is True:
- lines.append(f'{inner}true')
-
- emit_functional_options(lines, cmd.get('functionalOptions'), inner)
-
- if cmd.get('currentRowUse'):
- lines.append(f'{inner}{cmd["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}{esc_xml(str(cmd_table))}')
-
- if cmd.get('shortcut'):
- lines.append(f'{inner}{cmd["shortcut"]}')
-
- emit_command_picture(lines, cmd.get('picture'), cmd.get('loadTransparent'), inner)
-
- if cmd.get('representation'):
- lines.append(f'{inner}{cmd["representation"]}')
-
- lines.append(f'{indent}\t')
- lines.append(f'{indent}')
-
-
-# Командный интерфейс формы (): панели 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}')
- 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')
- lines.append(f'{inner}\t\t{esc_xml(str(cmd))}')
- lines.append(f'{inner}\t\t{typ}')
- if attr:
- lines.append(f'{inner}\t\t{esc_xml(str(attr))}')
- if grp:
- lines.append(f'{inner}\t\t{esc_xml(str(grp))}')
- if idx is not None:
- lines.append(f'{inner}\t\t{idx}')
- if dv is not None:
- lines.append(f'{inner}\t\t{"true" if dv else "false"}')
- if vis is not None:
- emit_xr_flag(lines, 'Visible', vis, f'{inner}\t\t')
- lines.append(f'{inner}\t')
- lines.append(f'{inner}{tag}>')
- lines.append(f'{indent}')
-
-
-# --- Properties emitter ---
-
-PROP_MAP = {
- "autoTitle": "AutoTitle",
- "windowOpeningMode": "WindowOpeningMode",
- "commandBarLocation": "CommandBarLocation",
- "saveDataInSettings": "SaveDataInSettings",
- "autoSaveDataInSettings": "AutoSaveDataInSettings",
- "autoTime": "AutoTime",
- "usePostingMode": "UsePostingMode",
- "repostOnWrite": "RepostOnWrite",
- "autoURL": "AutoURL",
- "autoFillCheck": "AutoFillCheck",
- "customizable": "Customizable",
- "enterKeyBehavior": "EnterKeyBehavior",
- "verticalScroll": "VerticalScroll",
- "scalingMode": "ScalingMode",
- "useForFoldersAndItems": "UseForFoldersAndItems",
- "reportResult": "ReportResult",
- "detailsData": "DetailsData",
- "reportFormType": "ReportFormType",
- "autoShowState": "AutoShowState",
- "width": "Width",
- "height": "Height",
- "group": "Group",
-}
-
-
-def emit_properties(lines, props, indent):
- if not props:
- return
-
- for p_name, p_value in props.items():
- xml_name = PROP_MAP.get(p_name)
- if not xml_name:
- # Auto PascalCase
- xml_name = p_name[0].upper() + p_name[1:]
-
- # Пустая строка = суппресс-маркер (напр. 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']+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('')
- lines.append(f'')
-
- # --- 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'' not in raw_text:
- # Insert before
- if '' in raw_text:
- insert_line = f'\t\t\t\n'
- raw_text = raw_text.replace('', insert_line + '\t\t', 1)
- elif '' in raw_text:
- replacement = f'\n\t\t\t\n\t\t'
- raw_text = raw_text.replace('', replacement, 1)
-
- write_utf8_bom(object_xml_path, raw_text)
- print(f" Registered: 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):
+ # Экранирование ТЕКСТА элемента (, ): только & < > .
+ # Кавычки/апострофы в тексте 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}{tag}>")
+
+
+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)}{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 (напр. тип «Неопределено» = :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, block_user_setting_presentation=None):
+ has_items = bool(items) and len(items) > 0
+ has_block_meta = (block_view_mode is not None) or (block_user_setting_id is not None) or (block_user_setting_presentation is not None)
+ if not has_items and not has_block_meta:
+ return
+ lines.append(f'{indent}')
+ for item in (items or []):
+ if isinstance(item, str):
+ parsed = parse_filter_shorthand(item)
+ obj = {'field': parsed['field'], 'op': parsed['op']}
+ if parsed['use'] is False:
+ obj['use'] = False
+ if parsed['value'] is not None:
+ obj['value'] = parsed['value']
+ if parsed.get('valueType'):
+ obj['valueType'] = parsed['valueType']
+ if parsed.get('userSettingID'):
+ obj['userSettingID'] = parsed['userSettingID']
+ if parsed.get('viewMode'):
+ obj['viewMode'] = parsed['viewMode']
+ emit_filter_item(lines, obj, f'{indent}\t')
+ else:
+ emit_filter_item(lines, item, f'{indent}\t')
+ if block_view_mode is not None:
+ lines.append(f'{indent}\t{esc_xml(str(block_view_mode))}')
+ if block_user_setting_id is not None:
+ uid = new_uuid() if str(block_user_setting_id) == 'auto' else str(block_user_setting_id)
+ lines.append(f'{indent}\t{esc_xml(uid)}')
+ if block_user_setting_presentation is not None:
+ emit_us_presentation(lines, f'{indent}\t', 'dcsset:userSettingPresentation', block_user_setting_presentation)
+ lines.append(f'{indent}')
+
+
+def emit_order(lines, items, indent, skip_auto=False, block_view_mode=None, block_user_setting_id=None, block_user_setting_presentation=None):
+ has_items = bool(items) and len(items) > 0
+ has_block_meta = (block_view_mode is not None) or (block_user_setting_id is not None) or (block_user_setting_presentation is not None)
+ if not has_items and not has_block_meta:
+ return
+ lines.append(f'{indent}')
+ for item in (items or []):
+ if isinstance(item, str):
+ if item == 'Auto':
+ if not skip_auto:
+ lines.append(f'{indent}\t')
+ else:
+ parts = re.split(r'\s+', item)
+ field = parts[0]
+ direction = 'Asc'
+ if len(parts) > 1 and re.match(r'(?i)^(desc|убыв)', parts[1]):
+ direction = 'Desc'
+ elif len(parts) > 1 and re.match(r'(?i)^(asc|возр)', parts[1]):
+ direction = 'Asc'
+ lines.append(f'{indent}\t')
+ lines.append(f'{indent}\t\t{esc_xml(field)}')
+ lines.append(f'{indent}\t\t{direction}')
+ lines.append(f'{indent}\t')
+ else:
+ if item.get('field') == 'Auto' or item.get('type') == 'auto':
+ if not skip_auto:
+ lines.append(f'{indent}\t')
+ continue
+ direction = str(item['direction']) if item.get('direction') else 'Asc'
+ if re.match(r'(?i)^(desc|убыв)', direction):
+ direction = 'Desc'
+ elif re.match(r'(?i)^(asc|возр)', direction):
+ direction = 'Asc'
+ lines.append(f'{indent}\t')
+ if item.get('use') is False:
+ lines.append(f'{indent}\t\tfalse')
+ lines.append(f'{indent}\t\t{esc_xml(str(item.get("field", "")))}')
+ lines.append(f'{indent}\t\t{direction}')
+ if item.get('viewMode'):
+ lines.append(f'{indent}\t\t{esc_xml(str(item["viewMode"]))}')
+ lines.append(f'{indent}\t')
+ if block_view_mode is not None:
+ lines.append(f'{indent}\t{esc_xml(str(block_view_mode))}')
+ if block_user_setting_id is not None:
+ uid = new_uuid() if str(block_user_setting_id) == 'auto' else str(block_user_setting_id)
+ lines.append(f'{indent}\t{esc_xml(uid)}')
+ if block_user_setting_presentation is not None:
+ emit_us_presentation(lines, f'{indent}\t', 'dcsset:userSettingPresentation', block_user_setting_presentation)
+ lines.append(f'{indent}')
+
+
+def emit_appearance_value(lines, key, val, indent):
+ lines.append(f'{indent}')
+
+ def _has_key(o, k):
+ return isinstance(o, dict) and (k in o)
+
+ def _get(o, k):
+ return o.get(k) if isinstance(o, dict) else None
+
+ is_top_level_line = _has_key(val, '@type') and (str(_get(val, '@type')) == 'Line')
+ use_wrapper = False
+ inner_val = val
+ nested_items = None
+ if is_top_level_line:
+ if _has_key(val, 'use') and (_get(val, 'use') is False):
+ use_wrapper = True
+ if _has_key(val, 'items'):
+ nested_items = _get(val, 'items')
+ elif _has_key(val, 'value') and isinstance(val, dict):
+ inner_val = _get(val, 'value')
+ if _has_key(val, 'use') and (_get(val, 'use') is False):
+ use_wrapper = True
+ if _has_key(val, 'items'):
+ nested_items = _get(val, 'items')
+ if use_wrapper:
+ lines.append(f'{indent}\tfalse')
+ lines.append(f'{indent}\t{esc_xml(key)}')
+
+ is_font_dict = isinstance(inner_val, dict) and inner_val.get('@type') is not None and str(inner_val.get('@type')) == 'Font'
+ is_line_dict = _has_key(inner_val, '@type') and (str(_get(inner_val, '@type')) == 'Line')
+ is_dict = isinstance(inner_val, dict)
+ if is_line_dict:
+ lw = _get(inner_val, 'width') if _has_key(inner_val, 'width') else 0
+ lg = ('true' if _get(inner_val, 'gap') else 'false') if _has_key(inner_val, 'gap') else 'false'
+ ls = str(_get(inner_val, 'style')) if _has_key(inner_val, 'style') else 'None'
+ lines.append(f'{indent}\t')
+ lines.append(f'{indent}\t\t{esc_xml(ls)}')
+ lines.append(f'{indent}\t')
+ elif is_font_dict:
+ attr_parts = []
+ for attr_name in ('ref', 'faceName', 'height', 'bold', 'italic', 'underline', 'strikeout', 'kind', 'scale'):
+ if attr_name in inner_val:
+ av = inner_val[attr_name]
+ if av is not None:
+ attr_parts.append(f'{attr_name}="{esc_xml(str(av))}"')
+ lines.append(f'{indent}\t')
+ elif is_dict and _has_key(inner_val, 'field'):
+ # Ссылка на поле (dcscor:Field) — значение параметра оформления = поле компоновки
+ lines.append(f'{indent}\t{esc_xml(str(_get(inner_val, "field")))}')
+ elif is_dict:
+ # Локализуемый текст параметра оформления: платформа объявляет xsi:type на dcscor:value
+ emit_mltext(lines, f'{indent}\t', 'dcscor:value', inner_val, xsi_type='v8:LocalStringType')
+ else:
+ actual_val = str(inner_val)
+ key_type_map = {
+ 'Размещение': 'dcscor:DataCompositionTextPlacementType',
+ 'ГоризонтальноеПоложение': 'v8ui:HorizontalAlign',
+ 'ВертикальноеПоложение': 'v8ui:VerticalAlign',
+ 'ОриентацияТекста': 'xs:decimal',
+ 'РасположениеИтогов': 'dcscor:DataCompositionTotalPlacement',
+ 'ТипМакета': 'dcsset:DataCompositionGroupTemplateType',
+ }
+ key_type = key_type_map.get(key)
+ if key_type:
+ lines.append(f'{indent}\t{esc_xml(actual_val)}')
+ elif re.match(r'^(style|web|win):', actual_val):
+ lines.append(f'{indent}\t{esc_xml(actual_val)}')
+ elif actual_val == 'true' or actual_val == 'false':
+ lines.append(f'{indent}\t{actual_val}')
+ elif key == 'Текст' or key == 'Заголовок' or key == 'Формат':
+ # Голая строка = плоский xs:string (нелокализованный литерал). Локализуемый → объект {ru,en}.
+ # Пустая строка → самозакрывающийся тег (как у платформы).
+ if actual_val == '':
+ lines.append(f'{indent}\t')
+ else:
+ lines.append(f'{indent}\t{esc_xml(actual_val)}')
+ elif re.match(r'^-?\d+(\.\d+)?$', actual_val):
+ lines.append(f'{indent}\t{actual_val}')
+ elif key == 'ЦветТекста' or key == 'ЦветФона' or key == 'ЦветГраницы':
+ lines.append(f'{indent}\t{esc_xml(actual_val)}')
+ else:
+ lines.append(f'{indent}\t{esc_xml(actual_val)}')
+ if nested_items:
+ if isinstance(nested_items, dict):
+ for nk, nv in nested_items.items():
+ emit_appearance_value(lines, nk, nv, f'{indent}\t')
+ lines.append(f'{indent}')
+
+
+# === Группировка строк динамического списка (DCS-структура ListSettings) ===
+# Линейная цепочка (каждый уровень = одно поле в groupItems;
+# вложенность — через дочерний ). Плоская модель уровней (список всегда линеен).
+def get_list_grouping_value(s):
+ for k in ('grouping', 'structure', 'группировка'):
+ if s.get(k):
+ return s[k]
+ return None
+
+
+def parse_list_grouping(grouping):
+ # Шорткат "A > B > C" → массив имён; массив строк/объектов → как есть.
+ if not grouping:
+ return []
+ if isinstance(grouping, str):
+ return [p.strip() for p in re.split(r'\s*>\s*', grouping) if p.strip()]
+ return list(grouping)
+
+
+def emit_group_item_field(lines, level, indent):
+ if isinstance(level, str):
+ field, gt, pat = level, 'Items', 'None'
+ pab = pae = '0001-01-01T00:00:00'
+ else:
+ field = str(level.get('field', ''))
+ gt = str(level.get('groupType') or 'Items')
+ pat = str(level.get('periodAdditionType') or 'None')
+ pab = str(level.get('periodAdditionBegin') or '0001-01-01T00:00:00')
+ pae = str(level.get('periodAdditionEnd') or '0001-01-01T00:00:00')
+ lines.append(f'{indent}')
+ lines.append(f'{indent}\t{esc_xml(field)}')
+ lines.append(f'{indent}\t{esc_xml(gt)}')
+ lines.append(f'{indent}\t{esc_xml(pat)}')
+ # Авто-детект: ISO-дата → xs:dateTime, иначе путь → dcscor:Field.
+ pab_t = 'xs:dateTime' if re.match(r'^\d{4}-\d{2}-\d{2}T', pab) else 'dcscor:Field'
+ pae_t = 'xs:dateTime' if re.match(r'^\d{4}-\d{2}-\d{2}T', pae) else 'dcscor:Field'
+ lines.append(f'{indent}\t{esc_xml(pab)}')
+ lines.append(f'{indent}\t{esc_xml(pae)}')
+ lines.append(f'{indent}')
+
+
+def emit_list_grouping_levels(lines, levels, i, indent):
+ lines.append(f'{indent}')
+ lines.append(f'{indent}\t')
+ emit_group_item_field(lines, levels[i], f'{indent}\t\t')
+ lines.append(f'{indent}\t')
+ if i < len(levels) - 1:
+ emit_list_grouping_levels(lines, levels, i + 1, f'{indent}\t')
+ lines.append(f'{indent}')
+
+
+def emit_list_grouping(lines, grouping, indent):
+ levels = parse_list_grouping(grouping)
+ if not levels:
+ return
+ emit_list_grouping_levels(lines, levels, 0, indent)
+
+
+# === Вычисляемые поля DataSet динамического списка () ===
+# Зеркало skd calculatedFields: shorthand "Имя [Заголовок]: тип = Выражение #noField #noFilter
+# #noGroup #noOrder" или объект. Форм-специфика: dcssch:-теги + presentationExpression/orderExpression.
+_CALC_RESTRICT_MAP = {'noField': 'field', 'noFilter': 'condition', 'noCondition': 'condition',
+ 'noGroup': 'group', 'noOrder': 'order'}
+_DCS_COMMON_NS = 'http://v8.1c.ru/8.1/data-composition-system/common'
+
+
+def parse_calc_shorthand(s):
+ restrict = re.findall(r'#(noField|noFilter|noCondition|noGroup|noOrder)\b', s)
+ s = re.sub(r'\s*#(?:noField|noFilter|noCondition|noGroup|noOrder)\b', '', s)
+ eq = s.find('=')
+ lhs, rhs = (s[:eq], s[eq + 1:].strip()) if eq > 0 else (s, '')
+ title = ''
+ m = re.search(r'\[([^\]]+)\]', lhs)
+ if m:
+ title = m.group(1)
+ lhs = re.sub(r'\s*\[[^\]]+\]', '', lhs)
+ lhs = lhs.strip()
+ typ, data_path = '', lhs
+ if ':' in lhs:
+ data_path, t = lhs.split(':', 1)
+ data_path, typ = data_path.strip(), resolve_type_str(t.strip())
+ return {'dataPath': data_path, 'expression': rhs, 'type': typ, 'title': title, 'restrict': restrict}
+
+
+def emit_calc_fields(lines, calc_fields, indent):
+ if not calc_fields:
+ return
+ for cf in calc_fields:
+ if isinstance(cf, str):
+ p = parse_calc_shorthand(cf)
+ data_path, expression, title = p['dataPath'], p['expression'], p['title']
+ type_str = p['type']
+ restrict = [_CALC_RESTRICT_MAP[r] for r in p['restrict'] if r in _CALC_RESTRICT_MAP]
+ pres_expr = order_expr = None
+ else:
+ data_path = str(cf.get('dataPath') or cf.get('field') or cf.get('name', ''))
+ expression = str(cf.get('expression', ''))
+ title = cf.get('title')
+ type_str = cf.get('valueType') or cf.get('type')
+ type_str = str(type_str) if type_str else None
+ ur = cf.get('useRestriction') or cf.get('restrict')
+ if isinstance(ur, dict):
+ restrict = [k for k in ('field', 'condition', 'group', 'order') if ur.get(k)]
+ elif isinstance(ur, str):
+ restrict = [_CALC_RESTRICT_MAP.get(t.strip().lstrip('#'), t.strip().lstrip('#')) for t in ur.split() if t.strip()]
+ elif isinstance(ur, list):
+ restrict = [_CALC_RESTRICT_MAP.get(str(r), str(r)) for r in ur]
+ else:
+ restrict = []
+ pres_expr = cf.get('presentationExpression')
+ order_expr = cf.get('orderExpression')
+ ci = f'{indent}\t'
+ lines.append(f'{indent}')
+ lines.append(f'{ci}{esc_xml(data_path)}')
+ lines.append(f'{ci}{esc_xml(expression)}')
+ if title:
+ emit_mltext(lines, ci, 'dcssch:title', title, xsi_type='v8:LocalStringType')
+ if restrict:
+ lines.append(f'{ci}')
+ for r in ('field', 'condition', 'group', 'order'):
+ if r in restrict:
+ lines.append(f'{ci}\ttrue')
+ lines.append(f'{ci}')
+ if pres_expr:
+ lines.append(f'{ci}{esc_xml(str(pres_expr))}')
+ if order_expr:
+ for oe in (order_expr if isinstance(order_expr, list) else [order_expr]):
+ if isinstance(oe, str):
+ expr_v, otype, auto = oe, 'Asc', 'false'
+ else:
+ expr_v = str(oe.get('expression', ''))
+ otype = str(oe.get('orderType', 'Asc'))
+ auto = 'true' if oe.get('autoOrder') else 'false'
+ lines.append(f'{ci}')
+ lines.append(f'{ci}\t{esc_xml(expr_v)}')
+ lines.append(f'{ci}\t{otype}')
+ lines.append(f'{ci}\t{auto}')
+ lines.append(f'{ci}')
+ if type_str:
+ emit_dl_value_type(lines, type_str, ci)
+ lines.append(f'{indent}')
+
+
+# Ограничения использования поля/вычисляемого поля (useRestriction / attributeUseRestriction).
+# Значение: объект {field?,condition?,group?,order?} | флаг-строка "#noField …" | массив.
+def parse_restrict(ur):
+ if not ur:
+ return []
+ if isinstance(ur, dict):
+ return [k for k in ('field', 'condition', 'group', 'order') if ur.get(k)]
+ if isinstance(ur, str):
+ return [_CALC_RESTRICT_MAP.get(t.strip().lstrip('#'), t.strip().lstrip('#')) for t in ur.split() if t.strip()]
+ if isinstance(ur, list):
+ return [_CALC_RESTRICT_MAP.get(str(r), str(r)) for r in ur]
+ return []
+
+
+def emit_restrict_block(lines, tag, ur, indent):
+ r = parse_restrict(ur)
+ if not r:
+ return
+ lines.append(f'{indent}')
+ for k in ('field', 'condition', 'group', 'order'):
+ if k in r:
+ lines.append(f'{indent}\ttrue')
+ lines.append(f'{indent}')
+
+
+def emit_conditional_appearance(lines, items, indent, block_view_mode=None, block_user_setting_id=None, wrap_tag='dcsset:conditionalAppearance', block_user_setting_presentation=None):
+ has_items = bool(items) and len(items) > 0
+ has_block_meta = (block_view_mode is not None) or (block_user_setting_id is not None) or (block_user_setting_presentation is not None)
+ if not has_items and not has_block_meta:
+ return
+ lines.append(f'{indent}<{wrap_tag}>')
+ for ca in (items or []):
+ lines.append(f'{indent}\t')
+ if ca.get('use') is False:
+ lines.append(f'{indent}\t\tfalse')
+ if ca.get('selection') and len(ca['selection']) > 0:
+ lines.append(f'{indent}\t\t')
+ for sel in ca['selection']:
+ lines.append(f'{indent}\t\t\t')
+ lines.append(f'{indent}\t\t\t\t{esc_xml(str(sel))}')
+ lines.append(f'{indent}\t\t\t')
+ lines.append(f'{indent}\t\t')
+ else:
+ lines.append(f'{indent}\t\t')
+ if ca.get('filter') and len(ca['filter']) > 0:
+ emit_filter(lines, ca['filter'], f'{indent}\t\t')
+ else:
+ lines.append(f'{indent}\t\t')
+ if ca.get('appearance'):
+ lines.append(f'{indent}\t\t')
+ for k, v in ca['appearance'].items():
+ emit_appearance_value(lines, k, v, f'{indent}\t\t\t')
+ lines.append(f'{indent}\t\t')
+ if ca.get('presentation'):
+ if isinstance(ca['presentation'], dict):
+ # Мультиязык → LocalStringType (платформа объявляет тип у локализованного presentation)
+ lines.append(f'{indent}\t\t')
+ emit_ml_items(lines, f'{indent}\t\t\t', ca['presentation'])
+ lines.append(f'{indent}\t\t')
+ else:
+ lines.append(f'{indent}\t\t{esc_xml(str(ca["presentation"]))}')
+ if ca.get('viewMode'):
+ lines.append(f'{indent}\t\t{esc_xml(str(ca["viewMode"]))}')
+ if ca.get('userSettingID'):
+ uid = new_uuid() if str(ca['userSettingID']) == 'auto' else str(ca['userSettingID'])
+ lines.append(f'{indent}\t\t{esc_xml(uid)}')
+ if ca.get('userSettingPresentation'):
+ emit_us_presentation(lines, f'{indent}\t\t', 'dcsset:userSettingPresentation', ca['userSettingPresentation'])
+ if ca.get('useInDontUse') and len(ca['useInDontUse']) > 0:
+ use_in_order = ['group', 'hierarchicalGroup', 'overall', 'fieldsHeader', 'header',
+ 'parameters', 'filter', 'resourceFieldsHeader', 'overallHeader',
+ 'overallResourceFieldsHeader']
+ sset = {str(n): True for n in ca['useInDontUse']}
+ for n in use_in_order:
+ if n in sset:
+ tag = 'useIn' + n[0].upper() + n[1:]
+ lines.append(f'{indent}\t\tDontUse')
+ lines.append(f'{indent}\t')
+ if block_view_mode is not None:
+ lines.append(f'{indent}\t{esc_xml(str(block_view_mode))}')
+ if block_user_setting_id is not None:
+ uid = new_uuid() if str(block_user_setting_id) == 'auto' else str(block_user_setting_id)
+ lines.append(f'{indent}\t{esc_xml(uid)}')
+ if block_user_setting_presentation is not None:
+ emit_us_presentation(lines, f'{indent}\t', 'dcsset:userSettingPresentation', block_user_setting_presentation)
+ lines.append(f'{indent}{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' — это направление раскладки детей
+# (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}{tag}>')
+
+
+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}{tag}>')
+
+
+# Дополнения командной панели таблицы: тип 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}{m["tag"]}>')
+
+
+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}{m["tag"]}>')
+
+
+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}{tag}>")
+ 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}{tag}>")
+
+
+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"}{tag}>')
+ # Динамический заголовок колонки-группы из данных (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}{pic_tag}>")
+
+
+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))}{tag}>')
+ 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"}{tag}>')
+ else:
+ v = str(el[key])
+ if v == '':
+ continue
+ lines.append(f'{indent}<{tag}>{esc_xml(v)}{tag}>')
+
+
+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}{tag}>')
+
+
+# --- 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"}{tag}>')
+ # Ограничение доступных типов (поле на составном типе): домен типов + явный набор.
+ # 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]))}{tag}>')
+ # 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]))}{tag}>')
+ 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}')
+ 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}')
+
+ 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]}{tag}>')
+
+ # Оформление (цвета/шрифты/граница) — перед компаньонами
+ 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}{xml_tag}>')
+
+
+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}{tag}>')
+
+
+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}{pf}value>')
+ 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{pf}value>')
+ elif re.match(r'^decimal', t_bare):
+ lines.append(f'{indent}<{pf}value xsi:type="xs:decimal">0{pf}value>')
+ elif t_bare == 'boolean':
+ lines.append(f'{indent}<{pf}value xsi:type="xs:boolean">false{pf}value>')
+ 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}{cmd["action"]}')
+
+ if cmd.get('modifiesSavedData') is True:
+ lines.append(f'{inner}true')
+
+ emit_functional_options(lines, cmd.get('functionalOptions'), inner)
+
+ if cmd.get('currentRowUse'):
+ lines.append(f'{inner}{cmd["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}{esc_xml(str(cmd_table))}')
+
+ if cmd.get('shortcut'):
+ lines.append(f'{inner}{cmd["shortcut"]}')
+
+ emit_command_picture(lines, cmd.get('picture'), cmd.get('loadTransparent'), inner)
+
+ if cmd.get('representation'):
+ lines.append(f'{inner}{cmd["representation"]}')
+
+ lines.append(f'{indent}\t')
+ lines.append(f'{indent}')
+
+
+# Командный интерфейс формы (): панели 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}')
+ 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')
+ lines.append(f'{inner}\t\t{esc_xml(str(cmd))}')
+ lines.append(f'{inner}\t\t{typ}')
+ if attr:
+ lines.append(f'{inner}\t\t{esc_xml(str(attr))}')
+ if grp:
+ lines.append(f'{inner}\t\t{esc_xml(str(grp))}')
+ if idx is not None:
+ lines.append(f'{inner}\t\t{idx}')
+ if dv is not None:
+ lines.append(f'{inner}\t\t{"true" if dv else "false"}')
+ if vis is not None:
+ emit_xr_flag(lines, 'Visible', vis, f'{inner}\t\t')
+ lines.append(f'{inner}\t')
+ lines.append(f'{inner}{tag}>')
+ lines.append(f'{indent}')
+
+
+# --- Properties emitter ---
+
+PROP_MAP = {
+ "autoTitle": "AutoTitle",
+ "windowOpeningMode": "WindowOpeningMode",
+ "commandBarLocation": "CommandBarLocation",
+ "saveDataInSettings": "SaveDataInSettings",
+ "autoSaveDataInSettings": "AutoSaveDataInSettings",
+ "autoTime": "AutoTime",
+ "usePostingMode": "UsePostingMode",
+ "repostOnWrite": "RepostOnWrite",
+ "autoURL": "AutoURL",
+ "autoFillCheck": "AutoFillCheck",
+ "customizable": "Customizable",
+ "enterKeyBehavior": "EnterKeyBehavior",
+ "verticalScroll": "VerticalScroll",
+ "scalingMode": "ScalingMode",
+ "useForFoldersAndItems": "UseForFoldersAndItems",
+ "reportResult": "ReportResult",
+ "detailsData": "DetailsData",
+ "reportFormType": "ReportFormType",
+ "autoShowState": "AutoShowState",
+ "width": "Width",
+ "height": "Height",
+ "group": "Group",
+}
+
+
+def emit_properties(lines, props, indent):
+ if not props:
+ return
+
+ for p_name, p_value in props.items():
+ xml_name = PROP_MAP.get(p_name)
+ if not xml_name:
+ # Auto PascalCase
+ xml_name = p_name[0].upper() + p_name[1:]
+
+ # Пустая строка = суппресс-маркер (напр. 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']+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('')
+ lines.append(f'')
+
+ # --- 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'' not in raw_text:
+ # Insert before
+ if '' in raw_text:
+ insert_line = f'\t\t\t\n'
+ raw_text = raw_text.replace('', insert_line + '\t\t', 1)
+ elif '' in raw_text:
+ replacement = f'\n\t\t\t\n\t\t'
+ raw_text = raw_text.replace('', replacement, 1)
+
+ write_utf8_bom(object_xml_path, raw_text)
+ print(f" Registered: 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'})"
+ # Контейнер может нести собственный (кастомная подпись
+ # настройки) — сохраняем форму по 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 | **Дескриптор формы скелета ``** — только для НЕ-каноничных (частичных/минимальных) форм. Ordered-карта present top-level элементов: контейнеры `filter`/`order`/`conditionalAppearance` → блок-мета (`"vu"`=viewMode+userSettingID, `"u"`=только userSettingID, `"v"`, `""`); `itemsViewMode`/`itemsUserSettingID` → `true`. Компилятор эмитит ТОЛЬКО указанные части (контент берёт из `filter`/`order`/`conditionalAppearance`). Нет ключа → полный каноничный скелет. Пустой объект `{}` → self-closing `` (оригинал без скелета). Декомпилятор пишет дескриптор только для отклонений от канона |
+| `listSettings` | object | **Дескриптор формы скелета ``** — только для НЕ-каноничных (частичных/минимальных) форм. 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 `` (оригинал без скелета). Декомпилятор пишет дескриптор только для отклонений от канона |
#### parameters — параметры схемы дин-списка