Files
cc-1c-skills/.codeassistant/skills/form-compile/scripts/form-compile.py
T
2026-06-14 11:08:54 +00:00

6452 lines
334 KiB
Python
Raw Blame History

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